114 lines
4.0 KiB
TypeScript
114 lines
4.0 KiB
TypeScript
import type { DevToolbarRule } from "./types.ts";
|
||
import { createIssue } from "./helpers.ts";
|
||
|
||
export const seoRules: DevToolbarRule[] = [
|
||
{
|
||
id: "seo/document",
|
||
category: "seo",
|
||
defaultSeverity: "warning",
|
||
description: "Validates core document metadata.",
|
||
run: ({ document }) => {
|
||
const issues = [];
|
||
const title = document.title.trim();
|
||
if (!title)
|
||
issues.push(
|
||
createIssue({
|
||
ruleId: "seo/title-missing",
|
||
category: "seo",
|
||
severity: "error",
|
||
title: "Page title is missing",
|
||
message: "The document does not have a useful title.",
|
||
recommendation: "Add a unique title describing this page.",
|
||
}),
|
||
);
|
||
else if (title.length < 20 || title.length > 65)
|
||
issues.push(
|
||
createIssue({
|
||
ruleId: "seo/title-length",
|
||
category: "seo",
|
||
severity: "warning",
|
||
title: "Page title length may be suboptimal",
|
||
message: `The title contains ${title.length} characters.`,
|
||
recommendation: "Keep most titles between about 20 and 65 characters.",
|
||
confidence: "medium",
|
||
}),
|
||
);
|
||
const description = document
|
||
.querySelector<HTMLMetaElement>('meta[name="description"]')
|
||
?.content.trim();
|
||
if (!description)
|
||
issues.push(
|
||
createIssue({
|
||
ruleId: "seo/description-missing",
|
||
category: "seo",
|
||
severity: "warning",
|
||
title: "Meta description is missing",
|
||
message: "Search and social previews may not have a useful description.",
|
||
recommendation: "Add a unique meta description for the page.",
|
||
}),
|
||
);
|
||
else if (description.length < 70 || description.length > 170)
|
||
issues.push(
|
||
createIssue({
|
||
ruleId: "seo/description-length",
|
||
category: "seo",
|
||
severity: "suggestion",
|
||
title: "Meta description length may be suboptimal",
|
||
message: `The description contains ${description.length} characters.`,
|
||
recommendation: "Aim for a concise description of roughly 70–170 characters.",
|
||
confidence: "medium",
|
||
}),
|
||
);
|
||
if (!document.querySelector('meta[name="viewport"]'))
|
||
issues.push(
|
||
createIssue({
|
||
ruleId: "seo/viewport-missing",
|
||
category: "seo",
|
||
severity: "error",
|
||
title: "Viewport meta tag is missing",
|
||
message: "The page may render incorrectly on mobile devices.",
|
||
recommendation: "Add width=device-width, initial-scale=1.",
|
||
}),
|
||
);
|
||
if (!document.querySelector('link[rel="canonical"]'))
|
||
issues.push(
|
||
createIssue({
|
||
ruleId: "seo/canonical-missing",
|
||
category: "seo",
|
||
severity: "suggestion",
|
||
title: "Canonical URL is missing",
|
||
message: "Search engines have no explicit preferred URL for this page.",
|
||
recommendation: "Add a canonical link for public indexable pages.",
|
||
confidence: "medium",
|
||
}),
|
||
);
|
||
const h1s = document.querySelectorAll("h1");
|
||
if (h1s.length === 0)
|
||
issues.push(
|
||
createIssue({
|
||
ruleId: "seo/h1-missing",
|
||
category: "seo",
|
||
severity: "warning",
|
||
title: "Page has no h1 heading",
|
||
message: "The page lacks a clear primary heading.",
|
||
recommendation: "Add one descriptive h1.",
|
||
}),
|
||
);
|
||
if (h1s.length > 1)
|
||
issues.push(
|
||
createIssue({
|
||
ruleId: "seo/multiple-h1",
|
||
category: "seo",
|
||
severity: "suggestion",
|
||
title: "Page has multiple h1 headings",
|
||
message: `Found ${h1s.length} h1 elements.`,
|
||
recommendation:
|
||
"Use one clear primary page heading unless multiple h1 elements are intentional.",
|
||
confidence: "medium",
|
||
}),
|
||
);
|
||
return issues;
|
||
},
|
||
},
|
||
];
|