/**
* HTML page analyzer using htmlparser2's streaming tokenizer.
*
* Extracts SEO-relevant data from a page's HTML: title, meta description,
* headings, images, links, canonical, OG tags, structured data, robots meta,
* word count, hreflang.
*
* Deliberately NOT a DOM parser: the previous cheerio implementation built a
* full DOM (~5-10x the HTML's size) per page, and with 25 concurrent parses
* on a 128MB isolate that was the audit engine's dominant OOM cause. The
* tokenizer keeps only the accumulated text and extracted fields in memory.
*/
import { Parser } from "htmlparser2";
import { normalizeUrl, isSameOrigin } from "./url-utils";
import type { PageAnalysis, PageLink } from "./types";
const SKIPPED_LINK_PROTOCOLS = /^(javascript:|mailto:|tel:|#)/;
/** Subtrees whose text is not visible content. */
const NON_CONTENT_TAGS = new Set(["script", "style", "noscript", "svg"]);
const HEADING_LEVELS: Record = {
h1: 1,
h2: 2,
h3: 3,
h4: 4,
h5: 5,
h6: 6,
};
const MAX_ANCHOR_CHARS = 200;
/**
* Per-page caps on the extracted collections. Crawler-trap and mega-menu
* pages can carry thousands of links/images per page, and crawled pages sit
* in memory in 25-page persist batches — uncapped collections were part of
* the audit engine's exceededMemory profile. Counts derived from these
* arrays saturate at the cap on such pathological pages.
*/
const MAX_EXTRACTED_LINKS = 1_000;
const MAX_EXTRACTED_IMAGES = 1_000;
interface OpenAnchor {
href: string;
rel: string;
text: string[];
}
/**
* Analyze an HTML string and extract all SEO-relevant data.
*/
export function analyzeHtml(
html: string,
pageUrl: string,
statusCode: number,
responseTimeMs: number,
redirectUrl: string | null = null,
): PageAnalysis {
let title: string | null = null;
let titleDepth = 0;
let titleDone = false;
// parse5 (the old DOM path) treats