pagegraph 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +403 -148
- package/dist/audit.d.ts +2 -2
- package/dist/audit.js +4 -4
- package/dist/build-only/audit.d.ts +1 -0
- package/dist/build-only/audit.js +6 -0
- package/dist/build-only/audit.js.map +1 -0
- package/dist/build-only/claims.d.ts +1 -0
- package/dist/build-only/claims.js +6 -0
- package/dist/build-only/claims.js.map +1 -0
- package/dist/build-only/config.d.ts +1 -0
- package/dist/build-only/config.js +6 -0
- package/dist/build-only/config.js.map +1 -0
- package/dist/build-only/oxlint.d.ts +1 -0
- package/dist/build-only/oxlint.js +6 -0
- package/dist/build-only/oxlint.js.map +1 -0
- package/dist/build-only/tanstack-start.d.ts +1 -0
- package/dist/build-only/tanstack-start.js +6 -0
- package/dist/build-only/tanstack-start.js.map +1 -0
- package/dist/build-only/vite.d.ts +1 -0
- package/dist/build-only/vite.js +6 -0
- package/dist/build-only/vite.js.map +1 -0
- package/dist/{checks-nk8ZRRIA.js → checks-BS-aDXB_.js} +4 -142
- package/dist/checks-BS-aDXB_.js.map +1 -0
- package/dist/claims-Bch7Lxlo.d.ts +92 -0
- package/dist/claims-Bv7voeEV.js +431 -0
- package/dist/claims-Bv7voeEV.js.map +1 -0
- package/dist/claims.d.ts +94 -0
- package/dist/claims.js +2 -0
- package/dist/cli.js +10368 -8485
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +4 -83
- package/dist/config.js +3 -122
- package/dist/config.js.map +1 -1
- package/dist/{content-CB_kHmJ3.js → content-CHd0X4s6.js} +3 -2
- package/dist/{content-CB_kHmJ3.js.map → content-CHd0X4s6.js.map} +1 -1
- package/dist/{content-DVL2iyGU.d.ts → content-DgNr7apS.d.ts} +5 -130
- package/dist/declare-D5o7hwmv.js +9 -0
- package/dist/declare-D5o7hwmv.js.map +1 -0
- package/dist/document-CztEu3Vd.d.ts +109 -0
- package/dist/document-Dvemcv7V.js +379 -0
- package/dist/document-Dvemcv7V.js.map +1 -0
- package/dist/facts-1OKclPOk.js +115 -0
- package/dist/facts-1OKclPOk.js.map +1 -0
- package/dist/graph-D1PIndsC.d.ts +12238 -0
- package/dist/graph-DMNOLXLu.js +172 -0
- package/dist/graph-DMNOLXLu.js.map +1 -0
- package/dist/index-C5w9ripz.d.ts +204 -0
- package/dist/index.d.ts +6 -196
- package/dist/index.js +55 -134
- package/dist/index.js.map +1 -1
- package/dist/inline-BbS93GAT.js +682 -0
- package/dist/inline-BbS93GAT.js.map +1 -0
- package/dist/llms-Dhr_5u2O.js +19 -0
- package/dist/llms-Dhr_5u2O.js.map +1 -0
- package/dist/markdown-C6dRE9zD.js +168 -0
- package/dist/markdown-C6dRE9zD.js.map +1 -0
- package/dist/markdown-path-lJB9BAVZ.js +6 -0
- package/dist/markdown-path-lJB9BAVZ.js.map +1 -0
- package/dist/message-CuBjgZ74.js +12 -0
- package/dist/message-CuBjgZ74.js.map +1 -0
- package/dist/oxlint.d.ts +58 -0
- package/dist/oxlint.js +202 -0
- package/dist/oxlint.js.map +1 -0
- package/dist/page-heads-BfjWQvau.d.ts +108 -0
- package/dist/page-heads-BgbaIjj9.js +25 -0
- package/dist/page-heads-BgbaIjj9.js.map +1 -0
- package/dist/projections-Bk-qOOsR.d.ts +78 -0
- package/dist/projections-D0oOFKiY.js +142 -0
- package/dist/projections-D0oOFKiY.js.map +1 -0
- package/dist/react.d.ts +27 -12051
- package/dist/react.js +14 -4
- package/dist/react.js.map +1 -1
- package/dist/route-config-35ZEzkNC.js +308 -0
- package/dist/route-config-35ZEzkNC.js.map +1 -0
- package/dist/route-config-wneg6ktn.d.ts +29 -0
- package/dist/tanstack-start/index.d.ts +56 -0
- package/dist/tanstack-start/index.js +717 -0
- package/dist/tanstack-start/index.js.map +1 -0
- package/dist/tanstack-start/markdown.d.ts +12 -0
- package/dist/tanstack-start/markdown.js +149 -0
- package/dist/tanstack-start/markdown.js.map +1 -0
- package/dist/tanstack-start/prerender-worker.d.ts +2 -0
- package/dist/tanstack-start/prerender-worker.js +7 -0
- package/dist/tanstack-start/prerender-worker.js.map +1 -0
- package/dist/tanstack-start/react.d.ts +7 -0
- package/dist/tanstack-start/react.js +12 -0
- package/dist/tanstack-start/react.js.map +1 -0
- package/dist/tanstack-start/server.d.ts +20 -0
- package/dist/tanstack-start/server.js +77 -0
- package/dist/tanstack-start/server.js.map +1 -0
- package/dist/vite-graph-loader-CsGiYNWE.js +105 -0
- package/dist/vite-graph-loader-CsGiYNWE.js.map +1 -0
- package/dist/vite-graph-loader-DZCwejuJ.d.ts +133 -0
- package/dist/vite.d.ts +2 -28
- package/dist/vite.js +1 -307
- package/dist/wire-B4RQMc4e.js +26 -0
- package/dist/wire-B4RQMc4e.js.map +1 -0
- package/package.json +70 -7
- package/dist/checks-nk8ZRRIA.js.map +0 -1
- package/dist/vite.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content-CB_kHmJ3.js","names":["decodeEntities","decodeEntities"],"sources":["../src/core/inspect-html.ts","../src/core/links.ts","../src/core/content.ts"],"sourcesContent":["/**\n * Read a rendered `<head>` and validate it: title, meta\n * (description/robots/og/twitter), canonical link, and `application/ld+json`\n * blocks, with a minimal per-type JSON-LD check.\n *\n * This is the pure half of `pagegraph inspect --live` — the half worth having on its\n * own. The CLI fetches a URL and hands the body here; a test suite can render a\n * page and hand *that* here, asserting the head it actually ships. Both get the\n * same verdict, because it is the same function.\n *\n * No HTML-parsing dependency, by design: a `<head>` is small and well-formed, so\n * string/regex scanning is enough, and the package's core stays zero-dependency\n * (which is also why the object guard below is hand-rolled — `effect/Predicate`\n * is not reachable from this entry). It is honest about its limits: it does not\n * build a DOM, so exotic markup (commented-out tags, CDATA, attributes spanning\n * constructs) is out of scope. This validates *rendered output*, not arbitrary\n * HTML.\n *\n * `issues` are blocking: a non-empty list makes `pagegraph inspect --live` exit 1.\n * Required tags are `<title>`, `meta[name=description]`, and\n * `link[rel=canonical]`; any JSON-LD that fails to parse or fails its minimal\n * schema is also blocking.\n */\n\nexport interface JsonLdReport {\n type: string;\n valid: boolean;\n errors: Array<string>;\n}\n\nexport interface LiveHeadReport {\n url: string;\n status: number;\n title?: string | undefined;\n description?: string | undefined;\n canonical?: string | undefined;\n robots?: string | undefined;\n og: Record<string, string>;\n twitter: Record<string, string>;\n jsonLd: Array<JsonLdReport>;\n issues: Array<string>;\n}\n\nconst ENTITIES: Record<string, string> = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n \""\": '\"',\n \"'\": \"'\",\n \"'\": \"'\",\n};\n\nconst decodeEntities = (value: string): string =>\n value.replace(/&(?:amp|lt|gt|quot|#39|apos);/g, (match) => ENTITIES[match] ?? match);\n\n/** Pull double/single-quoted attributes off a single tag string. */\nconst parseAttrs = (tag: string): Record<string, string> => {\n const attrs: Record<string, string> = {};\n const re = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)')/g;\n let match: RegExpExecArray | null;\n while ((match = re.exec(tag)) !== null) {\n attrs[match[1]!.toLowerCase()] = decodeEntities(match[2] ?? match[3] ?? \"\");\n }\n return attrs;\n};\n\n/** Normalize a JSON-LD `@type` (string or array) to a single readable label. */\nconst typeName = (value: unknown): string => {\n if (typeof value === \"string\") return value;\n if (Array.isArray(value))\n return value.filter((v) => typeof v === \"string\").join(\", \") || \"unknown\";\n return \"unknown\";\n};\n\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n // NOTE: This zero-dependency core entry cannot import Effect; JSON-LD validation happens immediately after this shallow narrowing.\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\n/** Flatten a parsed JSON-LD payload (single object, array, or `@graph`) to items. */\nconst collectItems = (parsed: unknown): Array<Record<string, unknown>> => {\n if (Array.isArray(parsed)) return parsed.filter(isObject);\n if (isObject(parsed)) {\n if (Array.isArray(parsed[\"@graph\"])) return parsed[\"@graph\"].filter(isObject);\n return [parsed];\n }\n return [];\n};\n\nconst countQuestions = (mainEntity: unknown): number => {\n if (!Array.isArray(mainEntity)) return 0;\n return mainEntity.filter((entry) => isObject(entry) && typeName(entry[\"@type\"]) === \"Question\")\n .length;\n};\n\nconst validateItemListElements = (value: unknown): Array<string> => {\n if (!Array.isArray(value) || value.length < 1) {\n return [\"ItemList needs at least one `itemListElement` entry.\"];\n }\n const errors: Array<string> = [];\n value.forEach((entry, index) => {\n if (!isObject(entry) || typeName(entry[\"@type\"]) !== \"ListItem\") {\n errors.push(`ItemList entry ${index + 1} is not a ListItem.`);\n return;\n }\n if (entry[\"position\"] !== index + 1) {\n errors.push(`ItemList entry ${index + 1} has an invalid position.`);\n }\n if (!entry[\"name\"] || !entry[\"url\"]) {\n errors.push(`ItemList entry ${index + 1} needs a name and URL.`);\n }\n });\n return errors;\n};\n\n/** Minimal per-type validation — enough to catch an empty or malformed block. */\nconst validateItem = (item: Record<string, unknown>): JsonLdReport => {\n const type = typeName(item[\"@type\"]);\n const errors: Array<string> = [];\n\n if (type === \"Article\" || type === \"NewsArticle\" || type === \"BlogPosting\") {\n if (!item[\"headline\"]) errors.push(\"Article is missing `headline`.\");\n if (!item[\"datePublished\"]) errors.push(\"Article is missing `datePublished`.\");\n } else if (type === \"FAQPage\") {\n if (countQuestions(item[\"mainEntity\"]) < 1) {\n errors.push(\"FAQPage needs at least one Question in `mainEntity`.\");\n }\n } else if (type === \"BreadcrumbList\") {\n const items = item[\"itemListElement\"];\n if (!Array.isArray(items) || items.length < 2) {\n errors.push(\"BreadcrumbList needs at least two `itemListElement` entries.\");\n }\n } else if (type === \"ItemList\") {\n errors.push(...validateItemListElements(item[\"itemListElement\"]));\n const items = item[\"itemListElement\"];\n if (Array.isArray(items) && item[\"numberOfItems\"] !== items.length) {\n errors.push(\"ItemList `numberOfItems` does not match its entries.\");\n }\n }\n\n return { type, valid: errors.length === 0, errors };\n};\n\nconst validateLdJson = (raw: string): Array<JsonLdReport> => {\n let parsed: unknown;\n // NOTE: JSON.parse boundary: converts the native parse throw into an unparseable JsonLdReport value in a pure sync validator\n try {\n parsed = JSON.parse(raw);\n } catch (cause) {\n return [\n {\n type: \"unparseable\",\n valid: false,\n errors: [`JSON parse error: ${cause instanceof Error ? cause.message : String(cause)}`],\n },\n ];\n }\n const items = collectItems(parsed);\n if (items.length === 0) {\n return [{ type: \"unknown\", valid: false, errors: [\"No JSON-LD object found in block.\"] }];\n }\n return items.map(validateItem);\n};\n\n/** Parse a rendered HTML document's `<head>` into a report. Pure. */\nexport const inspectHtml = (url: string, status: number, html: string): LiveHeadReport => {\n const headMatch = html.match(/<head[^>]*>([\\s\\S]*?)<\\/head>/i);\n const head = headMatch ? headMatch[1]! : html;\n\n const titleMatch = head.match(/<title[^>]*>([\\s\\S]*?)<\\/title>/i);\n const title = titleMatch ? decodeEntities(titleMatch[1]!.trim()) : undefined;\n\n const og: Record<string, string> = {};\n const twitter: Record<string, string> = {};\n let description: string | undefined;\n let robots: string | undefined;\n\n for (const tag of head.match(/<meta\\b[^>]*>/gi) ?? []) {\n const attrs = parseAttrs(tag);\n const content = attrs[\"content\"];\n if (content === undefined) continue;\n const property = attrs[\"property\"];\n const name = attrs[\"name\"];\n if (property?.startsWith(\"og:\")) og[property] = content;\n else if (name?.startsWith(\"twitter:\")) twitter[name] = content;\n else if (name === \"description\") description = content;\n else if (name === \"robots\") robots = content;\n }\n\n let canonical: string | undefined;\n for (const tag of head.match(/<link\\b[^>]*>/gi) ?? []) {\n const attrs = parseAttrs(tag);\n if (attrs[\"rel\"] === \"canonical\") canonical = attrs[\"href\"];\n }\n\n const jsonLd: Array<JsonLdReport> = [];\n const scriptRe = /<script\\b[^>]*type=[\"']application\\/ld\\+json[\"'][^>]*>([\\s\\S]*?)<\\/script>/gi;\n let scriptMatch: RegExpExecArray | null;\n while ((scriptMatch = scriptRe.exec(head)) !== null) {\n jsonLd.push(...validateLdJson(scriptMatch[1]!.trim()));\n }\n\n const issues: Array<string> = [];\n if (status >= 400) issues.push(`Fetch returned HTTP ${status}.`);\n if (!title) issues.push(\"Missing <title>.\");\n if (!description) issues.push(\"Missing meta description.\");\n if (!canonical) issues.push(\"Missing canonical link.\");\n for (const block of jsonLd) {\n if (!block.valid)\n issues.push(...block.errors.map((error) => `JSON-LD (${block.type}): ${error}`));\n }\n\n return { url, status, title, description, canonical, robots, og, twitter, jsonLd, issues };\n};\n\n/** A non-empty `issues` list fails `pagegraph inspect --live` (exit 1). */\nexport const hasBlockingIssues = (report: LiveHeadReport): boolean => report.issues.length > 0;\n","/**\n * Pure link-graph utilities: read anchors out of rendered HTML, model the\n * rendered graph, compute homepage depth, and diff it against the declared\n * graph. Effect- and framework-free so it runs in the CLI, a Worker, or a test\n * on the same data.\n *\n * This is the \"served HTML\" half of the SEO graph: the declared graph describes\n * intent, this describes what a crawler actually receives.\n */\n\n/** Where an anchor sits in the document, coarsely. */\nexport type AnchorRegion = \"nav\" | \"footer\" | \"header\" | \"body\";\n\nexport interface Anchor {\n readonly href: string;\n readonly text: string;\n readonly region: AnchorRegion;\n /** True when the resolved target shares the page's origin. */\n readonly internal: boolean;\n}\n\nexport interface LinkEdge {\n readonly from: string;\n readonly to: string;\n readonly region: AnchorRegion;\n}\n\nexport interface RenderedGraph {\n /** Every same-origin page path the crawl rendered, in first-seen order. */\n readonly nodes: ReadonlyArray<string>;\n readonly edges: ReadonlyArray<LinkEdge>;\n readonly internalEdges: ReadonlyArray<LinkEdge>;\n /** Same-origin body-region edges: the contextual surface. */\n readonly contextualEdges: ReadonlyArray<LinkEdge>;\n /** Same-origin pages with no incoming internal edge (\"reachable only via nav\" is not implied). */\n readonly orphans: ReadonlyArray<string>;\n readonly depthByPath: ReadonlyMap<string, number>;\n readonly maxDepth: number | null;\n}\n\nconst REGION_TAGS = new Set([\"nav\", \"footer\", \"header\"]);\n\nconst TOKEN =\n /<(\\/?)(nav|footer|header)\\b[^>]*>|<a\\b([^>]*)>([\\s\\S]*?)<\\/a>/gi;\n\n// A real `href` attribute is preceded by whitespace (or the tag's start). The\n// boundary keeps `data-href`, `xhref`, and similar attributes from reading as\n// links, and stops a decoy from winning over a genuine `href`.\nconst HREF = /(?:^|\\s)href\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s>]+))/i;\n\nconst BASE_TAG = /<base\\b[^>]*>/gi;\n\nconst NON_NAVIGABLE = /^(?:#|mailto:|tel:|javascript:|data:)/i;\n\nconst stripTags = (value: string): string => value.replace(/<[^>]*>/g, \" \");\n\nconst decodeEntities = (value: string): string =>\n value\n .replace(/ /gi, \" \")\n .replace(/&/gi, \"&\")\n .replace(/</gi, \"<\")\n .replace(/>/gi, \">\")\n .replace(/"/gi, \"\\\"\")\n .replace(/'|'/gi, \"'\");\n\nconst anchorText = (value: string): string =>\n decodeEntities(stripTags(value)).replace(/\\s+/g, \" \").trim();\n\n/** Normalize a same-origin URL to the graph's path key (no query/hash; \"/\" preserved). */\nexport const normalizePath = (url: URL): string => {\n const path = url.pathname.replace(/\\/+$/, \"\");\n return path === \"\" ? \"/\" : path;\n};\n\nconst hrefValue = (attributes: string): string | undefined => {\n const match = HREF.exec(attributes);\n if (match === null) return undefined;\n return match[1] ?? match[2] ?? match[3];\n};\n\n/**\n * The document base URL. Browsers use the first `<base>` element that has an\n * `href` attribute and ignore every later one, so a `<base>` carrying only\n * `target` is skipped, but the first `<base href>` wins even when its value is\n * empty (it then resolves to the document URL) or invalid (the base is ignored,\n * resolving to the document URL). With no `<base href>` at all, links resolve\n * against the response URL.\n */\nconst documentBase = (html: string, responseUrl: URL): URL => {\n BASE_TAG.lastIndex = 0;\n let tag: RegExpExecArray | null;\n while ((tag = BASE_TAG.exec(html)) !== null) {\n const href = hrefValue(tag[0]);\n if (href === undefined) continue;\n if (href.length === 0) return responseUrl;\n try {\n return new URL(href, responseUrl);\n } catch {\n return responseUrl;\n }\n }\n return responseUrl;\n};\n\n/**\n * Extract every anchor in document order, tagging each with the region tag it\n * sits inside. Anchor text keeps only its letters and a coarse entity decode;\n * this is a classifier input, not a renderer.\n */\nexport const extractAnchors = (html: string, baseUrl: string): ReadonlyArray<Anchor> => {\n const base = new URL(baseUrl);\n const resolutionBase = documentBase(html, base);\n const regions: Array<AnchorRegion> = [];\n const anchors: Array<Anchor> = [];\n TOKEN.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = TOKEN.exec(html)) !== null) {\n const closing = match[1];\n const regionTag = match[2];\n if (regionTag !== undefined) {\n const region = regionTag.toLowerCase() as AnchorRegion;\n if (closing === \"/\") {\n const index = regions.lastIndexOf(region);\n if (index !== -1) regions.splice(index, 1);\n } else {\n regions.push(region);\n }\n continue;\n }\n const attributes = match[3];\n const raw = hrefValue(attributes);\n if (raw === undefined || raw.length === 0 || NON_NAVIGABLE.test(raw)) continue;\n let resolved: URL;\n try {\n resolved = new URL(raw, resolutionBase);\n } catch {\n continue;\n }\n if (resolved.protocol !== \"http:\" && resolved.protocol !== \"https:\") continue;\n const region = regions.length === 0 ? \"body\" : regions[regions.length - 1]!;\n anchors.push({\n href: resolved.href,\n text: anchorText(match[4] ?? \"\"),\n region,\n internal: resolved.origin === base.origin,\n });\n }\n return anchors;\n};\n\nexport interface RenderedPage {\n readonly url: string;\n readonly anchors: ReadonlyArray<Anchor>;\n}\n\nconst breadthFirstDepth = (\n edges: ReadonlyArray<LinkEdge>,\n root: string,\n): ReadonlyMap<string, number> => {\n const adjacency = new Map<string, Set<string>>();\n for (const edge of edges) {\n const targets = adjacency.get(edge.from) ?? new Set<string>();\n targets.add(edge.to);\n adjacency.set(edge.from, targets);\n }\n const depth = new Map<string, number>([[root, 0]]);\n const queue: Array<string> = [root];\n while (queue.length > 0) {\n const current = queue.shift()!;\n const currentDepth = depth.get(current)!;\n for (const next of adjacency.get(current) ?? []) {\n if (depth.has(next)) continue;\n depth.set(next, currentDepth + 1);\n queue.push(next);\n }\n }\n return depth;\n};\n\n/**\n * Assemble a {@link RenderedGraph} from page/edge sets. Edges are already\n * same-origin and path-keyed by the callers, so this is the shared, pure tail of\n * {@link buildRenderedGraph} and {@link renderedGraphFromEdges}.\n */\nconst graphFromEdges = (\n nodes: Set<string>,\n edges: Array<LinkEdge>,\n root: string,\n): RenderedGraph => {\n const rootKey = root.replace(/\\/+$/, \"\") || \"/\";\n const internalEdges = edges;\n const contextualEdges = edges.filter((edge) => edge.region === \"body\");\n const incoming = new Set(internalEdges.map((edge) => edge.to));\n const orphans = [...nodes].filter((path) => path !== rootKey && !incoming.has(path)).sort();\n const depthByPath = breadthFirstDepth(internalEdges, rootKey);\n let maxDepth: number | null = null;\n for (const value of depthByPath.values()) {\n if (maxDepth === null || value > maxDepth) maxDepth = value;\n }\n return {\n nodes: [...nodes],\n edges,\n internalEdges,\n contextualEdges,\n orphans,\n depthByPath,\n maxDepth,\n };\n};\n\n/**\n * Build the rendered graph from crawled pages. `origin` fixes the graph's\n * same-origin boundary — pages and anchors on another origin are dropped, not\n * silently folded in — and `root` is the normalized path the depth BFS starts\n * from (the seed's final path when the homepage redirected). Every page path is\n * keyed the way the declared graph keys its routes.\n */\nexport const buildRenderedGraph = (\n origin: string,\n pages: ReadonlyArray<RenderedPage>,\n root: string = \"/\",\n): RenderedGraph => {\n const originUrl = new URL(origin);\n const graphOrigin = originUrl.origin;\n const edges: Array<LinkEdge> = [];\n const nodes = new Set<string>();\n for (const page of pages) {\n let pageUrl: URL;\n try {\n pageUrl = new URL(page.url);\n } catch {\n continue;\n }\n if (pageUrl.origin !== graphOrigin) continue;\n const path = normalizePath(pageUrl);\n nodes.add(path);\n for (const anchor of page.anchors) {\n let targetUrl: URL;\n try {\n targetUrl = new URL(anchor.href);\n } catch {\n continue;\n }\n if (targetUrl.origin !== graphOrigin) continue;\n const target = normalizePath(targetUrl);\n if (target === path) continue;\n edges.push({ from: path, to: target, region: anchor.region });\n }\n }\n return graphFromEdges(nodes, edges, root);\n};\n\n/** A URL or an already-normalized graph path key both collapse to the path key. */\nconst pathKeyOf = (value: string): string => {\n try {\n return normalizePath(new URL(value));\n } catch {\n const pathname = value.split(/[?#]/, 1)[0] ?? \"\";\n return pathname.replace(/\\/+$/, \"\") || \"/\";\n }\n};\n\n/**\n * Rebuild a {@link RenderedGraph} from a rendered-edge set alone — the artifact\n * `pagegraph links verify --emit-rendered` writes. Path keys are normalized the\n * same way as a crawl, and `nodes` carries the rendered page paths the artifact\n * recorded, so an edge-less page (a redirect target with no internal links) stays\n * a node and its orphan/depth report survives replay. Without `nodes` the node set\n * falls back to the edge endpoints.\n */\nexport const renderedGraphFromEdges = (\n edges: ReadonlyArray<LinkEdge>,\n root: string = \"/\",\n nodes: ReadonlyArray<string> = [],\n): RenderedGraph => {\n const nodeSet = new Set(nodes.map(pathKeyOf));\n const kept: Array<LinkEdge> = [];\n for (const edge of edges) {\n const from = pathKeyOf(edge.from);\n const to = pathKeyOf(edge.to);\n nodeSet.add(from);\n nodeSet.add(to);\n kept.push({ from, to, region: edge.region });\n }\n return graphFromEdges(nodeSet, kept, root);\n};\n\nexport interface SimpleEdge {\n readonly from: string;\n readonly to: string;\n}\n\nexport interface LinkGraphDiff {\n /** Declared internal edges with no matching rendered anchor. */\n readonly declaredNotRendered: ReadonlyArray<SimpleEdge>;\n /** Rendered contextual edges that no declared edge accounts for. */\n readonly renderedNotDeclared: ReadonlyArray<SimpleEdge>;\n readonly declaredCount: number;\n readonly renderedContextualCount: number;\n}\n\nconst edgeKey = (edge: SimpleEdge): string => `${edge.from}\\u0000${edge.to}`;\n\nconst normalizeEdge = (edge: SimpleEdge): SimpleEdge => ({\n from: edge.from === \"\" ? \"/\" : edge.from.replace(/\\/+$/, \"\") || \"/\",\n to: edge.to === \"\" ? \"/\" : edge.to.replace(/\\/+$/, \"\") || \"/\",\n});\n\n/**\n * Diff rendered contextual edges against declared `related`/`crumb` edges. Both\n * sides are path-keyed; the diff is directional and reports each direction\n * separately because they call for different fixes.\n */\nexport const diffLinkGraph = (\n declared: ReadonlyArray<SimpleEdge>,\n rendered: RenderedGraph,\n): LinkGraphDiff => {\n const declaredNormalized = declared.map(normalizeEdge);\n const declaredKeys = new Set(declaredNormalized.map(edgeKey));\n const renderedKeys = new Set(rendered.contextualEdges.map(edgeKey));\n const declaredNotRendered = declaredNormalized.filter(\n (edge) => !renderedKeys.has(edgeKey(edge)),\n );\n const renderedNotDeclared = rendered.internalEdges\n .filter((edge) => edge.region === \"body\")\n .map((edge) => normalizeEdge(edge))\n .filter((edge) => !declaredKeys.has(edgeKey(edge)));\n return {\n declaredNotRendered,\n renderedNotDeclared,\n declaredCount: declaredNormalized.length,\n renderedContextualCount: rendered.contextualEdges.length,\n };\n};\n\n/** A page the crawl could not render; its outgoing anchors are absent. */\nexport interface RenderedEdgeFailure {\n readonly url: string;\n readonly error: string;\n}\n\n/**\n * Provenance for a rendered-edge artifact: enough to trust the edges and to\n * refuse a gate that would assert on an incomplete crawl. `root` is the seed's\n * normalized final path (its post-redirect path), so depth and orphans rebuild\n * exactly as the crawl computed them.\n */\nexport interface RenderedEdgeArtifactCrawl {\n /** Pages whose HTML was rendered into `edges`. */\n readonly pages: number;\n /** Normalized final path of the seed — the depth BFS root. */\n readonly root: string;\n /** The crawl's page budget (`--limit`). */\n readonly limit: number;\n /** True when discovery outran `limit`; a gate must not assert on this. */\n readonly truncated: boolean;\n /** True when any captured body was cut at `--max-body-bytes`. */\n readonly bodyTruncated: boolean;\n /** URLs whose captured body was cut; anchors past the cutoff are missing. */\n readonly truncatedPages: ReadonlyArray<string>;\n /** Pages that failed to fetch; their outgoing anchors are missing. */\n readonly failures: ReadonlyArray<RenderedEdgeFailure>;\n /** Non-HTML responses from link-only discovery; these do not hide HTML pages. */\n readonly nonHtml?: ReadonlyArray<{ readonly url: string; readonly finalUrl: string; readonly contentType: string }>;\n readonly discoveryFailures?: ReadonlyArray<RenderedEdgeFailure>;\n readonly skipped?: ReadonlyArray<string>;\n readonly sitemapTruncated?: boolean;\n readonly sitemapUsed?: boolean;\n}\n\n/**\n * The stable, `decodeRenderedEdges`-compatible artifact `pagegraph links verify\n * --emit-rendered` writes and `--rendered` reads. It carries the raw served edges\n * (with `region`), the rendered page paths, and the provenance a coverage gate\n * needs to decide whether the crawl is trustworthy, so one crawl can be asserted\n * repeatedly without re-fetching.\n */\nexport interface RenderedEdgeArtifact {\n readonly kind: \"links-rendered\";\n readonly schemaVersion: 1;\n readonly origin: string;\n readonly seed: string;\n readonly crawl: RenderedEdgeArtifactCrawl;\n /** Normalized paths of every rendered page — replay's node set. */\n readonly nodes: ReadonlyArray<string>;\n readonly edges: ReadonlyArray<LinkEdge>;\n}\n\n/** The rendered-edge artifact schema this package writes and accepts. */\nexport const RENDERED_EDGE_ARTIFACT_SCHEMA_VERSION = 1 as const;\n\nconst ARTIFACT_KIND = \"links-rendered\";\n\nconst REGIONS: ReadonlySet<string> = new Set([\"nav\", \"footer\", \"header\", \"body\"]);\n\nconst decodeArtifactEdges = (input: unknown): ReadonlyArray<LinkEdge> => {\n if (!Array.isArray(input)) {\n throw new Error(\"`edges` must be an array of { from, to, region } edges\");\n }\n return input.map((item) => {\n if (item === null || typeof item !== \"object\") {\n throw new Error(\"each artifact edge must be an object with string `from` and `to`\");\n }\n const { from, to, region } = item as { from?: unknown; to?: unknown; region?: unknown };\n if (typeof from !== \"string\" || typeof to !== \"string\") {\n throw new Error(\"each artifact edge must have string `from` and `to`\");\n }\n if (region !== undefined && !REGIONS.has(region as string)) {\n throw new Error(\n `artifact edge region must be one of nav, footer, header, body (received ${JSON.stringify(region)})`,\n );\n }\n return { from, to, region: (region as AnchorRegion | undefined) ?? \"body\" };\n });\n};\n\nconst decodeArtifactNodes = (input: unknown): ReadonlyArray<string> => {\n if (input === undefined) return [];\n if (!Array.isArray(input) || !input.every((path) => typeof path === \"string\")) {\n throw new Error(\"rendered-edge artifact `nodes` must be an array of paths\");\n }\n return input;\n};\n\nconst decodeArtifactFailures = (input: unknown): ReadonlyArray<RenderedEdgeFailure> => {\n if (input === undefined) return [];\n if (!Array.isArray(input)) {\n throw new Error(\"rendered-edge artifact `crawl.failures` must be an array\");\n }\n return input.map((item) => {\n if (item === null || typeof item !== \"object\") {\n throw new Error(\"each artifact failure must be an object with string `url` and `error`\");\n }\n const { url, error } = item as { url?: unknown; error?: unknown };\n if (typeof url !== \"string\" || typeof error !== \"string\") {\n throw new Error(\"each artifact failure must have string `url` and `error`\");\n }\n return { url, error };\n });\n};\n\nconst requiredAbsoluteUrl = (value: unknown, field: string): string => {\n if (typeof value !== \"string\" || value === \"\") {\n throw new Error(`rendered-edge artifact must carry a non-empty string \\`${field}\\``);\n }\n try {\n void new URL(value);\n } catch {\n throw new Error(\n `rendered-edge artifact \\`${field}\\` must be an absolute URL (received ${JSON.stringify(value)})`,\n );\n }\n return value;\n};\n\nconst requiredInteger = (value: unknown, field: string, minimum: number): number => {\n if (!Number.isSafeInteger(value) || (value as number) < minimum) {\n throw new Error(\n `rendered-edge artifact \\`${field}\\` must be an integer >= ${minimum} (received ${JSON.stringify(value)})`,\n );\n }\n return value as number;\n};\n\n/**\n * Decode a rendered-edge artifact from user input. Throws on any other shape —\n * this is user input, not a provider payload. `root`, `bodyTruncated`,\n * `truncatedPages`, `failures`, and `nodes` are optional so a hand-written edge\n * dump that only carries provenance can still be replayed.\n */\nexport const decodeRenderedEdgeArtifact = (input: unknown): RenderedEdgeArtifact => {\n if (input === null || typeof input !== \"object\") {\n throw new Error(\"rendered-edge artifact must be an object\");\n }\n const value = input as Record<string, unknown>;\n if (value[\"kind\"] !== ARTIFACT_KIND) {\n throw new Error(\n `rendered-edge artifact \\`kind\\` must be \"${ARTIFACT_KIND}\" (received ${JSON.stringify(value[\"kind\"])})`,\n );\n }\n if (value[\"schemaVersion\"] !== RENDERED_EDGE_ARTIFACT_SCHEMA_VERSION) {\n throw new Error(\n `unsupported rendered-edge artifact schemaVersion: ${JSON.stringify(value[\"schemaVersion\"])}`,\n );\n }\n const origin = requiredAbsoluteUrl(value[\"origin\"], \"origin\");\n const seed = requiredAbsoluteUrl(value[\"seed\"], \"seed\");\n const crawl = value[\"crawl\"];\n if (crawl === null || typeof crawl !== \"object\") {\n throw new Error(\"rendered-edge artifact must carry a `crawl` object\");\n }\n const provenance = crawl as Record<string, unknown>;\n const truncatedPages = provenance[\"truncatedPages\"];\n if (\n truncatedPages !== undefined &&\n (!Array.isArray(truncatedPages) || !truncatedPages.every((url) => typeof url === \"string\"))\n ) {\n throw new Error(\"rendered-edge artifact `truncatedPages` must be an array of URLs\");\n }\n const bodyTruncated = provenance[\"bodyTruncated\"];\n if (bodyTruncated !== undefined && typeof bodyTruncated !== \"boolean\") {\n throw new Error(\"rendered-edge artifact `bodyTruncated` must be a boolean\");\n }\n if (typeof provenance[\"truncated\"] !== \"boolean\") {\n throw new Error(\"rendered-edge artifact `crawl.truncated` must be a boolean\");\n }\n const skipped = provenance[\"skipped\"];\n if (skipped !== undefined && (!Array.isArray(skipped) || !skipped.every((url) => typeof url === \"string\"))) {\n throw new Error(\"rendered-edge artifact `crawl.skipped` must be an array of URLs\");\n }\n const nonHtml = provenance[\"nonHtml\"];\n if (nonHtml !== undefined && (!Array.isArray(nonHtml) || !nonHtml.every((item) =>\n item !== null && typeof item === \"object\" &&\n typeof item.url === \"string\" && typeof item.finalUrl === \"string\" && typeof item.contentType === \"string\"\n ))) {\n throw new Error(\"rendered-edge artifact `crawl.nonHtml` must be an array of { url, finalUrl, contentType }\");\n }\n if (Array.isArray(nonHtml)) {\n for (const item of nonHtml) {\n requiredAbsoluteUrl(item.url, \"crawl.nonHtml.url\");\n requiredAbsoluteUrl(item.finalUrl, \"crawl.nonHtml.finalUrl\");\n }\n }\n for (const field of [\"sitemapTruncated\", \"sitemapUsed\"] as const) {\n if (provenance[field] !== undefined && typeof provenance[field] !== \"boolean\") {\n throw new Error(`rendered-edge artifact \\`crawl.${field}\\` must be a boolean`);\n }\n }\n return {\n kind: ARTIFACT_KIND,\n schemaVersion: RENDERED_EDGE_ARTIFACT_SCHEMA_VERSION,\n origin,\n seed,\n crawl: {\n pages: requiredInteger(provenance[\"pages\"], \"crawl.pages\", 0),\n root: typeof provenance[\"root\"] === \"string\" ? provenance[\"root\"] : pathKeyOf(seed),\n limit: requiredInteger(provenance[\"limit\"], \"crawl.limit\", 1),\n truncated: provenance[\"truncated\"],\n bodyTruncated: bodyTruncated === true,\n truncatedPages: (truncatedPages as ReadonlyArray<string> | undefined) ?? [],\n failures: decodeArtifactFailures(provenance[\"failures\"]),\n ...(nonHtml === undefined ? {} : { nonHtml: nonHtml as ReadonlyArray<{ url: string; finalUrl: string; contentType: string }> }),\n ...(provenance[\"discoveryFailures\"] === undefined ? {} : { discoveryFailures: decodeArtifactFailures(provenance[\"discoveryFailures\"]) }),\n ...(skipped === undefined ? {} : { skipped: skipped as ReadonlyArray<string> }),\n ...(provenance[\"sitemapTruncated\"] === undefined ? {} : { sitemapTruncated: provenance[\"sitemapTruncated\"] as boolean }),\n ...(provenance[\"sitemapUsed\"] === undefined ? {} : { sitemapUsed: provenance[\"sitemapUsed\"] as boolean }),\n },\n nodes: decodeArtifactNodes(value[\"nodes\"]),\n edges: decodeArtifactEdges(value[\"edges\"]),\n };\n};\n","/**\n * Content checks over server-rendered HTML: headings, structured data against\n * visible text, word count, and declared-vs-rendered robots and canonical.\n *\n * These run on rendered documents, not on the route graph, because the facts\n * they judge do not exist in a declaration: headings come from components, JSON-LD\n * and the `<title>` are composed by `head()` at render time, and the visible text\n * is whatever the page's components produce. The graph supplies the other half —\n * which pages are indexable and what their canonical path and robots policy are —\n * so the declared-vs-rendered rules can say \"this page drifted from its\n * declaration\".\n *\n * Like `inspectHtml`, this is pure string scanning of rendered output with no\n * HTML-parsing dependency, and it keeps the core entry zero-dependency. It reads\n * server-rendered markup, which is what a crawler receives before any script runs;\n * text a client inserts after hydration is invisible here, and is reported as such.\n */\n\nimport type { Severity, Violation } from \"./checks\";\nimport { globToRegExp } from \"./checks\";\nimport type { SeoGraph, SeoNode } from \"./graph\";\nimport { inspectHtml } from \"./inspect-html\";\nimport { normalizePath } from \"./links\";\nimport { isSitemapEligible } from \"./projections\";\n\nexport interface Heading {\n readonly level: number;\n readonly text: string;\n}\n\n/** A JSON-LD object that carries an `@type`, found at any depth of any block. */\nexport type JsonLdNode = Readonly<Record<string, unknown>>;\n\n/** What a rendered document says, reduced to the fields the content rules read. */\nexport interface PageContent {\n readonly title: string | undefined;\n readonly description: string | undefined;\n readonly robots: string | undefined;\n readonly canonical: string | undefined;\n /** Every H1 in the body, in document order, by accessible text. */\n readonly h1: ReadonlyArray<string>;\n /** Headings in the main content region (`<main>`, else the body without nav and footer). */\n readonly outline: ReadonlyArray<Heading>;\n /** Words of visible text in the main content region. */\n readonly wordCount: number;\n /** Visible body text with whitespace collapsed; scripts, styles, and templates removed. */\n readonly text: string;\n /** Every typed JSON-LD node on the page, flattened out of arrays, `@graph`, and nesting. */\n readonly jsonLd: ReadonlyArray<JsonLdNode>;\n}\n\nexport type ContentRuleName =\n | \"missing-h1\"\n | \"multiple-h1\"\n | \"empty-h1\"\n | \"heading-level-skip\"\n | \"h1-title-mismatch\"\n | \"faq-not-visible\"\n | \"offer-price-not-visible\"\n | \"webpage-head-mismatch\"\n | \"thin-content\";\n\nexport interface ContentFinding {\n readonly rule: ContentRuleName;\n readonly severity: Severity;\n readonly message: string;\n readonly fix: string;\n}\n\nexport interface ContentCheckOptions {\n /** Minimum words of main-region text; omit to skip the `thin-content` rule. */\n readonly minWords?: number | undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Extraction\n// ---------------------------------------------------------------------------\n\nconst NAMED_ENTITIES: Record<string, string> = {\n amp: \"&\",\n lt: \"<\",\n gt: \">\",\n quot: '\"',\n apos: \"'\",\n nbsp: \" \",\n ndash: \"–\",\n mdash: \"—\",\n lsquo: \"‘\",\n rsquo: \"’\",\n ldquo: \"“\",\n rdquo: \"”\",\n hellip: \"…\",\n middot: \"·\",\n bull: \"•\",\n copy: \"©\",\n reg: \"®\",\n trade: \"™\",\n times: \"×\",\n euro: \"€\",\n pound: \"£\",\n};\n\nconst decodeEntities = (value: string): string =>\n value.replace(/&(#x[0-9a-f]+|#\\d+|[a-z]+);/gi, (entity, code: string) => {\n if (code.startsWith(\"#\")) {\n const point =\n code[1] === \"x\" || code[1] === \"X\"\n ? Number.parseInt(code.slice(2), 16)\n : Number.parseInt(code.slice(1), 10);\n return Number.isInteger(point) && point > 0 && point <= 0x10ffff\n ? String.fromCodePoint(point)\n : entity;\n }\n return NAMED_ENTITIES[code.toLowerCase()] ?? entity;\n });\n\n/** Elements whose contents a reader never sees as page text. */\nconst INVISIBLE = /<(script|style|template|noscript|svg)\\b[\\s\\S]*?<\\/\\1\\s*>/gi;\nconst COMMENT = /<!--[\\s\\S]*?-->/g;\n\nconst withoutInvisible = (html: string): string =>\n html.replace(COMMENT, \" \").replace(INVISIBLE, \" \");\n\nconst textOf = (html: string): string =>\n decodeEntities(html.replace(/<[^>]*>/g, \" \"))\n .replace(/\\s+/g, \" \")\n .trim();\n\nconst attributeOf = (tag: string, name: string): string | undefined => {\n const match = new RegExp(`(?:^|\\\\s)${name}\\\\s*=\\\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\\\s>]+))`, \"i\").exec(\n tag,\n );\n const value = match?.[1] ?? match?.[2] ?? match?.[3];\n return value === undefined ? undefined : decodeEntities(value);\n};\n\nconst bodyOf = (html: string): string => {\n const body = /<body\\b[^>]*>([\\s\\S]*)<\\/body>/i.exec(html);\n if (body) return body[1]!;\n return html.replace(/<head\\b[^>]*>[\\s\\S]*?<\\/head>/i, \" \");\n};\n\n/**\n * The main content region: `<main>` when the page has one, otherwise the body\n * without its `<nav>` and `<footer>` blocks. Site chrome repeats on every page,\n * so counting its headings or words would judge the layout, not the page.\n */\nconst mainRegionOf = (body: string): string => {\n const main = /<main\\b[^>]*>([\\s\\S]*?)<\\/main\\s*>/i.exec(body);\n if (main) return main[1]!;\n return body.replace(/<(nav|footer)\\b[\\s\\S]*?<\\/\\1\\s*>/gi, \" \");\n};\n\nconst HEADING = /<h([1-6])\\b([^>]*)>([\\s\\S]*?)<\\/h\\1\\s*>/gi;\n\n/**\n * A heading's accessible text: its content, else its `aria-label`, else the\n * `alt` of images inside it (a logo H1).\n */\nconst headingText = (attributes: string, inner: string): string => {\n const text = textOf(inner);\n if (text !== \"\") return text;\n const label = attributeOf(attributes, \"aria-label\")?.trim();\n if (label) return label;\n return [...inner.matchAll(/<img\\b[^>]*>/gi)]\n .map((image) => attributeOf(image[0], \"alt\")?.trim() ?? \"\")\n .filter(Boolean)\n .join(\" \");\n};\n\nconst headingsOf = (html: string): Array<Heading> =>\n [...html.matchAll(HEADING)].map((match) => ({\n level: Number(match[1]),\n text: headingText(match[2]!, match[3]!),\n }));\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n // NOTE: This zero-dependency core entry cannot import Effect; JSON-LD is untyped input narrowed field by field below.\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\n/** Every object with an `@type`, at any depth — offers nest in products, questions in FAQ pages. */\nconst collectTyped = (value: unknown, into: Array<JsonLdNode>): void => {\n if (Array.isArray(value)) {\n for (const entry of value) collectTyped(entry, into);\n return;\n }\n if (!isRecord(value)) return;\n if (value[\"@type\"] !== undefined) into.push(value);\n for (const child of Object.values(value)) collectTyped(child, into);\n};\n\nconst JSON_LD = /<script\\b[^>]*type\\s*=\\s*[\"']application\\/ld\\+json[\"'][^>]*>([\\s\\S]*?)<\\/script\\s*>/gi;\n\n/** Parse every JSON-LD block. A block that does not parse is `inspectHtml`'s finding, not ours. */\nconst jsonLdOf = (html: string): Array<JsonLdNode> => {\n const nodes: Array<JsonLdNode> = [];\n for (const match of html.matchAll(JSON_LD)) {\n let parsed: unknown;\n // NOTE: JSON.parse boundary: an unparseable block is skipped here; inspectHtml reports it.\n try {\n parsed = JSON.parse(match[1]!.trim());\n } catch {\n continue;\n }\n collectTyped(parsed, nodes);\n }\n return nodes;\n};\n\n/**\n * Text normalized for \"does this appear on the page\" comparisons: Unicode-folded,\n * lowercased, and reduced to runs of letters and digits. Punctuation, quotes,\n * dashes, and markup spacing differ freely between JSON-LD and rendered copy\n * without changing what a reader sees.\n */\nexport const matchText = (value: string): string =>\n value\n .normalize(\"NFKC\")\n .toLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter(Boolean)\n .join(\" \");\n\nconst countWords = (text: string): number => {\n const normalized = matchText(text);\n return normalized === \"\" ? 0 : normalized.split(\" \").length;\n};\n\n/** Reduce a rendered HTML document to the fields the content rules read. Pure. */\nexport const extractPageContent = (html: string): PageContent => {\n const head = inspectHtml(\"\", 200, html);\n const body = withoutInvisible(bodyOf(html));\n const main = mainRegionOf(body);\n return {\n title: head.title,\n description: head.description,\n robots: head.robots,\n canonical: head.canonical,\n h1: headingsOf(body)\n .filter((heading) => heading.level === 1)\n .map((heading) => heading.text),\n outline: headingsOf(main),\n wordCount: countWords(textOf(main)),\n text: textOf(body),\n jsonLd: jsonLdOf(html),\n };\n};\n\n// ---------------------------------------------------------------------------\n// Rules\n// ---------------------------------------------------------------------------\n\nconst typesOf = (node: JsonLdNode): Array<string> => {\n const type = node[\"@type\"];\n if (typeof type === \"string\") return [type];\n if (Array.isArray(type)) return type.filter((entry): entry is string => typeof entry === \"string\");\n return [];\n};\n\nconst hasType = (node: JsonLdNode, types: ReadonlySet<string>): boolean =>\n typesOf(node).some((type) => types.has(type));\n\nconst stringField = (node: JsonLdNode, field: string): string | undefined => {\n const value = node[field];\n return typeof value === \"string\" && value.trim() !== \"\" ? value : undefined;\n};\n\n/** Standard HTML element names; other angle-bracket text is not markup. */\nconst HTML_ELEMENT = new RegExp(\n `</?(?:${\"a abbr address article aside audio b bdi bdo blockquote br button caption cite code col colgroup dd del details dfn div dl dt em fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i img ins kbd label legend li main mark nav ol p picture pre q s samp section small span strong sub summary sup table tbody td tfoot th thead time tr u ul var video wbr\".split(\" \").join(\"|\")})\\\\b[^>]*>`,\n \"gi\",\n);\n\nconst inPage = (visible: string, text: string): boolean => {\n const needle = matchText(text);\n return needle === \"\" || ` ${visible} `.includes(` ${needle} `);\n};\n\n/**\n * JSON-LD text may carry the HTML Google allows in answers, or plain text with\n * angle-bracket placeholders such as `samva-<id>._domainkey`, which the page\n * renders escaped. Only standard element tags are markup, and a candidate with\n * no closing tag is also compared literally because `<i>` is ambiguous.\n */\nconst isVisible = (visible: string, candidate: string): boolean =>\n inPage(visible, decodeEntities(candidate.replace(HTML_ELEMENT, \" \"))) ||\n (!/<\\//.test(candidate) && inPage(visible, decodeEntities(candidate)));\n\nconst clip = (value: string, max = 80): string =>\n value.length > max ? `${value.slice(0, max - 1)}…` : value;\n\nconst STOPWORDS = new Set(\n \"a an and are as at be but by can for from get has have how in into is it its more not of on or our per than that the their this to use via vs was what when who why will with you your\".split(\n \" \",\n ),\n);\n\n/**\n * Terms that carry meaning, reduced to a crude stem: stopwords drop, a plural\n * `s` folds, and long words compare by their first six letters, so\n * \"Authentication\" and \"Authenticate\" or \"templates\" and \"template\" agree. The\n * rule only asks whether any term is shared, so a lenient stem errs toward\n * silence, never toward a false warning.\n */\nconst significantTerms = (value: string): Set<string> =>\n new Set(\n matchText(value)\n .split(\" \")\n .filter((term) => term !== \"\" && !STOPWORDS.has(term) && (term.length > 2 || /\\d/.test(term)))\n .map((term) => (term.length > 3 && term.endsWith(\"s\") ? term.slice(0, -1) : term))\n .map((term) => term.slice(0, 6)),\n );\n\n/** schema.org `WebPage` and the subtypes whose `name`/`description` describe the page itself. */\nconst WEB_PAGE_TYPES: ReadonlySet<string> = new Set([\n \"WebPage\",\n \"AboutPage\",\n \"CheckoutPage\",\n \"CollectionPage\",\n \"ContactPage\",\n \"FAQPage\",\n \"ItemPage\",\n \"MedicalWebPage\",\n \"ProfilePage\",\n \"QAPage\",\n \"RealEstateListing\",\n \"SearchResultsPage\",\n]);\n\nconst OFFER_TYPES: ReadonlySet<string> = new Set([\"Offer\", \"AggregateOffer\"]);\nconst FAQ_TYPES: ReadonlySet<string> = new Set([\"FAQPage\"]);\n\nconst asArray = (value: unknown): Array<unknown> =>\n value === undefined ? [] : Array.isArray(value) ? value : [value];\n\n/** Parse a schema.org price (`19`, `\"19.00\"`, `\"1,000\"`); undefined when it is not a number. */\nconst parsePrice = (value: unknown): number | undefined => {\n if (typeof value === \"number\") return Number.isFinite(value) ? value : undefined;\n if (typeof value !== \"string\") return undefined;\n const cleaned = value.replace(/[^\\d.]/g, \"\");\n if (cleaned === \"\" || !/^\\d*\\.?\\d+$/.test(cleaned)) return undefined;\n return Number(cleaned);\n};\n\nconst pricesOf = (offer: JsonLdNode): Array<number> =>\n [\n offer[\"price\"],\n offer[\"lowPrice\"],\n offer[\"highPrice\"],\n ...asArray(offer[\"priceSpecification\"]).flatMap((spec) =>\n isRecord(spec) ? [spec[\"price\"], spec[\"minPrice\"], spec[\"maxPrice\"]] : [],\n ),\n ].flatMap((value) => {\n const price = parsePrice(value);\n return price === undefined ? [] : [price];\n });\n\n/**\n * Whether a price appears as a number in the visible text: `19` matches `$19`,\n * `19.00`, and `19/mo`; `1000` matches `1,000`; `0.5` matches `0.50`. A price of\n * zero also matches the word \"free\".\n */\nexport const priceIsVisible = (price: number, text: string): boolean => {\n if (price === 0 && /\\bfree\\b/i.test(text)) return true;\n const [whole, fraction = \"\"] = String(price).split(\".\");\n const grouped = whole!.replace(/\\B(?=(\\d{3})+(?!\\d))/g, \"[,.\\\\u00a0 ]?\");\n const decimals = fraction === \"\" ? \"(?:[.,]0+)?\" : `[.,]${fraction}0*`;\n return new RegExp(`(?<![\\\\d.,])${grouped}${decimals}(?![\\\\d]|[.,]\\\\d)`).test(text);\n};\n\n/**\n * Whether a JSON-LD `name` agrees with the rendered `<title>`. A title usually\n * wraps the page name in a site-name affix (\"Pricing | Example\"), so either\n * containing the other counts as agreement.\n */\nconst namesAgree = (a: string, b: string): boolean => {\n const left = matchText(a);\n const right = matchText(b);\n if (left === \"\" || right === \"\") return true;\n return ` ${left} `.includes(` ${right} `) || ` ${right} `.includes(` ${left} `);\n};\n\ninterface ContentRule {\n readonly name: ContentRuleName;\n readonly severity: Severity;\n readonly evaluate: (\n content: PageContent,\n options: ContentCheckOptions,\n ) => ReadonlyArray<{ readonly message: string; readonly fix: string }>;\n}\n\n/**\n * Every content rule, in one place. Heading-count and structured-data rules that\n * Google treats as eligibility or policy problems are structural; outline and\n * wording smells are editorial.\n */\nconst CONTENT_RULES: ReadonlyArray<ContentRule> = [\n {\n name: \"missing-h1\",\n severity: \"structural\",\n evaluate: (content) =>\n content.h1.length === 0\n ? [\n {\n message: \"Page has no H1.\",\n fix: \"Render exactly one <h1> that names the page's topic.\",\n },\n ]\n : [],\n },\n {\n name: \"multiple-h1\",\n severity: \"structural\",\n evaluate: (content) =>\n content.h1.length > 1\n ? [\n {\n message: `Page has ${content.h1.length} H1s: ${content.h1.map((text) => `\"${clip(text, 40)}\"`).join(\", \")}.`,\n fix: \"Keep one <h1> for the page topic; demote the others to <h2>.\",\n },\n ]\n : [],\n },\n {\n name: \"empty-h1\",\n severity: \"structural\",\n evaluate: (content) =>\n content.h1.some((text) => text === \"\")\n ? [\n {\n message: \"An H1 has no text, aria-label, or image alt.\",\n fix: \"Give the <h1> visible text that names the page's topic.\",\n },\n ]\n : [],\n },\n {\n // The outline starts under the page's H1, so a first heading of h3 is a\n // skip just as h2 → h4 is. Going back up any number of levels is fine.\n name: \"heading-level-skip\",\n severity: \"editorial\",\n evaluate: (content) => {\n const skips: Array<string> = [];\n let previous = 1;\n for (const heading of content.outline) {\n if (heading.level > previous + 1) {\n skips.push(`h${previous} → h${heading.level} \"${clip(heading.text, 40)}\"`);\n }\n previous = heading.level;\n }\n return skips.length === 0\n ? []\n : [\n {\n message: `Heading levels skip ${skips.length} time(s): ${skips.slice(0, 3).join(\"; \")}${skips.length > 3 ? \"; …\" : \"\"}.`,\n fix: \"Nest headings one level at a time; style a heading with CSS, not by choosing a deeper level.\",\n },\n ];\n },\n },\n {\n name: \"h1-title-mismatch\",\n severity: \"editorial\",\n evaluate: (content) => {\n const [h1] = content.h1;\n if (content.h1.length !== 1 || !h1 || !content.title) return [];\n const title = significantTerms(content.title);\n const shared = [...significantTerms(h1)].some((term) => title.has(term));\n return shared\n ? []\n : [\n {\n message: `H1 \"${clip(h1, 60)}\" shares no significant term with the title \"${clip(content.title, 60)}\".`,\n fix: \"Make the H1 and <title> describe the same topic; a searcher who clicks the title should recognize the page.\",\n },\n ];\n },\n },\n {\n name: \"faq-not-visible\",\n severity: \"structural\",\n evaluate: (content) => {\n const visible = matchText(content.text);\n return content.jsonLd\n .filter((node) => hasType(node, FAQ_TYPES))\n .flatMap((faq) => asArray(faq[\"mainEntity\"]))\n .filter(isRecord)\n .flatMap((question) => {\n const name = stringField(question, \"name\");\n const answers = asArray(question[\"acceptedAnswer\"])\n .filter(isRecord)\n .flatMap((answer) => {\n const text = stringField(answer, \"text\");\n return text === undefined ? [] : [text];\n });\n const missing = [\n ...(name !== undefined && !isVisible(visible, name) ? [\"question\"] : []),\n ...(answers.some((answer) => !isVisible(visible, answer)) ? [\"answer\"] : []),\n ];\n return missing.length === 0\n ? []\n : [\n {\n message: `FAQPage ${missing.join(\" and \")} for \"${clip(name ?? \"(unnamed)\", 70)}\" is not in the rendered page text.`,\n fix: \"Render every FAQ question and answer in the server HTML, or drop the entry from the FAQPage JSON-LD.\",\n },\n ];\n });\n },\n },\n {\n name: \"offer-price-not-visible\",\n severity: \"structural\",\n evaluate: (content) =>\n content.jsonLd\n .filter((node) => hasType(node, OFFER_TYPES))\n .flatMap((offer) => {\n const hidden = pricesOf(offer).filter((price) => !priceIsVisible(price, content.text));\n if (hidden.length === 0) return [];\n const currency = stringField(offer, \"priceCurrency\");\n const label = stringField(offer, \"name\") ?? typesOf(offer).join(\", \");\n return [\n {\n message: `${label} price ${hidden.map((price) => `${price}${currency ? ` ${currency}` : \"\"}`).join(\", \")} does not appear in the rendered page text.`,\n fix: \"Show the offer's price in the server-rendered copy, or correct the JSON-LD price.\",\n },\n ];\n }),\n },\n {\n name: \"webpage-head-mismatch\",\n severity: \"editorial\",\n evaluate: (content) =>\n content.jsonLd\n .filter((node) => hasType(node, WEB_PAGE_TYPES))\n .flatMap((page) => {\n const findings: Array<{ message: string; fix: string }> = [];\n const name = stringField(page, \"name\");\n if (name !== undefined && content.title && !namesAgree(name, content.title)) {\n findings.push({\n message: `${typesOf(page).join(\", \")} name \"${clip(name, 60)}\" does not match the title \"${clip(content.title, 60)}\".`,\n fix: \"Derive the JSON-LD name from the same value as the <title>.\",\n });\n }\n const description = stringField(page, \"description\");\n if (\n description !== undefined &&\n content.description &&\n matchText(description) !== matchText(content.description)\n ) {\n findings.push({\n message: `${typesOf(page).join(\", \")} description differs from the meta description.`,\n fix: \"Derive the JSON-LD description from the same value as the meta description.\",\n });\n }\n return findings;\n }),\n },\n {\n name: \"thin-content\",\n severity: \"editorial\",\n evaluate: (content, options) =>\n options.minWords !== undefined && content.wordCount < options.minWords\n ? [\n {\n message: `Main content has ${content.wordCount} words (min ${options.minWords}).`,\n fix: \"Answer the page's search intent in more depth, or merge it into a stronger page.\",\n },\n ]\n : [],\n },\n];\n\n/** Run every content rule against one rendered page. Graph-independent; pure. */\nexport const checkPageContent = (\n content: PageContent,\n options: ContentCheckOptions = {},\n): Array<ContentFinding> =>\n CONTENT_RULES.flatMap((rule) =>\n rule.evaluate(content, options).map((raw) => ({\n rule: rule.name,\n severity: rule.severity,\n message: raw.message,\n fix: raw.fix,\n })),\n );\n\n// ---------------------------------------------------------------------------\n// Rendered pages against the declared graph\n// ---------------------------------------------------------------------------\n\n/** A word-count floor for sitemap-eligible pages matching a path glob. */\nexport interface WordCountRule {\n /** Path glob: `*` matches within a segment, `**` matches across segments. */\n readonly path: string;\n readonly minWords: number;\n}\n\n/** Project policy for rendered content checks. */\nexport interface ContentPolicy {\n /** Word-count floors; a page matching several rules must meet the highest. */\n readonly minWords?: ReadonlyArray<WordCountRule> | undefined;\n}\n\n/** One declared page as the server rendered it, or why it could not be read. */\nexport type RenderedDocument =\n | {\n readonly path: string;\n readonly ok: true;\n /** Path of the final response URL, after redirects, encoded or not. */\n readonly finalPath: string;\n readonly html: string;\n }\n | { readonly path: string; readonly ok: false; readonly error: string };\n\nconst declaresNoindex = (node: SeoNode): boolean =>\n node.policy.robots?.toLowerCase().includes(\"noindex\") === true;\n\n/**\n * The declared pages a rendered check fetches: every sitemap-eligible page,\n * plus concrete pages that declare `noindex` so the rendered robots meta can be\n * held to that declaration. Param templates and redirects render no page of\n * their own.\n */\nexport const renderedCheckTargets = (graph: SeoGraph): Array<string> =>\n [...graph.nodes.values()]\n .filter(\n (node) =>\n isSitemapEligible(node) ||\n (declaresNoindex(node) &&\n node.policy.redirectTo === undefined &&\n !node.path.includes(\"$\")),\n )\n .map((node) => node.path);\n\nconst PATH_BASE = \"https://pagegraph.invalid\";\n\n/**\n * A path as a URL serializes it (`/café` → `/caf%C3%A9`), trailing slash\n * dropped. Declared paths, response URLs, and canonicals are compared in this\n * one form, or a page with non-ASCII segments would read as redirected.\n */\nconst urlPath = (value: string): string | undefined => {\n try {\n return normalizePath(new URL(value, PATH_BASE));\n } catch {\n return undefined;\n }\n};\n\n/**\n * Judge rendered documents against the declared graph. Every indexable page must\n * render, stay on its path, keep a self-referencing canonical and indexable\n * robots meta, and pass {@link checkPageContent}; a page that declares `noindex`\n * must render a `noindex` robots meta.\n *\n * Robots are read from the `<meta name=\"robots\">` tag only. An `X-Robots-Tag`\n * header is usually host policy (preview hosts noindex everything), so holding\n * pages to it would fail every local run for a reason no declaration controls.\n */\nexport function checkRenderedContent(\n graph: SeoGraph,\n documents: ReadonlyArray<RenderedDocument>,\n policy: ContentPolicy = {},\n): Array<Violation> {\n const violations: Array<Violation> = [];\n const push = (\n rule: string,\n severity: Severity,\n path: string,\n message: string,\n fix: string,\n ): void => {\n violations.push({ rule, severity, path, message, fix });\n };\n\n const wordRules = (policy.minWords ?? []).map((rule) => ({\n rule,\n matcher: globToRegExp(rule.path),\n }));\n const eligible = [...graph.nodes.values()].filter(isSitemapEligible).map((node) => node.path);\n for (const { rule, matcher } of wordRules) {\n if (!eligible.some((path) => matcher.test(path))) {\n violations.push({\n severity: \"structural\",\n rule: \"content-rule-unmatched\",\n message: `Word-count rule \"${rule.path}\" matches no sitemap-eligible page.`,\n fix: \"Point the rule at a sitemap-eligible path, or drop it.\",\n });\n }\n }\n\n for (const document of documents) {\n const node = graph.nodes.get(document.path);\n if (node === undefined) continue;\n const declaredPath = urlPath(document.path);\n if (!document.ok) {\n push(\n \"rendered-page-unavailable\",\n \"structural\",\n document.path,\n `Could not read the rendered page: ${document.error}.`,\n \"Serve the declared page with a 2xx HTML response.\",\n );\n continue;\n }\n if (urlPath(document.finalPath) !== declaredPath) {\n push(\n \"rendered-page-unavailable\",\n \"structural\",\n document.path,\n `Rendered page redirects to \"${document.finalPath}\".`,\n \"Declare the redirect with `redirectTo`, or serve the page at its declared path.\",\n );\n continue;\n }\n\n const content = extractPageContent(document.html);\n const renderedNoindex = content.robots?.toLowerCase().includes(\"noindex\") === true;\n const indexable = isSitemapEligible(node);\n\n if (!indexable) {\n if (!renderedNoindex) {\n push(\n \"rendered-robots-mismatch\",\n \"structural\",\n document.path,\n `Declared robots \"${node.policy.robots}\" but the rendered page has ${content.robots ? `robots \"${content.robots}\"` : \"no robots meta\"}.`,\n \"Render the declared robots value in the page's <meta name=\\\"robots\\\">.\",\n );\n }\n continue;\n }\n\n if (renderedNoindex) {\n push(\n \"rendered-robots-mismatch\",\n \"structural\",\n document.path,\n `Page is in the sitemap but renders robots \"${content.robots}\".`,\n \"Drop the noindex from the rendered head, or declare the page noindex and remove its sitemap policy.\",\n );\n }\n\n const canonical = content.canonical === undefined ? undefined : urlPath(content.canonical);\n if (canonical !== declaredPath) {\n push(\n \"rendered-canonical-mismatch\",\n \"structural\",\n document.path,\n content.canonical === undefined\n ? \"Rendered page has no canonical link.\"\n : `Rendered canonical \"${content.canonical}\" does not point at \"${document.path}\".`,\n \"Render a self-referencing canonical for every indexable page.\",\n );\n }\n\n const floors = wordRules.filter(({ matcher }) => matcher.test(document.path));\n const minWords =\n floors.length === 0 ? undefined : Math.max(...floors.map(({ rule }) => rule.minWords));\n for (const finding of checkPageContent(content, { minWords })) {\n push(finding.rule, finding.severity, document.path, finding.message, finding.fix);\n }\n }\n return violations;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"content-CHd0X4s6.js","names":["decodeEntities","decodeEntities"],"sources":["../src/core/inspect-html.ts","../src/core/links.ts","../src/core/content.ts"],"sourcesContent":["/**\n * Read a rendered `<head>` and validate it: title, meta\n * (description/robots/og/twitter), canonical link, and `application/ld+json`\n * blocks, with a minimal per-type JSON-LD check.\n *\n * This is the pure half of `pagegraph inspect --live` — the half worth having on its\n * own. The CLI fetches a URL and hands the body here; a test suite can render a\n * page and hand *that* here, asserting the head it actually ships. Both get the\n * same verdict, because it is the same function.\n *\n * No HTML-parsing dependency, by design: a `<head>` is small and well-formed, so\n * string/regex scanning is enough, and the package's core stays zero-dependency\n * (which is also why the object guard below is hand-rolled — `effect/Predicate`\n * is not reachable from this entry). It is honest about its limits: it does not\n * build a DOM, so exotic markup (commented-out tags, CDATA, attributes spanning\n * constructs) is out of scope. This validates *rendered output*, not arbitrary\n * HTML.\n *\n * `issues` are blocking: a non-empty list makes `pagegraph inspect --live` exit 1.\n * Required tags are `<title>`, `meta[name=description]`, and\n * `link[rel=canonical]`; any JSON-LD that fails to parse or fails its minimal\n * schema is also blocking.\n */\n\nexport interface JsonLdReport {\n type: string;\n valid: boolean;\n errors: Array<string>;\n}\n\nexport interface LiveHeadReport {\n url: string;\n status: number;\n title?: string | undefined;\n description?: string | undefined;\n canonical?: string | undefined;\n robots?: string | undefined;\n og: Record<string, string>;\n twitter: Record<string, string>;\n jsonLd: Array<JsonLdReport>;\n issues: Array<string>;\n}\n\nconst ENTITIES: Record<string, string> = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n \""\": '\"',\n \"'\": \"'\",\n \"'\": \"'\",\n};\n\nconst decodeEntities = (value: string): string =>\n value.replace(/&(?:amp|lt|gt|quot|#39|apos);/g, (match) => ENTITIES[match] ?? match);\n\n/** Pull double/single-quoted attributes off a single tag string. */\nconst parseAttrs = (tag: string): Record<string, string> => {\n const attrs: Record<string, string> = {};\n const re = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)')/g;\n let match: RegExpExecArray | null;\n while ((match = re.exec(tag)) !== null) {\n attrs[match[1]!.toLowerCase()] = decodeEntities(match[2] ?? match[3] ?? \"\");\n }\n return attrs;\n};\n\n/** Normalize a JSON-LD `@type` (string or array) to a single readable label. */\nconst typeName = (value: unknown): string => {\n if (typeof value === \"string\") return value;\n if (Array.isArray(value))\n return value.filter((v) => typeof v === \"string\").join(\", \") || \"unknown\";\n return \"unknown\";\n};\n\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n // NOTE: This zero-dependency core entry cannot import Effect; JSON-LD validation happens immediately after this shallow narrowing.\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\n/** Flatten a parsed JSON-LD payload (single object, array, or `@graph`) to items. */\nconst collectItems = (parsed: unknown): Array<Record<string, unknown>> => {\n if (Array.isArray(parsed)) return parsed.filter(isObject);\n if (isObject(parsed)) {\n if (Array.isArray(parsed[\"@graph\"])) return parsed[\"@graph\"].filter(isObject);\n return [parsed];\n }\n return [];\n};\n\nconst countQuestions = (mainEntity: unknown): number => {\n if (!Array.isArray(mainEntity)) return 0;\n return mainEntity.filter((entry) => isObject(entry) && typeName(entry[\"@type\"]) === \"Question\")\n .length;\n};\n\nconst validateItemListElements = (value: unknown): Array<string> => {\n if (!Array.isArray(value) || value.length < 1) {\n return [\"ItemList needs at least one `itemListElement` entry.\"];\n }\n const errors: Array<string> = [];\n value.forEach((entry, index) => {\n if (!isObject(entry) || typeName(entry[\"@type\"]) !== \"ListItem\") {\n errors.push(`ItemList entry ${index + 1} is not a ListItem.`);\n return;\n }\n if (entry[\"position\"] !== index + 1) {\n errors.push(`ItemList entry ${index + 1} has an invalid position.`);\n }\n if (!entry[\"name\"] || !entry[\"url\"]) {\n errors.push(`ItemList entry ${index + 1} needs a name and URL.`);\n }\n });\n return errors;\n};\n\n/** Minimal per-type validation — enough to catch an empty or malformed block. */\nconst validateItem = (item: Record<string, unknown>): JsonLdReport => {\n const type = typeName(item[\"@type\"]);\n const errors: Array<string> = [];\n\n if (type === \"Article\" || type === \"NewsArticle\" || type === \"BlogPosting\") {\n if (!item[\"headline\"]) errors.push(\"Article is missing `headline`.\");\n if (!item[\"datePublished\"]) errors.push(\"Article is missing `datePublished`.\");\n } else if (type === \"FAQPage\") {\n if (countQuestions(item[\"mainEntity\"]) < 1) {\n errors.push(\"FAQPage needs at least one Question in `mainEntity`.\");\n }\n } else if (type === \"BreadcrumbList\") {\n const items = item[\"itemListElement\"];\n if (!Array.isArray(items) || items.length < 2) {\n errors.push(\"BreadcrumbList needs at least two `itemListElement` entries.\");\n }\n } else if (type === \"ItemList\") {\n errors.push(...validateItemListElements(item[\"itemListElement\"]));\n const items = item[\"itemListElement\"];\n if (Array.isArray(items) && item[\"numberOfItems\"] !== items.length) {\n errors.push(\"ItemList `numberOfItems` does not match its entries.\");\n }\n }\n\n return { type, valid: errors.length === 0, errors };\n};\n\nconst validateLdJson = (raw: string): Array<JsonLdReport> => {\n let parsed: unknown;\n // NOTE: JSON.parse boundary: converts the native parse throw into an unparseable JsonLdReport value in a pure sync validator\n try {\n parsed = JSON.parse(raw);\n } catch (cause) {\n return [\n {\n type: \"unparseable\",\n valid: false,\n errors: [`JSON parse error: ${cause instanceof Error ? cause.message : String(cause)}`],\n },\n ];\n }\n const items = collectItems(parsed);\n if (items.length === 0) {\n return [{ type: \"unknown\", valid: false, errors: [\"No JSON-LD object found in block.\"] }];\n }\n return items.map(validateItem);\n};\n\n/** Parse a rendered HTML document's `<head>` into a report. Pure. */\nexport const inspectHtml = (url: string, status: number, html: string): LiveHeadReport => {\n const headMatch = html.match(/<head[^>]*>([\\s\\S]*?)<\\/head>/i);\n const head = headMatch ? headMatch[1]! : html;\n\n const titleMatch = head.match(/<title[^>]*>([\\s\\S]*?)<\\/title>/i);\n const title = titleMatch ? decodeEntities(titleMatch[1]!.trim()) : undefined;\n\n const og: Record<string, string> = {};\n const twitter: Record<string, string> = {};\n let description: string | undefined;\n let robots: string | undefined;\n\n for (const tag of head.match(/<meta\\b[^>]*>/gi) ?? []) {\n const attrs = parseAttrs(tag);\n const content = attrs[\"content\"];\n if (content === undefined) continue;\n const property = attrs[\"property\"];\n const name = attrs[\"name\"];\n if (property?.startsWith(\"og:\")) og[property] = content;\n else if (name?.startsWith(\"twitter:\")) twitter[name] = content;\n else if (name === \"description\") description = content;\n else if (name === \"robots\") robots = content;\n }\n\n let canonical: string | undefined;\n for (const tag of head.match(/<link\\b[^>]*>/gi) ?? []) {\n const attrs = parseAttrs(tag);\n if (attrs[\"rel\"] === \"canonical\") canonical = attrs[\"href\"];\n }\n\n const jsonLd: Array<JsonLdReport> = [];\n const scriptRe = /<script\\b[^>]*type=[\"']application\\/ld\\+json[\"'][^>]*>([\\s\\S]*?)<\\/script>/gi;\n let scriptMatch: RegExpExecArray | null;\n while ((scriptMatch = scriptRe.exec(head)) !== null) {\n jsonLd.push(...validateLdJson(scriptMatch[1]!.trim()));\n }\n\n const issues: Array<string> = [];\n if (status >= 400) issues.push(`Fetch returned HTTP ${status}.`);\n if (!title) issues.push(\"Missing <title>.\");\n if (!description) issues.push(\"Missing meta description.\");\n if (!canonical) issues.push(\"Missing canonical link.\");\n for (const block of jsonLd) {\n if (!block.valid)\n issues.push(...block.errors.map((error) => `JSON-LD (${block.type}): ${error}`));\n }\n\n return { url, status, title, description, canonical, robots, og, twitter, jsonLd, issues };\n};\n\n/** A non-empty `issues` list fails `pagegraph inspect --live` (exit 1). */\nexport const hasBlockingIssues = (report: LiveHeadReport): boolean => report.issues.length > 0;\n","/**\n * Pure link-graph utilities: read anchors out of rendered HTML, model the\n * rendered graph, compute homepage depth, and diff it against the declared\n * graph. Effect- and framework-free so it runs in the CLI, a Worker, or a test\n * on the same data.\n *\n * This is the \"served HTML\" half of the SEO graph: the declared graph describes\n * intent, this describes what a crawler actually receives.\n */\n\n/** Where an anchor sits in the document, coarsely. */\nexport type AnchorRegion = \"nav\" | \"footer\" | \"header\" | \"body\";\n\nexport interface Anchor {\n readonly href: string;\n readonly text: string;\n readonly region: AnchorRegion;\n /** True when the resolved target shares the page's origin. */\n readonly internal: boolean;\n}\n\nexport interface LinkEdge {\n readonly from: string;\n readonly to: string;\n readonly region: AnchorRegion;\n}\n\nexport interface RenderedGraph {\n /** Every same-origin page path the crawl rendered, in first-seen order. */\n readonly nodes: ReadonlyArray<string>;\n readonly edges: ReadonlyArray<LinkEdge>;\n readonly internalEdges: ReadonlyArray<LinkEdge>;\n /** Same-origin body-region edges: the contextual surface. */\n readonly contextualEdges: ReadonlyArray<LinkEdge>;\n /** Same-origin pages with no incoming internal edge (\"reachable only via nav\" is not implied). */\n readonly orphans: ReadonlyArray<string>;\n readonly depthByPath: ReadonlyMap<string, number>;\n readonly maxDepth: number | null;\n}\n\nconst REGION_TAGS = new Set([\"nav\", \"footer\", \"header\"]);\n\nconst TOKEN =\n /<(\\/?)(nav|footer|header)\\b[^>]*>|<a\\b([^>]*)>([\\s\\S]*?)<\\/a>/gi;\n\n// A real `href` attribute is preceded by whitespace (or the tag's start). The\n// boundary keeps `data-href`, `xhref`, and similar attributes from reading as\n// links, and stops a decoy from winning over a genuine `href`.\nconst HREF = /(?:^|\\s)href\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s>]+))/i;\n\nconst BASE_TAG = /<base\\b[^>]*>/gi;\n\nconst NON_NAVIGABLE = /^(?:#|mailto:|tel:|javascript:|data:)/i;\n\nconst stripTags = (value: string): string => value.replace(/<[^>]*>/g, \" \");\n\nconst decodeEntities = (value: string): string =>\n value\n .replace(/ /gi, \" \")\n .replace(/&/gi, \"&\")\n .replace(/</gi, \"<\")\n .replace(/>/gi, \">\")\n .replace(/"/gi, \"\\\"\")\n .replace(/'|'/gi, \"'\");\n\nconst anchorText = (value: string): string =>\n decodeEntities(stripTags(value)).replace(/\\s+/g, \" \").trim();\n\n/** Normalize a same-origin URL to the graph's path key (no query/hash; \"/\" preserved). */\nexport const normalizePath = (url: URL): string => {\n const path = url.pathname.replace(/\\/+$/, \"\");\n return path === \"\" ? \"/\" : path;\n};\n\nconst hrefValue = (attributes: string): string | undefined => {\n const match = HREF.exec(attributes);\n if (match === null) return undefined;\n return match[1] ?? match[2] ?? match[3];\n};\n\n/**\n * The document base URL. Browsers use the first `<base>` element that has an\n * `href` attribute and ignore every later one, so a `<base>` carrying only\n * `target` is skipped, but the first `<base href>` wins even when its value is\n * empty (it then resolves to the document URL) or invalid (the base is ignored,\n * resolving to the document URL). With no `<base href>` at all, links resolve\n * against the response URL.\n */\nconst documentBase = (html: string, responseUrl: URL): URL => {\n BASE_TAG.lastIndex = 0;\n let tag: RegExpExecArray | null;\n while ((tag = BASE_TAG.exec(html)) !== null) {\n const href = hrefValue(tag[0]);\n if (href === undefined) continue;\n if (href.length === 0) return responseUrl;\n try {\n return new URL(href, responseUrl);\n } catch {\n return responseUrl;\n }\n }\n return responseUrl;\n};\n\n/**\n * Extract every anchor in document order, tagging each with the region tag it\n * sits inside. Anchor text keeps only its letters and a coarse entity decode;\n * this is a classifier input, not a renderer.\n */\nexport const extractAnchors = (html: string, baseUrl: string): ReadonlyArray<Anchor> => {\n const base = new URL(baseUrl);\n const resolutionBase = documentBase(html, base);\n const regions: Array<AnchorRegion> = [];\n const anchors: Array<Anchor> = [];\n TOKEN.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = TOKEN.exec(html)) !== null) {\n const closing = match[1];\n const regionTag = match[2];\n if (regionTag !== undefined) {\n const region = regionTag.toLowerCase() as AnchorRegion;\n if (closing === \"/\") {\n const index = regions.lastIndexOf(region);\n if (index !== -1) regions.splice(index, 1);\n } else {\n regions.push(region);\n }\n continue;\n }\n const attributes = match[3];\n const raw = hrefValue(attributes);\n if (raw === undefined || raw.length === 0 || NON_NAVIGABLE.test(raw)) continue;\n let resolved: URL;\n try {\n resolved = new URL(raw, resolutionBase);\n } catch {\n continue;\n }\n if (resolved.protocol !== \"http:\" && resolved.protocol !== \"https:\") continue;\n const region = regions.length === 0 ? \"body\" : regions[regions.length - 1]!;\n anchors.push({\n href: resolved.href,\n text: anchorText(match[4] ?? \"\"),\n region,\n internal: resolved.origin === base.origin,\n });\n }\n return anchors;\n};\n\nexport interface RenderedPage {\n readonly url: string;\n readonly anchors: ReadonlyArray<Anchor>;\n}\n\nconst breadthFirstDepth = (\n edges: ReadonlyArray<LinkEdge>,\n root: string,\n): ReadonlyMap<string, number> => {\n const adjacency = new Map<string, Set<string>>();\n for (const edge of edges) {\n const targets = adjacency.get(edge.from) ?? new Set<string>();\n targets.add(edge.to);\n adjacency.set(edge.from, targets);\n }\n const depth = new Map<string, number>([[root, 0]]);\n const queue: Array<string> = [root];\n while (queue.length > 0) {\n const current = queue.shift()!;\n const currentDepth = depth.get(current)!;\n for (const next of adjacency.get(current) ?? []) {\n if (depth.has(next)) continue;\n depth.set(next, currentDepth + 1);\n queue.push(next);\n }\n }\n return depth;\n};\n\n/**\n * Assemble a {@link RenderedGraph} from page/edge sets. Edges are already\n * same-origin and path-keyed by the callers, so this is the shared, pure tail of\n * {@link buildRenderedGraph} and {@link renderedGraphFromEdges}.\n */\nconst graphFromEdges = (\n nodes: Set<string>,\n edges: Array<LinkEdge>,\n root: string,\n): RenderedGraph => {\n const rootKey = root.replace(/\\/+$/, \"\") || \"/\";\n const internalEdges = edges;\n const contextualEdges = edges.filter((edge) => edge.region === \"body\");\n const incoming = new Set(internalEdges.map((edge) => edge.to));\n const orphans = [...nodes].filter((path) => path !== rootKey && !incoming.has(path)).sort();\n const depthByPath = breadthFirstDepth(internalEdges, rootKey);\n let maxDepth: number | null = null;\n for (const value of depthByPath.values()) {\n if (maxDepth === null || value > maxDepth) maxDepth = value;\n }\n return {\n nodes: [...nodes],\n edges,\n internalEdges,\n contextualEdges,\n orphans,\n depthByPath,\n maxDepth,\n };\n};\n\n/**\n * Build the rendered graph from crawled pages. `origin` fixes the graph's\n * same-origin boundary — pages and anchors on another origin are dropped, not\n * silently folded in — and `root` is the normalized path the depth BFS starts\n * from (the seed's final path when the homepage redirected). Every page path is\n * keyed the way the declared graph keys its routes.\n */\nexport const buildRenderedGraph = (\n origin: string,\n pages: ReadonlyArray<RenderedPage>,\n root: string = \"/\",\n): RenderedGraph => {\n const originUrl = new URL(origin);\n const graphOrigin = originUrl.origin;\n const edges: Array<LinkEdge> = [];\n const nodes = new Set<string>();\n for (const page of pages) {\n let pageUrl: URL;\n try {\n pageUrl = new URL(page.url);\n } catch {\n continue;\n }\n if (pageUrl.origin !== graphOrigin) continue;\n const path = normalizePath(pageUrl);\n nodes.add(path);\n for (const anchor of page.anchors) {\n let targetUrl: URL;\n try {\n targetUrl = new URL(anchor.href);\n } catch {\n continue;\n }\n if (targetUrl.origin !== graphOrigin) continue;\n const target = normalizePath(targetUrl);\n if (target === path) continue;\n edges.push({ from: path, to: target, region: anchor.region });\n }\n }\n return graphFromEdges(nodes, edges, root);\n};\n\n/** A URL or an already-normalized graph path key both collapse to the path key. */\nconst pathKeyOf = (value: string): string => {\n try {\n return normalizePath(new URL(value));\n } catch {\n const pathname = value.split(/[?#]/, 1)[0] ?? \"\";\n return pathname.replace(/\\/+$/, \"\") || \"/\";\n }\n};\n\n/**\n * Rebuild a {@link RenderedGraph} from a rendered-edge set alone — the artifact\n * `pagegraph links verify --emit-rendered` writes. Path keys are normalized the\n * same way as a crawl, and `nodes` carries the rendered page paths the artifact\n * recorded, so an edge-less page (a redirect target with no internal links) stays\n * a node and its orphan/depth report survives replay. Without `nodes` the node set\n * falls back to the edge endpoints.\n */\nexport const renderedGraphFromEdges = (\n edges: ReadonlyArray<LinkEdge>,\n root: string = \"/\",\n nodes: ReadonlyArray<string> = [],\n): RenderedGraph => {\n const nodeSet = new Set(nodes.map(pathKeyOf));\n const kept: Array<LinkEdge> = [];\n for (const edge of edges) {\n const from = pathKeyOf(edge.from);\n const to = pathKeyOf(edge.to);\n nodeSet.add(from);\n nodeSet.add(to);\n kept.push({ from, to, region: edge.region });\n }\n return graphFromEdges(nodeSet, kept, root);\n};\n\nexport interface SimpleEdge {\n readonly from: string;\n readonly to: string;\n}\n\nexport interface LinkGraphDiff {\n /** Declared internal edges with no matching rendered anchor. */\n readonly declaredNotRendered: ReadonlyArray<SimpleEdge>;\n /** Rendered contextual edges that no declared edge accounts for. */\n readonly renderedNotDeclared: ReadonlyArray<SimpleEdge>;\n readonly declaredCount: number;\n readonly renderedContextualCount: number;\n}\n\nconst edgeKey = (edge: SimpleEdge): string => `${edge.from}\\u0000${edge.to}`;\n\nconst normalizeEdge = (edge: SimpleEdge): SimpleEdge => ({\n from: edge.from === \"\" ? \"/\" : edge.from.replace(/\\/+$/, \"\") || \"/\",\n to: edge.to === \"\" ? \"/\" : edge.to.replace(/\\/+$/, \"\") || \"/\",\n});\n\n/**\n * Diff rendered contextual edges against declared `related`/`crumb` edges. Both\n * sides are path-keyed; the diff is directional and reports each direction\n * separately because they call for different fixes.\n */\nexport const diffLinkGraph = (\n declared: ReadonlyArray<SimpleEdge>,\n rendered: RenderedGraph,\n): LinkGraphDiff => {\n const declaredNormalized = declared.map(normalizeEdge);\n const declaredKeys = new Set(declaredNormalized.map(edgeKey));\n const renderedKeys = new Set(rendered.contextualEdges.map(edgeKey));\n const declaredNotRendered = declaredNormalized.filter(\n (edge) => !renderedKeys.has(edgeKey(edge)),\n );\n const renderedNotDeclared = rendered.internalEdges\n .filter((edge) => edge.region === \"body\")\n .map((edge) => normalizeEdge(edge))\n .filter((edge) => !declaredKeys.has(edgeKey(edge)));\n return {\n declaredNotRendered,\n renderedNotDeclared,\n declaredCount: declaredNormalized.length,\n renderedContextualCount: rendered.contextualEdges.length,\n };\n};\n\n/** A page the crawl could not render; its outgoing anchors are absent. */\nexport interface RenderedEdgeFailure {\n readonly url: string;\n readonly error: string;\n}\n\n/**\n * Provenance for a rendered-edge artifact: enough to trust the edges and to\n * refuse a gate that would assert on an incomplete crawl. `root` is the seed's\n * normalized final path (its post-redirect path), so depth and orphans rebuild\n * exactly as the crawl computed them.\n */\nexport interface RenderedEdgeArtifactCrawl {\n /** Pages whose HTML was rendered into `edges`. */\n readonly pages: number;\n /** Normalized final path of the seed — the depth BFS root. */\n readonly root: string;\n /** The crawl's page budget (`--limit`). */\n readonly limit: number;\n /** True when discovery outran `limit`; a gate must not assert on this. */\n readonly truncated: boolean;\n /** True when any captured body was cut at `--max-body-bytes`. */\n readonly bodyTruncated: boolean;\n /** URLs whose captured body was cut; anchors past the cutoff are missing. */\n readonly truncatedPages: ReadonlyArray<string>;\n /** Pages that failed to fetch; their outgoing anchors are missing. */\n readonly failures: ReadonlyArray<RenderedEdgeFailure>;\n /** Non-HTML responses from link-only discovery; these do not hide HTML pages. */\n readonly nonHtml?: ReadonlyArray<{ readonly url: string; readonly finalUrl: string; readonly contentType: string }>;\n readonly discoveryFailures?: ReadonlyArray<RenderedEdgeFailure>;\n readonly skipped?: ReadonlyArray<string>;\n readonly sitemapTruncated?: boolean;\n readonly sitemapUsed?: boolean;\n}\n\n/**\n * The stable, `decodeRenderedEdges`-compatible artifact `pagegraph links verify\n * --emit-rendered` writes and `--rendered` reads. It carries the raw served edges\n * (with `region`), the rendered page paths, and the provenance a coverage gate\n * needs to decide whether the crawl is trustworthy, so one crawl can be asserted\n * repeatedly without re-fetching.\n */\nexport interface RenderedEdgeArtifact {\n readonly kind: \"links-rendered\";\n readonly schemaVersion: 1;\n readonly origin: string;\n readonly seed: string;\n readonly crawl: RenderedEdgeArtifactCrawl;\n /** Normalized paths of every rendered page — replay's node set. */\n readonly nodes: ReadonlyArray<string>;\n readonly edges: ReadonlyArray<LinkEdge>;\n}\n\n/** The rendered-edge artifact schema this package writes and accepts. */\nexport const RENDERED_EDGE_ARTIFACT_SCHEMA_VERSION = 1 as const;\n\nconst ARTIFACT_KIND = \"links-rendered\";\n\nconst REGIONS: ReadonlySet<string> = new Set([\"nav\", \"footer\", \"header\", \"body\"]);\n\nconst decodeArtifactEdges = (input: unknown): ReadonlyArray<LinkEdge> => {\n if (!Array.isArray(input)) {\n throw new Error(\"`edges` must be an array of { from, to, region } edges\");\n }\n return input.map((item) => {\n if (item === null || typeof item !== \"object\") {\n throw new Error(\"each artifact edge must be an object with string `from` and `to`\");\n }\n const { from, to, region } = item as { from?: unknown; to?: unknown; region?: unknown };\n if (typeof from !== \"string\" || typeof to !== \"string\") {\n throw new Error(\"each artifact edge must have string `from` and `to`\");\n }\n if (region !== undefined && !REGIONS.has(region as string)) {\n throw new Error(\n `artifact edge region must be one of nav, footer, header, body (received ${JSON.stringify(region)})`,\n );\n }\n return { from, to, region: (region as AnchorRegion | undefined) ?? \"body\" };\n });\n};\n\nconst decodeArtifactNodes = (input: unknown): ReadonlyArray<string> => {\n if (input === undefined) return [];\n if (!Array.isArray(input) || !input.every((path) => typeof path === \"string\")) {\n throw new Error(\"rendered-edge artifact `nodes` must be an array of paths\");\n }\n return input;\n};\n\nconst decodeArtifactFailures = (input: unknown): ReadonlyArray<RenderedEdgeFailure> => {\n if (input === undefined) return [];\n if (!Array.isArray(input)) {\n throw new Error(\"rendered-edge artifact `crawl.failures` must be an array\");\n }\n return input.map((item) => {\n if (item === null || typeof item !== \"object\") {\n throw new Error(\"each artifact failure must be an object with string `url` and `error`\");\n }\n const { url, error } = item as { url?: unknown; error?: unknown };\n if (typeof url !== \"string\" || typeof error !== \"string\") {\n throw new Error(\"each artifact failure must have string `url` and `error`\");\n }\n return { url, error };\n });\n};\n\nconst requiredAbsoluteUrl = (value: unknown, field: string): string => {\n if (typeof value !== \"string\" || value === \"\") {\n throw new Error(`rendered-edge artifact must carry a non-empty string \\`${field}\\``);\n }\n try {\n void new URL(value);\n } catch {\n throw new Error(\n `rendered-edge artifact \\`${field}\\` must be an absolute URL (received ${JSON.stringify(value)})`,\n );\n }\n return value;\n};\n\nconst requiredInteger = (value: unknown, field: string, minimum: number): number => {\n if (!Number.isSafeInteger(value) || (value as number) < minimum) {\n throw new Error(\n `rendered-edge artifact \\`${field}\\` must be an integer >= ${minimum} (received ${JSON.stringify(value)})`,\n );\n }\n return value as number;\n};\n\n/**\n * Decode a rendered-edge artifact from user input. Throws on any other shape —\n * this is user input, not a provider payload. `root`, `bodyTruncated`,\n * `truncatedPages`, `failures`, and `nodes` are optional so a hand-written edge\n * dump that only carries provenance can still be replayed.\n */\nexport const decodeRenderedEdgeArtifact = (input: unknown): RenderedEdgeArtifact => {\n if (input === null || typeof input !== \"object\") {\n throw new Error(\"rendered-edge artifact must be an object\");\n }\n const value = input as Record<string, unknown>;\n if (value[\"kind\"] !== ARTIFACT_KIND) {\n throw new Error(\n `rendered-edge artifact \\`kind\\` must be \"${ARTIFACT_KIND}\" (received ${JSON.stringify(value[\"kind\"])})`,\n );\n }\n if (value[\"schemaVersion\"] !== RENDERED_EDGE_ARTIFACT_SCHEMA_VERSION) {\n throw new Error(\n `unsupported rendered-edge artifact schemaVersion: ${JSON.stringify(value[\"schemaVersion\"])}`,\n );\n }\n const origin = requiredAbsoluteUrl(value[\"origin\"], \"origin\");\n const seed = requiredAbsoluteUrl(value[\"seed\"], \"seed\");\n const crawl = value[\"crawl\"];\n if (crawl === null || typeof crawl !== \"object\") {\n throw new Error(\"rendered-edge artifact must carry a `crawl` object\");\n }\n const provenance = crawl as Record<string, unknown>;\n const truncatedPages = provenance[\"truncatedPages\"];\n if (\n truncatedPages !== undefined &&\n (!Array.isArray(truncatedPages) || !truncatedPages.every((url) => typeof url === \"string\"))\n ) {\n throw new Error(\"rendered-edge artifact `truncatedPages` must be an array of URLs\");\n }\n const bodyTruncated = provenance[\"bodyTruncated\"];\n if (bodyTruncated !== undefined && typeof bodyTruncated !== \"boolean\") {\n throw new Error(\"rendered-edge artifact `bodyTruncated` must be a boolean\");\n }\n if (typeof provenance[\"truncated\"] !== \"boolean\") {\n throw new Error(\"rendered-edge artifact `crawl.truncated` must be a boolean\");\n }\n const skipped = provenance[\"skipped\"];\n if (skipped !== undefined && (!Array.isArray(skipped) || !skipped.every((url) => typeof url === \"string\"))) {\n throw new Error(\"rendered-edge artifact `crawl.skipped` must be an array of URLs\");\n }\n const nonHtml = provenance[\"nonHtml\"];\n if (nonHtml !== undefined && (!Array.isArray(nonHtml) || !nonHtml.every((item) =>\n item !== null && typeof item === \"object\" &&\n typeof item.url === \"string\" && typeof item.finalUrl === \"string\" && typeof item.contentType === \"string\"\n ))) {\n throw new Error(\"rendered-edge artifact `crawl.nonHtml` must be an array of { url, finalUrl, contentType }\");\n }\n if (Array.isArray(nonHtml)) {\n for (const item of nonHtml) {\n requiredAbsoluteUrl(item.url, \"crawl.nonHtml.url\");\n requiredAbsoluteUrl(item.finalUrl, \"crawl.nonHtml.finalUrl\");\n }\n }\n for (const field of [\"sitemapTruncated\", \"sitemapUsed\"] as const) {\n if (provenance[field] !== undefined && typeof provenance[field] !== \"boolean\") {\n throw new Error(`rendered-edge artifact \\`crawl.${field}\\` must be a boolean`);\n }\n }\n return {\n kind: ARTIFACT_KIND,\n schemaVersion: RENDERED_EDGE_ARTIFACT_SCHEMA_VERSION,\n origin,\n seed,\n crawl: {\n pages: requiredInteger(provenance[\"pages\"], \"crawl.pages\", 0),\n root: typeof provenance[\"root\"] === \"string\" ? provenance[\"root\"] : pathKeyOf(seed),\n limit: requiredInteger(provenance[\"limit\"], \"crawl.limit\", 1),\n truncated: provenance[\"truncated\"],\n bodyTruncated: bodyTruncated === true,\n truncatedPages: (truncatedPages as ReadonlyArray<string> | undefined) ?? [],\n failures: decodeArtifactFailures(provenance[\"failures\"]),\n ...(nonHtml === undefined ? {} : { nonHtml: nonHtml as ReadonlyArray<{ url: string; finalUrl: string; contentType: string }> }),\n ...(provenance[\"discoveryFailures\"] === undefined ? {} : { discoveryFailures: decodeArtifactFailures(provenance[\"discoveryFailures\"]) }),\n ...(skipped === undefined ? {} : { skipped: skipped as ReadonlyArray<string> }),\n ...(provenance[\"sitemapTruncated\"] === undefined ? {} : { sitemapTruncated: provenance[\"sitemapTruncated\"] as boolean }),\n ...(provenance[\"sitemapUsed\"] === undefined ? {} : { sitemapUsed: provenance[\"sitemapUsed\"] as boolean }),\n },\n nodes: decodeArtifactNodes(value[\"nodes\"]),\n edges: decodeArtifactEdges(value[\"edges\"]),\n };\n};\n","/**\n * Content checks over server-rendered HTML: headings, structured data against\n * visible text, word count, and declared-vs-rendered robots and canonical.\n *\n * These run on rendered documents, not on the route graph, because the facts\n * they judge do not exist in a declaration: headings come from components, JSON-LD\n * and the `<title>` are composed by `head()` at render time, and the visible text\n * is whatever the page's components produce. The graph supplies the other half —\n * which pages are indexable and what their canonical path and robots policy are —\n * so the declared-vs-rendered rules can say \"this page drifted from its\n * declaration\".\n *\n * Like `inspectHtml`, this is pure string scanning of rendered output with no\n * HTML-parsing dependency, and it keeps the core entry zero-dependency. It reads\n * server-rendered markup, which is what a crawler receives before any script runs;\n * text a client inserts after hydration is invisible here, and is reported as such.\n */\n\nimport type { Severity, Violation } from \"./checks\";\nimport { globToRegExp } from \"./checks\";\nimport type { SeoGraph, SeoNode } from \"./graph\";\nimport { inspectHtml } from \"./inspect-html\";\nimport { normalizePath } from \"./links\";\nimport { isSitemapEligible } from \"./projections\";\n\nexport interface Heading {\n readonly level: number;\n readonly text: string;\n}\n\n/** A JSON-LD object that carries an `@type`, found at any depth of any block. */\nexport type JsonLdNode = Readonly<Record<string, unknown>>;\n\n/** What a rendered document says, reduced to the fields the content rules read. */\nexport interface PageContent {\n readonly title: string | undefined;\n readonly description: string | undefined;\n readonly robots: string | undefined;\n readonly canonical: string | undefined;\n /** Every H1 in the body, in document order, by accessible text. */\n readonly h1: ReadonlyArray<string>;\n /** Headings in the main content region (`<main>`, else the body without nav and footer). */\n readonly outline: ReadonlyArray<Heading>;\n /** Words of visible text in the main content region. */\n readonly wordCount: number;\n /** Visible body text with whitespace collapsed; scripts, styles, and templates removed. */\n readonly text: string;\n /** Every typed JSON-LD node on the page, flattened out of arrays, `@graph`, and nesting. */\n readonly jsonLd: ReadonlyArray<JsonLdNode>;\n}\n\nexport type ContentRuleName =\n | \"missing-h1\"\n | \"multiple-h1\"\n | \"empty-h1\"\n | \"heading-level-skip\"\n | \"h1-title-mismatch\"\n | \"faq-not-visible\"\n | \"offer-price-not-visible\"\n | \"webpage-head-mismatch\"\n | \"thin-content\";\n\nexport interface ContentFinding {\n readonly rule: ContentRuleName;\n readonly severity: Severity;\n readonly message: string;\n readonly fix: string;\n}\n\nexport interface ContentCheckOptions {\n /** Minimum words of main-region text; omit to skip the `thin-content` rule. */\n readonly minWords?: number | undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Extraction\n// ---------------------------------------------------------------------------\n\nconst NAMED_ENTITIES: Record<string, string> = {\n amp: \"&\",\n lt: \"<\",\n gt: \">\",\n quot: '\"',\n apos: \"'\",\n nbsp: \" \",\n ndash: \"–\",\n mdash: \"—\",\n lsquo: \"‘\",\n rsquo: \"’\",\n ldquo: \"“\",\n rdquo: \"”\",\n hellip: \"…\",\n middot: \"·\",\n bull: \"•\",\n copy: \"©\",\n reg: \"®\",\n trade: \"™\",\n times: \"×\",\n euro: \"€\",\n pound: \"£\",\n};\n\nconst decodeEntities = (value: string): string =>\n value.replace(/&(#x[0-9a-f]+|#\\d+|[a-z]+);/gi, (entity, code: string) => {\n if (code.startsWith(\"#\")) {\n const point =\n code[1] === \"x\" || code[1] === \"X\"\n ? Number.parseInt(code.slice(2), 16)\n : Number.parseInt(code.slice(1), 10);\n return Number.isInteger(point) && point > 0 && point <= 0x10ffff\n ? String.fromCodePoint(point)\n : entity;\n }\n return NAMED_ENTITIES[code.toLowerCase()] ?? entity;\n });\n\n/** Elements whose contents a reader never sees as page text. */\nconst INVISIBLE = /<(script|style|template|noscript|svg)\\b[\\s\\S]*?<\\/\\1\\s*>/gi;\nconst COMMENT = /<!--[\\s\\S]*?-->/g;\n\nconst withoutInvisible = (html: string): string =>\n html.replace(COMMENT, \" \").replace(INVISIBLE, \" \");\n\nconst textOf = (html: string): string =>\n decodeEntities(html.replace(/<[^>]*>/g, \" \"))\n .replace(/\\s+/g, \" \")\n .trim();\n\nconst attributeOf = (tag: string, name: string): string | undefined => {\n const match = new RegExp(`(?:^|\\\\s)${name}\\\\s*=\\\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\\\s>]+))`, \"i\").exec(\n tag,\n );\n const value = match?.[1] ?? match?.[2] ?? match?.[3];\n return value === undefined ? undefined : decodeEntities(value);\n};\n\nconst bodyOf = (html: string): string => {\n const body = /<body\\b[^>]*>([\\s\\S]*)<\\/body>/i.exec(html);\n if (body) return body[1]!;\n return html.replace(/<head\\b[^>]*>[\\s\\S]*?<\\/head>/i, \" \");\n};\n\n/**\n * The main content region: `<main>` when the page has one, otherwise the body\n * without its `<nav>` and `<footer>` blocks. Site chrome repeats on every page,\n * so counting its headings or words would judge the layout, not the page.\n */\nconst mainRegionOf = (body: string): string => {\n const main = /<main\\b[^>]*>([\\s\\S]*?)<\\/main\\s*>/i.exec(body);\n if (main) return main[1]!;\n return body.replace(/<(nav|footer)\\b[\\s\\S]*?<\\/\\1\\s*>/gi, \" \");\n};\n\nconst HEADING = /<h([1-6])\\b([^>]*)>([\\s\\S]*?)<\\/h\\1\\s*>/gi;\n\n/**\n * A heading's accessible text: its content, else its `aria-label`, else the\n * `alt` of images inside it (a logo H1).\n */\nconst headingText = (attributes: string, inner: string): string => {\n const text = textOf(inner);\n if (text !== \"\") return text;\n const label = attributeOf(attributes, \"aria-label\")?.trim();\n if (label) return label;\n return [...inner.matchAll(/<img\\b[^>]*>/gi)]\n .map((image) => attributeOf(image[0], \"alt\")?.trim() ?? \"\")\n .filter(Boolean)\n .join(\" \");\n};\n\nconst headingsOf = (html: string): Array<Heading> =>\n [...html.matchAll(HEADING)].map((match) => ({\n level: Number(match[1]),\n text: headingText(match[2]!, match[3]!),\n }));\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n // NOTE: This zero-dependency core entry cannot import Effect; JSON-LD is untyped input narrowed field by field below.\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\n/** Every object with an `@type`, at any depth — offers nest in products, questions in FAQ pages. */\nconst collectTyped = (value: unknown, into: Array<JsonLdNode>): void => {\n if (Array.isArray(value)) {\n for (const entry of value) collectTyped(entry, into);\n return;\n }\n if (!isRecord(value)) return;\n if (value[\"@type\"] !== undefined) into.push(value);\n for (const child of Object.values(value)) collectTyped(child, into);\n};\n\nconst JSON_LD = /<script\\b[^>]*type\\s*=\\s*[\"']application\\/ld\\+json[\"'][^>]*>([\\s\\S]*?)<\\/script\\s*>/gi;\n\n/** Parse every JSON-LD block. A block that does not parse is `inspectHtml`'s finding, not ours. */\nconst jsonLdOf = (html: string): Array<JsonLdNode> => {\n const nodes: Array<JsonLdNode> = [];\n for (const match of html.matchAll(JSON_LD)) {\n let parsed: unknown;\n // NOTE: JSON.parse boundary: an unparseable block is skipped here; inspectHtml reports it.\n try {\n parsed = JSON.parse(match[1]!.trim());\n } catch {\n continue;\n }\n collectTyped(parsed, nodes);\n }\n return nodes;\n};\n\n/**\n * Text normalized for \"does this appear on the page\" comparisons: Unicode-folded,\n * lowercased, and reduced to runs of letters and digits. Punctuation, quotes,\n * dashes, and markup spacing differ freely between JSON-LD and rendered copy\n * without changing what a reader sees.\n */\nexport const matchText = (value: string): string =>\n value\n .normalize(\"NFKC\")\n .toLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter(Boolean)\n .join(\" \");\n\nconst countWords = (text: string): number => {\n const normalized = matchText(text);\n return normalized === \"\" ? 0 : normalized.split(\" \").length;\n};\n\n/** Reduce a rendered HTML document to the fields the content rules read. Pure. */\nexport const extractPageContent = (html: string): PageContent => {\n const head = inspectHtml(\"\", 200, html);\n const body = withoutInvisible(bodyOf(html));\n const main = mainRegionOf(body);\n return {\n title: head.title,\n description: head.description,\n robots: head.robots,\n canonical: head.canonical,\n h1: headingsOf(body)\n .filter((heading) => heading.level === 1)\n .map((heading) => heading.text),\n outline: headingsOf(main),\n wordCount: countWords(textOf(main)),\n text: textOf(body),\n jsonLd: jsonLdOf(html),\n };\n};\n\n// ---------------------------------------------------------------------------\n// Rules\n// ---------------------------------------------------------------------------\n\nconst typesOf = (node: JsonLdNode): Array<string> => {\n const type = node[\"@type\"];\n if (typeof type === \"string\") return [type];\n if (Array.isArray(type)) return type.filter((entry): entry is string => typeof entry === \"string\");\n return [];\n};\n\nconst hasType = (node: JsonLdNode, types: ReadonlySet<string>): boolean =>\n typesOf(node).some((type) => types.has(type));\n\nconst stringField = (node: JsonLdNode, field: string): string | undefined => {\n const value = node[field];\n return typeof value === \"string\" && value.trim() !== \"\" ? value : undefined;\n};\n\n/** Standard HTML element names; other angle-bracket text is not markup. */\nconst HTML_ELEMENT = new RegExp(\n `</?(?:${\"a abbr address article aside audio b bdi bdo blockquote br button caption cite code col colgroup dd del details dfn div dl dt em fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i img ins kbd label legend li main mark nav ol p picture pre q s samp section small span strong sub summary sup table tbody td tfoot th thead time tr u ul var video wbr\".split(\" \").join(\"|\")})\\\\b[^>]*>`,\n \"gi\",\n);\n\nconst inPage = (visible: string, text: string): boolean => {\n const needle = matchText(text);\n return needle === \"\" || ` ${visible} `.includes(` ${needle} `);\n};\n\n/**\n * JSON-LD text may carry the HTML Google allows in answers, or plain text with\n * angle-bracket placeholders such as `samva-<id>._domainkey`, which the page\n * renders escaped. Only standard element tags are markup, and a candidate with\n * no closing tag is also compared literally because `<i>` is ambiguous.\n */\nconst isVisible = (visible: string, candidate: string): boolean =>\n inPage(visible, decodeEntities(candidate.replace(HTML_ELEMENT, \" \"))) ||\n (!/<\\//.test(candidate) && inPage(visible, decodeEntities(candidate)));\n\nconst clip = (value: string, max = 80): string =>\n value.length > max ? `${value.slice(0, max - 1)}…` : value;\n\nconst STOPWORDS = new Set(\n \"a an and are as at be but by can for from get has have how in into is it its more not of on or our per than that the their this to use via vs was what when who why will with you your\".split(\n \" \",\n ),\n);\n\n/**\n * Terms that carry meaning, reduced to a crude stem: stopwords drop, a plural\n * `s` folds, and long words compare by their first six letters, so\n * \"Authentication\" and \"Authenticate\" or \"templates\" and \"template\" agree. The\n * rule only asks whether any term is shared, so a lenient stem errs toward\n * silence, never toward a false warning.\n */\nconst significantTerms = (value: string): Set<string> =>\n new Set(\n matchText(value)\n .split(\" \")\n .filter((term) => term !== \"\" && !STOPWORDS.has(term) && (term.length > 2 || /\\d/.test(term)))\n .map((term) => (term.length > 3 && term.endsWith(\"s\") ? term.slice(0, -1) : term))\n .map((term) => term.slice(0, 6)),\n );\n\n/** schema.org `WebPage` and the subtypes whose `name`/`description` describe the page itself. */\nconst WEB_PAGE_TYPES: ReadonlySet<string> = new Set([\n \"WebPage\",\n \"AboutPage\",\n \"CheckoutPage\",\n \"CollectionPage\",\n \"ContactPage\",\n \"FAQPage\",\n \"ItemPage\",\n \"MedicalWebPage\",\n \"ProfilePage\",\n \"QAPage\",\n \"RealEstateListing\",\n \"SearchResultsPage\",\n]);\n\nconst OFFER_TYPES: ReadonlySet<string> = new Set([\"Offer\", \"AggregateOffer\"]);\nconst FAQ_TYPES: ReadonlySet<string> = new Set([\"FAQPage\"]);\n\nconst asArray = (value: unknown): Array<unknown> =>\n value === undefined ? [] : Array.isArray(value) ? value : [value];\n\n/** Parse a schema.org price (`19`, `\"19.00\"`, `\"1,000\"`); undefined when it is not a number. */\nconst parsePrice = (value: unknown): number | undefined => {\n if (typeof value === \"number\") return Number.isFinite(value) ? value : undefined;\n if (typeof value !== \"string\") return undefined;\n const cleaned = value.replace(/[^\\d.]/g, \"\");\n if (cleaned === \"\" || !/^\\d*\\.?\\d+$/.test(cleaned)) return undefined;\n return Number(cleaned);\n};\n\nconst pricesOf = (offer: JsonLdNode): Array<number> =>\n [\n offer[\"price\"],\n offer[\"lowPrice\"],\n offer[\"highPrice\"],\n ...asArray(offer[\"priceSpecification\"]).flatMap((spec) =>\n isRecord(spec) ? [spec[\"price\"], spec[\"minPrice\"], spec[\"maxPrice\"]] : [],\n ),\n ].flatMap((value) => {\n const price = parsePrice(value);\n return price === undefined ? [] : [price];\n });\n\n/**\n * Whether a price appears as a number in the visible text: `19` matches `$19`,\n * `19.00`, and `19/mo`; `1000` matches `1,000`; `0.5` matches `0.50`. A price of\n * zero also matches the word \"free\".\n */\nexport const priceIsVisible = (price: number, text: string): boolean => {\n if (price === 0 && /\\bfree\\b/i.test(text)) return true;\n const [whole, fraction = \"\"] = String(price).split(\".\");\n const grouped = whole!.replace(/\\B(?=(\\d{3})+(?!\\d))/g, \"[,.\\\\u00a0 ]?\");\n const decimals = fraction === \"\" ? \"(?:[.,]0+)?\" : `[.,]${fraction}0*`;\n return new RegExp(`(?<![\\\\d.,])${grouped}${decimals}(?![\\\\d]|[.,]\\\\d)`).test(text);\n};\n\n/**\n * Whether a JSON-LD `name` agrees with the rendered `<title>`. A title usually\n * wraps the page name in a site-name affix (\"Pricing | Example\"), so either\n * containing the other counts as agreement.\n */\nconst namesAgree = (a: string, b: string): boolean => {\n const left = matchText(a);\n const right = matchText(b);\n if (left === \"\" || right === \"\") return true;\n return ` ${left} `.includes(` ${right} `) || ` ${right} `.includes(` ${left} `);\n};\n\ninterface ContentRule {\n readonly name: ContentRuleName;\n readonly severity: Severity;\n readonly evaluate: (\n content: PageContent,\n options: ContentCheckOptions,\n ) => ReadonlyArray<{ readonly message: string; readonly fix: string }>;\n}\n\n/**\n * Every content rule, in one place. Heading-count and structured-data rules that\n * Google treats as eligibility or policy problems are structural; outline and\n * wording smells are editorial.\n */\nconst CONTENT_RULES: ReadonlyArray<ContentRule> = [\n {\n name: \"missing-h1\",\n severity: \"structural\",\n evaluate: (content) =>\n content.h1.length === 0\n ? [\n {\n message: \"Page has no H1.\",\n fix: \"Render exactly one <h1> that names the page's topic.\",\n },\n ]\n : [],\n },\n {\n name: \"multiple-h1\",\n severity: \"structural\",\n evaluate: (content) =>\n content.h1.length > 1\n ? [\n {\n message: `Page has ${content.h1.length} H1s: ${content.h1.map((text) => `\"${clip(text, 40)}\"`).join(\", \")}.`,\n fix: \"Keep one <h1> for the page topic; demote the others to <h2>.\",\n },\n ]\n : [],\n },\n {\n name: \"empty-h1\",\n severity: \"structural\",\n evaluate: (content) =>\n content.h1.some((text) => text === \"\")\n ? [\n {\n message: \"An H1 has no text, aria-label, or image alt.\",\n fix: \"Give the <h1> visible text that names the page's topic.\",\n },\n ]\n : [],\n },\n {\n // The outline starts under the page's H1, so a first heading of h3 is a\n // skip just as h2 → h4 is. Going back up any number of levels is fine.\n name: \"heading-level-skip\",\n severity: \"editorial\",\n evaluate: (content) => {\n const skips: Array<string> = [];\n let previous = 1;\n for (const heading of content.outline) {\n if (heading.level > previous + 1) {\n skips.push(`h${previous} → h${heading.level} \"${clip(heading.text, 40)}\"`);\n }\n previous = heading.level;\n }\n return skips.length === 0\n ? []\n : [\n {\n message: `Heading levels skip ${skips.length} time(s): ${skips.slice(0, 3).join(\"; \")}${skips.length > 3 ? \"; …\" : \"\"}.`,\n fix: \"Nest headings one level at a time; style a heading with CSS, not by choosing a deeper level.\",\n },\n ];\n },\n },\n {\n name: \"h1-title-mismatch\",\n severity: \"editorial\",\n evaluate: (content) => {\n const [h1] = content.h1;\n if (content.h1.length !== 1 || !h1 || !content.title) return [];\n const title = significantTerms(content.title);\n const shared = [...significantTerms(h1)].some((term) => title.has(term));\n return shared\n ? []\n : [\n {\n message: `H1 \"${clip(h1, 60)}\" shares no significant term with the title \"${clip(content.title, 60)}\".`,\n fix: \"Make the H1 and <title> describe the same topic; a searcher who clicks the title should recognize the page.\",\n },\n ];\n },\n },\n {\n name: \"faq-not-visible\",\n severity: \"structural\",\n evaluate: (content) => {\n const visible = matchText(content.text);\n return content.jsonLd\n .filter((node) => hasType(node, FAQ_TYPES))\n .flatMap((faq) => asArray(faq[\"mainEntity\"]))\n .filter(isRecord)\n .flatMap((question) => {\n const name = stringField(question, \"name\");\n const answers = asArray(question[\"acceptedAnswer\"])\n .filter(isRecord)\n .flatMap((answer) => {\n const text = stringField(answer, \"text\");\n return text === undefined ? [] : [text];\n });\n const missing = [\n ...(name !== undefined && !isVisible(visible, name) ? [\"question\"] : []),\n ...(answers.some((answer) => !isVisible(visible, answer)) ? [\"answer\"] : []),\n ];\n return missing.length === 0\n ? []\n : [\n {\n message: `FAQPage ${missing.join(\" and \")} for \"${clip(name ?? \"(unnamed)\", 70)}\" is not in the rendered page text.`,\n fix: \"Render every FAQ question and answer in the server HTML, or drop the entry from the FAQPage JSON-LD.\",\n },\n ];\n });\n },\n },\n {\n name: \"offer-price-not-visible\",\n severity: \"structural\",\n evaluate: (content) =>\n content.jsonLd\n .filter((node) => hasType(node, OFFER_TYPES))\n .flatMap((offer) => {\n const hidden = pricesOf(offer).filter((price) => !priceIsVisible(price, content.text));\n if (hidden.length === 0) return [];\n const currency = stringField(offer, \"priceCurrency\");\n const label = stringField(offer, \"name\") ?? typesOf(offer).join(\", \");\n return [\n {\n message: `${label} price ${hidden.map((price) => `${price}${currency ? ` ${currency}` : \"\"}`).join(\", \")} does not appear in the rendered page text.`,\n fix: \"Show the offer's price in the server-rendered copy, or correct the JSON-LD price.\",\n },\n ];\n }),\n },\n {\n name: \"webpage-head-mismatch\",\n severity: \"editorial\",\n evaluate: (content) =>\n content.jsonLd\n .filter((node) => hasType(node, WEB_PAGE_TYPES))\n .flatMap((page) => {\n const findings: Array<{ message: string; fix: string }> = [];\n const name = stringField(page, \"name\");\n if (name !== undefined && content.title && !namesAgree(name, content.title)) {\n findings.push({\n message: `${typesOf(page).join(\", \")} name \"${clip(name, 60)}\" does not match the title \"${clip(content.title, 60)}\".`,\n fix: \"Derive the JSON-LD name from the same value as the <title>.\",\n });\n }\n const description = stringField(page, \"description\");\n if (\n description !== undefined &&\n content.description &&\n matchText(description) !== matchText(content.description)\n ) {\n findings.push({\n message: `${typesOf(page).join(\", \")} description differs from the meta description.`,\n fix: \"Derive the JSON-LD description from the same value as the meta description.\",\n });\n }\n return findings;\n }),\n },\n {\n name: \"thin-content\",\n severity: \"editorial\",\n evaluate: (content, options) =>\n options.minWords !== undefined && content.wordCount < options.minWords\n ? [\n {\n message: `Main content has ${content.wordCount} words (min ${options.minWords}).`,\n fix: \"Answer the page's search intent in more depth, or merge it into a stronger page.\",\n },\n ]\n : [],\n },\n];\n\n/** Run every content rule against one rendered page. Graph-independent; pure. */\nexport const checkPageContent = (\n content: PageContent,\n options: ContentCheckOptions = {},\n): Array<ContentFinding> =>\n CONTENT_RULES.flatMap((rule) =>\n rule.evaluate(content, options).map((raw) => ({\n rule: rule.name,\n severity: rule.severity,\n message: raw.message,\n fix: raw.fix,\n })),\n );\n\n// ---------------------------------------------------------------------------\n// Rendered pages against the declared graph\n// ---------------------------------------------------------------------------\n\n/** A word-count floor for sitemap-eligible pages matching a path glob. */\nexport interface WordCountRule {\n /** Path glob: `*` matches within a segment, `**` matches across segments. */\n readonly path: string;\n readonly minWords: number;\n}\n\n/** Project policy for rendered content checks. */\nexport interface ContentPolicy {\n /** Word-count floors; a page matching several rules must meet the highest. */\n readonly minWords?: ReadonlyArray<WordCountRule> | undefined;\n}\n\n/** One declared page as the server rendered it, or why it could not be read. */\nexport type RenderedDocument =\n | {\n readonly path: string;\n readonly ok: true;\n /** Path of the final response URL, after redirects, encoded or not. */\n readonly finalPath: string;\n readonly html: string;\n }\n | { readonly path: string; readonly ok: false; readonly error: string };\n\nconst declaresNoindex = (node: SeoNode): boolean =>\n node.policy.robots?.toLowerCase().includes(\"noindex\") === true;\n\n/**\n * The declared pages a rendered check fetches: every sitemap-eligible page,\n * plus concrete pages that declare `noindex` so the rendered robots meta can be\n * held to that declaration. Param templates and redirects render no page of\n * their own.\n */\nexport const renderedCheckTargets = (graph: SeoGraph): Array<string> =>\n [...graph.nodes.values()]\n .filter(\n (node) =>\n isSitemapEligible(node) ||\n (declaresNoindex(node) &&\n node.policy.redirectTo === undefined &&\n !node.path.includes(\"$\")),\n )\n .map((node) => node.path);\n\nconst PATH_BASE = \"https://pagegraph.invalid\";\n\n/**\n * A path as a URL serializes it (`/café` → `/caf%C3%A9`), trailing slash\n * dropped. Declared paths, response URLs, and canonicals are compared in this\n * one form, or a page with non-ASCII segments would read as redirected.\n */\nconst urlPath = (value: string): string | undefined => {\n try {\n return normalizePath(new URL(value, PATH_BASE));\n } catch {\n return undefined;\n }\n};\n\n/**\n * Judge rendered documents against the declared graph. Every indexable page must\n * render, stay on its path, keep a self-referencing canonical and indexable\n * robots meta, and pass {@link checkPageContent}; a page that declares `noindex`\n * must render a `noindex` robots meta.\n *\n * Robots are read from the `<meta name=\"robots\">` tag only. An `X-Robots-Tag`\n * header is usually host policy (preview hosts noindex everything), so holding\n * pages to it would fail every local run for a reason no declaration controls.\n */\nexport function checkRenderedContent(\n graph: SeoGraph,\n documents: ReadonlyArray<RenderedDocument>,\n policy: ContentPolicy = {},\n): Array<Violation> {\n const violations: Array<Violation> = [];\n const push = (\n rule: string,\n severity: Severity,\n path: string,\n message: string,\n fix: string,\n ): void => {\n violations.push({ rule, severity, path, message, fix });\n };\n\n const wordRules = (policy.minWords ?? []).map((rule) => ({\n rule,\n matcher: globToRegExp(rule.path),\n }));\n const eligible = [...graph.nodes.values()].filter(isSitemapEligible).map((node) => node.path);\n for (const { rule, matcher } of wordRules) {\n if (!eligible.some((path) => matcher.test(path))) {\n violations.push({\n severity: \"structural\",\n rule: \"content-rule-unmatched\",\n message: `Word-count rule \"${rule.path}\" matches no sitemap-eligible page.`,\n fix: \"Point the rule at a sitemap-eligible path, or drop it.\",\n });\n }\n }\n\n for (const document of documents) {\n const node = graph.nodes.get(document.path);\n if (node === undefined) continue;\n const declaredPath = urlPath(document.path);\n if (!document.ok) {\n push(\n \"rendered-page-unavailable\",\n \"structural\",\n document.path,\n `Could not read the rendered page: ${document.error}.`,\n \"Serve the declared page with a 2xx HTML response.\",\n );\n continue;\n }\n if (urlPath(document.finalPath) !== declaredPath) {\n push(\n \"rendered-page-unavailable\",\n \"structural\",\n document.path,\n `Rendered page redirects to \"${document.finalPath}\".`,\n \"Declare the redirect with `redirectTo`, or serve the page at its declared path.\",\n );\n continue;\n }\n\n const content = extractPageContent(document.html);\n const renderedNoindex = content.robots?.toLowerCase().includes(\"noindex\") === true;\n const indexable = isSitemapEligible(node);\n\n if (!indexable) {\n if (!renderedNoindex) {\n push(\n \"rendered-robots-mismatch\",\n \"structural\",\n document.path,\n `Declared robots \"${node.policy.robots}\" but the rendered page has ${content.robots ? `robots \"${content.robots}\"` : \"no robots meta\"}.`,\n \"Render the declared robots value in the page's <meta name=\\\"robots\\\">.\",\n );\n }\n continue;\n }\n\n if (renderedNoindex) {\n push(\n \"rendered-robots-mismatch\",\n \"structural\",\n document.path,\n `Page is in the sitemap but renders robots \"${content.robots}\".`,\n \"Drop the noindex from the rendered head, or declare the page noindex and remove its sitemap policy.\",\n );\n }\n\n const canonical = content.canonical === undefined ? undefined : urlPath(content.canonical);\n if (canonical !== declaredPath) {\n push(\n \"rendered-canonical-mismatch\",\n \"structural\",\n document.path,\n content.canonical === undefined\n ? \"Rendered page has no canonical link.\"\n : `Rendered canonical \"${content.canonical}\" does not point at \"${document.path}\".`,\n \"Render a self-referencing canonical for every indexable page.\",\n );\n }\n\n const floors = wordRules.filter(({ matcher }) => matcher.test(document.path));\n const minWords =\n floors.length === 0 ? undefined : Math.max(...floors.map(({ rule }) => rule.minWords));\n for (const finding of checkPageContent(content, { minWords })) {\n push(finding.rule, finding.severity, document.path, finding.message, finding.fix);\n }\n }\n return violations;\n}\n"],"mappings":"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"}
|
|
@@ -1,129 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
//#region src/core/declare.d.ts
|
|
3
|
-
/**
|
|
4
|
-
* Consumer-augmented registry.
|
|
5
|
-
*
|
|
6
|
-
* ```ts
|
|
7
|
-
* declare module "pagegraph" {
|
|
8
|
-
* interface Register {
|
|
9
|
-
* paths: FileRouteTypes["fullPaths"];
|
|
10
|
-
* kinds: "page" | "article" | "hub";
|
|
11
|
-
* }
|
|
12
|
-
* }
|
|
13
|
-
* ```
|
|
14
|
-
*/
|
|
15
|
-
interface Register {}
|
|
16
|
-
/** Every public full path in the consumer's route tree (type-only; erased at runtime). */
|
|
17
|
-
type PublicPath = Register extends {
|
|
18
|
-
paths: infer P extends string;
|
|
19
|
-
} ? P : string;
|
|
20
|
-
/** The consumer's page taxonomy. A label: nothing in the core branches on it. */
|
|
21
|
-
type SeoKind = Register extends {
|
|
22
|
-
kinds: infer K extends string;
|
|
23
|
-
} ? K : string;
|
|
24
|
-
interface SitemapPolicy {
|
|
25
|
-
priority: number;
|
|
26
|
-
changeFrequency: "weekly" | "monthly";
|
|
27
|
-
}
|
|
28
|
-
interface RouteSeo {
|
|
29
|
-
kind: SeoKind;
|
|
30
|
-
/** Breadcrumb label; fn form reads the match (e.g. loaderData frontmatter title). */
|
|
31
|
-
crumb?: string | ((match: AnyRouteMatch) => string) | undefined;
|
|
32
|
-
/** false = excluded from sitemap. Absent on non-indexable kinds is fine. */
|
|
33
|
-
sitemap?: SitemapPolicy | false | undefined;
|
|
34
|
-
/** meta robots value, e.g. "noindex, follow" on a hub page. */
|
|
35
|
-
robots?: string | undefined;
|
|
36
|
-
/** Typed cross-link edges. */
|
|
37
|
-
related?: ReadonlyArray<PublicPath> | undefined;
|
|
38
|
-
/** How OTHER pages render a card for this page. */
|
|
39
|
-
link?: {
|
|
40
|
-
title: string;
|
|
41
|
-
description: string;
|
|
42
|
-
} | undefined;
|
|
43
|
-
/** This route is a redirect/alias. */
|
|
44
|
-
redirectTo?: PublicPath | undefined;
|
|
45
|
-
/**
|
|
46
|
-
* When this page's content last changed meaningfully, as an ISO 8601 date.
|
|
47
|
-
* Feeds the sitemap `<lastmod>` and the freshness report. Content instances
|
|
48
|
-
* carry their own dates instead; a param route's value is never inherited.
|
|
49
|
-
*/
|
|
50
|
-
modifiedAt?: string | undefined;
|
|
51
|
-
}
|
|
52
|
-
declare module "@tanstack/react-router" {
|
|
53
|
-
interface StaticDataRouteOption {
|
|
54
|
-
seo?: RouteSeo | undefined;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
//#endregion
|
|
58
|
-
//#region src/core/graph.d.ts
|
|
59
|
-
/**
|
|
60
|
-
* Where a node came from: `"route"` for a structural route declaration, or the
|
|
61
|
-
* `source` label of the collection that produced the instance (e.g. "blog").
|
|
62
|
-
*/
|
|
63
|
-
type SeoSource = string;
|
|
64
|
-
interface SeoNode {
|
|
65
|
-
/** Canonical path, no origin (e.g. "/pricing", "/blog/my-post"). */
|
|
66
|
-
path: string;
|
|
67
|
-
kind: SeoKind;
|
|
68
|
-
source: SeoSource;
|
|
69
|
-
/** Route-declared policy, or synthesized (kind + inherited sitemap) for instances. */
|
|
70
|
-
policy: RouteSeo;
|
|
71
|
-
instance?: {
|
|
72
|
-
title: string;
|
|
73
|
-
description?: string | undefined;
|
|
74
|
-
publishedAt?: string | undefined;
|
|
75
|
-
modifiedAt?: string | undefined;
|
|
76
|
-
} | undefined;
|
|
77
|
-
}
|
|
78
|
-
type SeoEdgeType = "crumb-parent" | "related" | "redirect" | "collection-member";
|
|
79
|
-
interface SeoEdge {
|
|
80
|
-
from: string;
|
|
81
|
-
to: string;
|
|
82
|
-
type: SeoEdgeType;
|
|
83
|
-
}
|
|
84
|
-
interface SeoGraph {
|
|
85
|
-
nodes: Map<string, SeoNode>;
|
|
86
|
-
edges: Array<SeoEdge>;
|
|
87
|
-
/** Exact-path ownership conflicts encountered while assembling graph sources. */
|
|
88
|
-
collisions?: ReadonlyArray<{
|
|
89
|
-
path: string;
|
|
90
|
-
sources: ReadonlyArray<SeoSource>;
|
|
91
|
-
}> | undefined;
|
|
92
|
-
}
|
|
93
|
-
/** One concrete page produced by a collection. */
|
|
94
|
-
interface SeoInstance {
|
|
95
|
-
/** Canonical path of the page (e.g. "/blog/my-post"). */
|
|
96
|
-
readonly path: string;
|
|
97
|
-
readonly title: string;
|
|
98
|
-
readonly description?: string | undefined;
|
|
99
|
-
readonly publishedAt?: string | undefined;
|
|
100
|
-
readonly modifiedAt?: string | undefined;
|
|
101
|
-
}
|
|
102
|
-
interface SeoCollection {
|
|
103
|
-
/**
|
|
104
|
-
* The param route these instances render through (e.g. "/blog/$slug"). Instances
|
|
105
|
-
* inherit this route's declared policy (kind + sitemap) — the graph reads it from
|
|
106
|
-
* the structural node, so declarations stay the single source of truth.
|
|
107
|
-
*/
|
|
108
|
-
readonly route: PublicPath;
|
|
109
|
-
/** The `source` stamped on every node this collection produces (e.g. "blog"). */
|
|
110
|
-
readonly source: SeoSource;
|
|
111
|
-
readonly instances: ReadonlyArray<SeoInstance>;
|
|
112
|
-
/** Edges the collection declares between its instances and the rest of the graph. */
|
|
113
|
-
readonly edges?: ReadonlyArray<SeoEdge> | undefined;
|
|
114
|
-
}
|
|
115
|
-
interface BuildSeoGraphInput {
|
|
116
|
-
readonly routeTree: AnyRoute;
|
|
117
|
-
readonly collections?: ReadonlyArray<SeoCollection> | undefined;
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Build the SEO graph from route declarations and content collections.
|
|
121
|
-
*
|
|
122
|
-
* Synchronous: the caller materializes its collections before calling, so there is
|
|
123
|
-
* no async work here. Callers own the origin.
|
|
124
|
-
*/
|
|
125
|
-
declare function buildSeoGraph(input: BuildSeoGraphInput): SeoGraph;
|
|
126
|
-
//#endregion
|
|
1
|
+
import { _ as SeoKind, a as SeoGraph, u as SeoSource } from "./graph-D1PIndsC.js";
|
|
127
2
|
//#region src/core/freshness.d.ts
|
|
128
3
|
/** Project policy: pages whose last change is older than this are stale. */
|
|
129
4
|
interface FreshnessPolicy {
|
|
@@ -317,7 +192,7 @@ interface Violation {
|
|
|
317
192
|
* A contextual-link coverage rule: every sitemap-eligible page matching the
|
|
318
193
|
* `path` glob must have at least `minInbound` incoming contextual links. Unlike
|
|
319
194
|
* the static rules, these come from project policy (a CLI flag or
|
|
320
|
-
* `
|
|
195
|
+
* `pagegraph.config.ts`), because "which pages are money pages" is app knowledge.
|
|
321
196
|
*
|
|
322
197
|
* The same rule shape is evaluated twice: `checkCoverage` counts declared
|
|
323
198
|
* `related` edges, and `checkRenderedCoverage` counts the contextual anchors a
|
|
@@ -368,7 +243,7 @@ declare function checkCoverage(graph: SeoGraph, rules: ReadonlyArray<CoverageRul
|
|
|
368
243
|
* distinction the declared pass draws by counting `related` edges alone.
|
|
369
244
|
*
|
|
370
245
|
* The target universe is the declared graph's sitemap-eligible pages: the globs
|
|
371
|
-
* in `
|
|
246
|
+
* in `pagegraph.config.ts` are written against declared paths, and the crawl is what
|
|
372
247
|
* supplies the served edges. Pass the crawl's `internalEdges` (each edge carries
|
|
373
248
|
* its `region`).
|
|
374
249
|
*/
|
|
@@ -457,5 +332,5 @@ declare const renderedCheckTargets: (graph: SeoGraph) => Array<string>;
|
|
|
457
332
|
*/
|
|
458
333
|
declare function checkRenderedContent(graph: SeoGraph, documents: ReadonlyArray<RenderedDocument>, policy?: ContentPolicy): Array<Violation>;
|
|
459
334
|
//#endregion
|
|
460
|
-
export {
|
|
461
|
-
//# sourceMappingURL=content-
|
|
335
|
+
export { RenderedEdgeFailure as A, FreshnessEntry as B, Anchor as C, RENDERED_EDGE_ARTIFACT_SCHEMA_VERSION as D, LinkGraphDiff as E, decodeRenderedEdgeArtifact as F, FreshnessReport as H, diffLinkGraph as I, extractAnchors as L, RenderedPage as M, SimpleEdge as N, RenderedEdgeArtifact as O, buildRenderedGraph as P, normalizePath as R, hasStructuralViolations as S, LinkEdge as T, freshnessReport as U, FreshnessPolicy as V, Violation as _, Heading as a, checkGraph as b, RenderedDocument as c, checkRenderedContent as d, extractPageContent as f, Severity as g, CoverageRule as h, ContentRuleName as i, RenderedGraph as j, RenderedEdgeArtifactCrawl as k, WordCountRule as l, CheckGraphOptions as m, ContentFinding as n, JsonLdNode as o, renderedCheckTargets as p, ContentPolicy as r, PageContent as s, ContentCheckOptions as t, checkPageContent as u, checkCoverage as v, AnchorRegion as w, checkRenderedCoverage as x, checkFreshness as y, renderedGraphFromEdges as z };
|
|
336
|
+
//# sourceMappingURL=content-DgNr7apS.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/core/declare.ts
|
|
2
|
+
/** Apply a {@link RouteSeo.titleTemplate}; no template means the title is already full. */
|
|
3
|
+
function applyTitleTemplate(template, title) {
|
|
4
|
+
return template === void 0 ? title : template.replaceAll("%s", () => title);
|
|
5
|
+
}
|
|
6
|
+
//#endregion
|
|
7
|
+
export { applyTitleTemplate as t };
|
|
8
|
+
|
|
9
|
+
//# sourceMappingURL=declare-D5o7hwmv.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"declare-D5o7hwmv.js","names":[],"sources":["../src/core/declare.ts"],"sourcesContent":["/**\n * The declaration surface: what a route says about itself in\n * `staticData.seo`. Everything else in this package derives from it.\n *\n * Consumers supply valid route paths, page kinds, and authored fact ids by\n * augmenting {@link Register}, the same pattern TanStack Router uses for its own\n * typed registry. Unaugmented, these fall back to `string`, so the package\n * compiles and behaves on its own.\n */\n\nimport type { AnyRouteMatch } from \"@tanstack/react-router\";\nimport type { Graph, Thing, WithContext } from \"schema-dts\";\n\n/**\n * Consumer-augmented registry.\n *\n * ```ts\n * declare module \"pagegraph\" {\n * interface Register {\n * paths: FileRouteTypes[\"fullPaths\"];\n * kinds: \"page\" | \"article\" | \"hub\";\n * facts: typeof facts;\n * }\n * }\n * ```\n */\nexport interface Register {}\n\n/** Every public full path in the consumer's route tree (type-only; erased at runtime). */\nexport type PublicPath = Register extends { paths: infer P extends string } ? P : string;\n\n/** The consumer's page taxonomy. A label: nothing in the core branches on it. */\nexport type SeoKind = Register extends { kinds: infer K extends string } ? K : string;\n\nexport interface SitemapPolicy {\n priority: number;\n changeFrequency: \"weekly\" | \"monthly\";\n}\n\n/** A question and answer the page shows and marks up as FAQPage. */\nexport interface SeoFaq {\n question: string;\n answer: string;\n category?: string | undefined;\n isHighlighted?: boolean | undefined;\n}\n\n/** A top-level JSON-LD entity or graph ready to serialize into a script element. */\nexport type JsonLdDocument = WithContext<Thing> | Graph;\n\n/**\n * The document head a page ships. A static route declares it once in\n * `staticData.seo.head` and renders it with `seo.head` from `pagegraph/react`;\n * the graph, `pageHeads`, and the checks read the same title, description, and\n * FAQs. A dynamic route passes it to `seoHead(ctx, head)` from its `head()`.\n */\nexport interface SeoPageHead {\n /**\n * The page title. A route that declares {@link RouteSeo.titleTemplate} gets\n * it applied; otherwise this is the full `<title>`.\n */\n title: string;\n description: string;\n ogTitle?: string | undefined;\n ogDescription?: string | undefined;\n /** Override of the route's robots policy. */\n robots?: string | undefined;\n article?:\n | {\n publishedAt: string;\n modifiedAt?: string | undefined;\n author?: { name: string; url?: string | undefined } | undefined;\n image?: string | undefined;\n tags?: ReadonlyArray<string> | undefined;\n }\n | undefined;\n faqs?: ReadonlyArray<SeoFaq> | undefined;\n service?: { name: string; description?: string | undefined; serviceType: string } | undefined;\n /** Standalone keywords meta for non-article pages; article pages derive it from tags. */\n keywords?: ReadonlyArray<string> | undefined;\n /** Visible canonical pages rendered as an ordered collection on this page. */\n itemList?: { name: string; items: ReadonlyArray<{ name: string; url: string }> } | undefined;\n /** Additional schema.org documents for this route. */\n jsonLd?: ReadonlyArray<JsonLdDocument> | undefined;\n /**\n * Explicit canonical path (used for canonical + og:url instead of the match pathname).\n * For routes whose canonical is computed independently of the URL, e.g. a docs splat\n * that derives it from the resolved slug segments in its loader.\n */\n canonicalPath?: string | undefined;\n /**\n * Explicit breadcrumb trail for the BreadcrumbList JSON-LD, overriding the match-chain\n * trail. For routes whose hierarchy lives outside the route tree. Each `path` is a URL path.\n */\n breadcrumbs?: ReadonlyArray<{ name: string; path: string }> | undefined;\n}\n\nexport interface RouteSeo {\n kind: SeoKind;\n /** Breadcrumb label; fn form reads the match (e.g. loaderData frontmatter title). */\n crumb?: string | ((match: AnyRouteMatch) => string) | undefined;\n /** false = excluded from sitemap. Absent on non-indexable kinds is fine. */\n sitemap?: SitemapPolicy | false | undefined;\n /** meta robots value, e.g. \"noindex, follow\" on a hub page. */\n robots?: string | undefined;\n /** Typed cross-link edges. */\n related?: ReadonlyArray<PublicPath> | undefined;\n /** How OTHER pages render a card for this page. */\n link?: { title: string; description: string } | undefined;\n /** This route is a redirect/alias. */\n redirectTo?: PublicPath | undefined;\n /**\n * When this page's content last changed meaningfully, as an ISO 8601 date.\n * Feeds the sitemap `<lastmod>` and the freshness report. Content instances\n * carry their own dates instead; a param route's value is never inherited.\n */\n modifiedAt?: string | undefined;\n /** A static page's head, rendered by `seo.head`; the graph and `pageHeads` read it. */\n head?: SeoPageHead | undefined;\n /**\n * Full-title template for pages rendered through this route, with `%s` for the\n * page's own title (e.g. `\"%s | Example Blog\"`). `seoHead` applies it to the\n * title a dynamic route passes, and the graph applies it to the instances a\n * collection routes through it, so the suffix lives in one place.\n */\n titleTemplate?: string | undefined;\n}\n\n/** Apply a {@link RouteSeo.titleTemplate}; no template means the title is already full. */\nexport function applyTitleTemplate(template: string | undefined, title: string): string {\n // A replacer function keeps `$&` and friends in the title literal.\n return template === undefined ? title : template.replaceAll(\"%s\", () => title);\n}\n\ndeclare module \"@tanstack/react-router\" {\n interface StaticDataRouteOption {\n seo?: RouteSeo | undefined;\n /** How this route supplies its authored document. */\n markdown?: \"rendered\" | \"source\" | undefined;\n /** Group label for this page in llms.txt. */\n llms?: string | undefined;\n }\n}\n"],"mappings":";;AAiIA,SAAgB,mBAAmB,UAA8B,OAAuB;CAEtF,OAAO,aAAa,KAAA,IAAY,QAAQ,SAAS,WAAW,YAAY,KAAK;AAC/E"}
|