metatron-open-seo/src/server/workflows/site-audit-workflow-helpers.ts
Ben Senescu 638f5a6602 refactor: move lighthouse audits to dataforseo (#43)
* refactor: move lighthouse audits to dataforseo

* chore: remove obsolete audit settings modal

* refactor: rename psi flows to lighthouse

* save

* refactor: simplify audit lighthouse storage flow

* fix: separate lighthouse metrics from actionable audits

* refactor: remove redundant audit project inputs

* feat: redesign lighthouse issues screen with score gauges and table layout

Replace flat score cards with circular SVG gauges, condense metrics into
a compact grid, and switch issue list from cards to an expandable table
with fixed column widths.

* test: harden lighthouse regression coverage

* fix: restore project-scoped audit inputs

* refactor: simplify lighthouse payload handling

* refactor: inline lighthouse server handlers

* refactor: share audit workflow types

* refactor: simplify lighthouse payload flows

* save

* refactor: drop project pagespeed api key

* fix: restore lighthouse issues loading with resilient project context

* fix: restore audit issues back navigation

* refactor: simplify project context and lighthouse error handling

* fix: tolerate DataForSEO lighthouse payload drift

* refactor: route audit lighthouse through dataforseo client
2026-03-26 20:25:27 -04:00

126 lines
3.5 KiB
TypeScript

import { analyzeHtml } from "@/server/lib/audit/page-analyzer";
import type { StepPageResult } from "@/server/lib/audit/types";
import { isSameOrigin, normalizeUrl } from "@/server/lib/audit/url-utils";
export async function crawlPage(
url: string,
crawlOrigin: string,
): Promise<StepPageResult | null> {
const startTime = Date.now();
try {
const response = await fetch(url, {
headers: {
"User-Agent": "OpenSEO-Audit/1.0",
Accept: "text/html,application/xhtml+xml",
},
redirect: "follow",
signal: AbortSignal.timeout(15_000),
});
const responseTimeMs = Date.now() - startTime;
const statusCode = response.status;
const finalUrl = normalizeUrl(response.url) ?? response.url;
if (!isSameOrigin(finalUrl, crawlOrigin)) return null;
const redirectUrl =
response.redirected && response.url !== url ? response.url : null;
const contentType = response.headers.get("content-type") ?? "";
if (!contentType.includes("text/html")) {
return emptyPageResult(finalUrl, statusCode, redirectUrl, responseTimeMs);
}
const html = await response.text();
const analysis = analyzeHtml(
html,
finalUrl,
statusCode,
responseTimeMs,
redirectUrl,
);
const isIndexable = !(
analysis.robotsMeta?.toLowerCase().includes("noindex") ?? false
);
const h2Count = analysis.headingOrder.filter((h) => h === 2).length;
const h3Count = analysis.headingOrder.filter((h) => h === 3).length;
const h4Count = analysis.headingOrder.filter((h) => h === 4).length;
const h5Count = analysis.headingOrder.filter((h) => h === 5).length;
const h6Count = analysis.headingOrder.filter((h) => h === 6).length;
return {
id: crypto.randomUUID(),
url: finalUrl,
statusCode,
redirectUrl,
title: analysis.title,
metaDescription: analysis.metaDescription,
canonicalUrl: analysis.canonical,
robotsMeta: analysis.robotsMeta,
ogTitle: analysis.ogTitle,
ogDescription: analysis.ogDescription,
ogImage: analysis.ogImage,
h1Count: analysis.h1s.length,
h2Count,
h3Count,
h4Count,
h5Count,
h6Count,
headingOrder: analysis.headingOrder,
wordCount: analysis.wordCount,
imagesTotal: analysis.images.length,
imagesMissingAlt: analysis.images.filter(
(img) => !img.alt || img.alt === "",
).length,
images: analysis.images,
internalLinks: analysis.internalLinks,
externalLinks: analysis.externalLinks,
hasStructuredData: analysis.hasStructuredData,
hreflangTags: analysis.hreflangTags,
isIndexable,
responseTimeMs,
};
} catch (error) {
const responseTimeMs = Date.now() - startTime;
console.warn(`Failed to crawl ${url}:`, error);
return emptyPageResult(url, 0, null, responseTimeMs);
}
}
function emptyPageResult(
url: string,
statusCode: number,
redirectUrl: string | null,
responseTimeMs: number,
): StepPageResult {
return {
id: crypto.randomUUID(),
url,
statusCode,
redirectUrl,
title: "",
metaDescription: "",
canonicalUrl: null,
robotsMeta: null,
ogTitle: null,
ogDescription: null,
ogImage: null,
h1Count: 0,
h2Count: 0,
h3Count: 0,
h4Count: 0,
h5Count: 0,
h6Count: 0,
headingOrder: [],
wordCount: 0,
imagesTotal: 0,
imagesMissingAlt: 0,
images: [],
internalLinks: [],
externalLinks: [],
hasStructuredData: false,
hreflangTags: [],
isIndexable: false,
responseTimeMs,
};
}