metatroncubeswdev 510c46e5ed
Some checks failed
CI / ci (push) Has been cancelled
CI / docker-build (push) Has been cancelled
Publish Docker image / docker (push) Has been cancelled
Upload sourcemaps / upload (push) Has been cancelled
Add a real, rate-limited public quick-audit endpoint for crawlerx.co
The marketing site's "Run Live Audit" widget was entirely fake: it looked
up a canned preset or a generic templated result for any domain typed in,
after a scripted delay. This adds /api/public/quick-audit, which crawls
the submitted URL with the same crawler + HTML analyzer the authenticated
product uses, and reports one real finding (or an honest error if the
site is unreachable/blocked) instead of ever fabricating one. Per-IP and
global daily KV caps bound the cost of anonymous abuse.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-20 19:19:12 -04:00

142 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<QuickAuditFinding, "domain" | "score"> & { 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 <title> 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</title>`,
});
} 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>${title}</title>`,
suggested: `<title>${title.slice(0, TITLE_MAX_LENGTH - 1).trim()}…</title>`,
});
}
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: '<!-- no <meta name="description"> tag found -->',
suggested: `<meta name="description" content="Describe what ${domain} offers in one or two sentences, under ${DESCRIPTION_MAX_LENGTH} characters." />`,
});
} 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: `<meta name="description" content="${description}" />`,
suggested: `<meta name="description" content="${description.slice(0, DESCRIPTION_MAX_LENGTH - 1).trim()}…" />`,
});
}
if (page.h1Count === 0) {
issues.push({
weight: 18,
action: "Add an H1 heading",
issue:
"No <h1> tag found on this page — a single clear H1 helps both users and search engines understand the page topic.",
priority: "High Priority",
current: "<!-- no <h1> tag found -->",
suggested: "<h1>State the page's main topic here</h1>",
});
} else if (page.h1Count > 1) {
issues.push({
weight: 10,
action: "Use a single H1 per page",
issue: `${page.h1Count} <h1> tags found — multiple H1s dilute the page's topical signal.`,
priority: "High Priority",
current: `${page.h1Count}× <h1>…</h1>`,
suggested: "Keep exactly one <h1>; demote the rest to <h2>.",
});
}
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: '<!-- no <link rel="canonical"> tag found -->',
suggested: `<link rel="canonical" href="${page.url}" />`,
});
}
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: '<img src="...">',
suggested: '<img src="..." alt="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>${title}</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 };
}