import type { CrawledPageResult } from "@/server/lib/audit/types"; export type QuickAuditPriority = "Critical Impact" | "High Priority" | "Good Health"; export interface QuickAuditFinding { domain: string; score: number; action: string; issue: string; priority: QuickAuditPriority; current: string; suggested: string; } const TITLE_MAX_LENGTH = 60; const DESCRIPTION_MAX_LENGTH = 160; type Issue = Omit & { weight: number }; /** * Picks the single most-impactful real issue on a crawled page, plus a * composite score from every issue found. Used by the public marketing-site * "quick audit" widget, which crawls exactly one real page (via * {@link import("@/server/workflows/site-audit-workflow-helpers").crawlPage}) * — this never fabricates a finding for a page it hasn't actually fetched. */ export function selectQuickAuditFinding( domain: string, page: CrawledPageResult, ): QuickAuditFinding { const title = page.title.trim(); const description = page.metaDescription.trim(); const issues: Issue[] = []; if (!title) { issues.push({ weight: 30, action: "Add a title tag", issue: "This page has no tag — search engines fall back to a generic or URL-based title in results.", priority: "Critical Impact", current: "<!-- no <title> tag found -->", suggested: `<title>${domain} | Describe this page in 50-60 characters`, }); } else if (title.length > TITLE_MAX_LENGTH) { issues.push({ weight: 15, action: "Shorten the title tag", issue: `Title tag is ${title.length} characters — Google truncates titles past ~${TITLE_MAX_LENGTH} characters in search results.`, priority: "High Priority", current: `${title}`, suggested: `${title.slice(0, TITLE_MAX_LENGTH - 1).trim()}…`, }); } if (!description) { issues.push({ weight: 20, action: "Add a meta description", issue: "No meta description found — Google will auto-generate a snippet from page content instead.", priority: "High Priority", current: '', suggested: ``, }); } else if (description.length > DESCRIPTION_MAX_LENGTH) { issues.push({ weight: 8, action: "Shorten the meta description", issue: `Meta description is ${description.length} characters — Google truncates snippets past ~${DESCRIPTION_MAX_LENGTH} characters.`, priority: "High Priority", current: ``, suggested: ``, }); } if (page.h1Count === 0) { issues.push({ weight: 18, action: "Add an H1 heading", issue: "No

tag found on this page — a single clear H1 helps both users and search engines understand the page topic.", priority: "High Priority", current: "", suggested: "

State the page's main topic here

", }); } else if (page.h1Count > 1) { issues.push({ weight: 10, action: "Use a single H1 per page", issue: `${page.h1Count}

tags found — multiple H1s dilute the page's topical signal.`, priority: "High Priority", current: `${page.h1Count}×

…

`, suggested: "Keep exactly one

; demote the rest to

.", }); } if (!page.canonicalUrl) { issues.push({ weight: 10, action: "Add a canonical tag", issue: "No canonical URL found — without one, duplicate or parameterized versions of this page can compete with each other in search results.", priority: "High Priority", current: '', suggested: ``, }); } if (page.imagesMissingAlt > 0) { issues.push({ weight: 6, action: "Add alt text to images", issue: `${page.imagesMissingAlt} of ${page.imagesTotal} images on this page are missing alt text.`, priority: "Good Health", current: '', suggested: 'Describe what the image shows', }); } if (issues.length === 0) { return { domain, score: 96, action: "No critical issues found on this quick scan", issue: "Title, meta description, H1, and canonical tag all look healthy on this page.", priority: "Good Health", current: `${title}`, suggested: "Run a full crawl in CrawlerX to check Core Web Vitals, internal linking, and every other page on the site.", }; } issues.sort((a, b) => b.weight - a.weight); const totalPenalty = issues.reduce((sum, issue) => sum + issue.weight, 0); const score = Math.max(35, 100 - totalPenalty); const { weight: _weight, ...top } = issues[0]; return { domain, score, ...top }; }