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('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; }, }, ];