pagegraph 0.6.0 → 0.7.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 +226 -5
- package/dist/audit.js +1 -1
- package/dist/checks-6noG_xTS.d.ts +313 -0
- package/dist/cli.js +1980 -609
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/index.d.ts +2 -75
- package/dist/index.js +47 -13
- package/dist/index.js.map +1 -1
- package/dist/{links-sGbLkl-7.js → links-BFJc-pJ9.js} +152 -18
- package/dist/links-BFJc-pJ9.js.map +1 -0
- package/package.json +1 -1
- package/dist/checks-BfsQtKga.d.ts +0 -163
- package/dist/links-sGbLkl-7.js.map +0 -1
|
@@ -103,6 +103,30 @@ const breadthFirstDepth = (edges, root) => {
|
|
|
103
103
|
return depth;
|
|
104
104
|
};
|
|
105
105
|
/**
|
|
106
|
+
* Assemble a {@link RenderedGraph} from page/edge sets. Edges are already
|
|
107
|
+
* same-origin and path-keyed by the callers, so this is the shared, pure tail of
|
|
108
|
+
* {@link buildRenderedGraph} and {@link renderedGraphFromEdges}.
|
|
109
|
+
*/
|
|
110
|
+
const graphFromEdges = (nodes, edges, root) => {
|
|
111
|
+
const rootKey = root.replace(/\/+$/, "") || "/";
|
|
112
|
+
const internalEdges = edges;
|
|
113
|
+
const contextualEdges = edges.filter((edge) => edge.region === "body");
|
|
114
|
+
const incoming = new Set(internalEdges.map((edge) => edge.to));
|
|
115
|
+
const orphans = [...nodes].filter((path) => path !== rootKey && !incoming.has(path)).sort();
|
|
116
|
+
const depthByPath = breadthFirstDepth(internalEdges, rootKey);
|
|
117
|
+
let maxDepth = null;
|
|
118
|
+
for (const value of depthByPath.values()) if (maxDepth === null || value > maxDepth) maxDepth = value;
|
|
119
|
+
return {
|
|
120
|
+
nodes: [...nodes],
|
|
121
|
+
edges,
|
|
122
|
+
internalEdges,
|
|
123
|
+
contextualEdges,
|
|
124
|
+
orphans,
|
|
125
|
+
depthByPath,
|
|
126
|
+
maxDepth
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
106
130
|
* Build the rendered graph from crawled pages. `origin` fixes the graph's
|
|
107
131
|
* same-origin boundary — pages and anchors on another origin are dropped, not
|
|
108
132
|
* silently folded in — and `root` is the normalized path the depth BFS starts
|
|
@@ -111,7 +135,6 @@ const breadthFirstDepth = (edges, root) => {
|
|
|
111
135
|
*/
|
|
112
136
|
const buildRenderedGraph = (origin, pages, root = "/") => {
|
|
113
137
|
const graphOrigin = new URL(origin).origin;
|
|
114
|
-
const rootKey = root.replace(/\/+$/, "") || "/";
|
|
115
138
|
const edges = [];
|
|
116
139
|
const nodes = /* @__PURE__ */ new Set();
|
|
117
140
|
for (const page of pages) {
|
|
@@ -141,21 +164,39 @@ const buildRenderedGraph = (origin, pages, root = "/") => {
|
|
|
141
164
|
});
|
|
142
165
|
}
|
|
143
166
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
167
|
+
return graphFromEdges(nodes, edges, root);
|
|
168
|
+
};
|
|
169
|
+
/** A URL or an already-normalized graph path key both collapse to the path key. */
|
|
170
|
+
const pathKeyOf = (value) => {
|
|
171
|
+
try {
|
|
172
|
+
return normalizePath(new URL(value));
|
|
173
|
+
} catch {
|
|
174
|
+
return (value.split(/[?#]/, 1)[0] ?? "").replace(/\/+$/, "") || "/";
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Rebuild a {@link RenderedGraph} from a rendered-edge set alone — the artifact
|
|
179
|
+
* `pagegraph links verify --emit-rendered` writes. Path keys are normalized the
|
|
180
|
+
* same way as a crawl, and `nodes` carries the rendered page paths the artifact
|
|
181
|
+
* recorded, so an edge-less page (a redirect target with no internal links) stays
|
|
182
|
+
* a node and its orphan/depth report survives replay. Without `nodes` the node set
|
|
183
|
+
* falls back to the edge endpoints.
|
|
184
|
+
*/
|
|
185
|
+
const renderedGraphFromEdges = (edges, root = "/", nodes = []) => {
|
|
186
|
+
const nodeSet = new Set(nodes.map(pathKeyOf));
|
|
187
|
+
const kept = [];
|
|
188
|
+
for (const edge of edges) {
|
|
189
|
+
const from = pathKeyOf(edge.from);
|
|
190
|
+
const to = pathKeyOf(edge.to);
|
|
191
|
+
nodeSet.add(from);
|
|
192
|
+
nodeSet.add(to);
|
|
193
|
+
kept.push({
|
|
194
|
+
from,
|
|
195
|
+
to,
|
|
196
|
+
region: edge.region
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return graphFromEdges(nodeSet, kept, root);
|
|
159
200
|
};
|
|
160
201
|
const edgeKey = (edge) => `${edge.from}\u0000${edge.to}`;
|
|
161
202
|
const normalizeEdge = (edge) => ({
|
|
@@ -178,7 +219,100 @@ const diffLinkGraph = (declared, rendered) => {
|
|
|
178
219
|
renderedContextualCount: rendered.contextualEdges.length
|
|
179
220
|
};
|
|
180
221
|
};
|
|
222
|
+
/** The rendered-edge artifact schema this package writes and accepts. */
|
|
223
|
+
const RENDERED_EDGE_ARTIFACT_SCHEMA_VERSION = 1;
|
|
224
|
+
const ARTIFACT_KIND = "links-rendered";
|
|
225
|
+
const REGIONS = /* @__PURE__ */ new Set([
|
|
226
|
+
"nav",
|
|
227
|
+
"footer",
|
|
228
|
+
"header",
|
|
229
|
+
"body"
|
|
230
|
+
]);
|
|
231
|
+
const decodeArtifactEdges = (input) => {
|
|
232
|
+
if (!Array.isArray(input)) throw new Error("`edges` must be an array of { from, to, region } edges");
|
|
233
|
+
return input.map((item) => {
|
|
234
|
+
if (item === null || typeof item !== "object") throw new Error("each artifact edge must be an object with string `from` and `to`");
|
|
235
|
+
const { from, to, region } = item;
|
|
236
|
+
if (typeof from !== "string" || typeof to !== "string") throw new Error("each artifact edge must have string `from` and `to`");
|
|
237
|
+
if (region !== void 0 && !REGIONS.has(region)) throw new Error(`artifact edge region must be one of nav, footer, header, body (received ${JSON.stringify(region)})`);
|
|
238
|
+
return {
|
|
239
|
+
from,
|
|
240
|
+
to,
|
|
241
|
+
region: region ?? "body"
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
};
|
|
245
|
+
const decodeArtifactNodes = (input) => {
|
|
246
|
+
if (input === void 0) return [];
|
|
247
|
+
if (!Array.isArray(input) || !input.every((path) => typeof path === "string")) throw new Error("rendered-edge artifact `nodes` must be an array of paths");
|
|
248
|
+
return input;
|
|
249
|
+
};
|
|
250
|
+
const decodeArtifactFailures = (input) => {
|
|
251
|
+
if (input === void 0) return [];
|
|
252
|
+
if (!Array.isArray(input)) throw new Error("rendered-edge artifact `crawl.failures` must be an array");
|
|
253
|
+
return input.map((item) => {
|
|
254
|
+
if (item === null || typeof item !== "object") throw new Error("each artifact failure must be an object with string `url` and `error`");
|
|
255
|
+
const { url, error } = item;
|
|
256
|
+
if (typeof url !== "string" || typeof error !== "string") throw new Error("each artifact failure must have string `url` and `error`");
|
|
257
|
+
return {
|
|
258
|
+
url,
|
|
259
|
+
error
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
};
|
|
263
|
+
const requiredAbsoluteUrl = (value, field) => {
|
|
264
|
+
if (typeof value !== "string" || value === "") throw new Error(`rendered-edge artifact must carry a non-empty string \`${field}\``);
|
|
265
|
+
try {
|
|
266
|
+
new URL(value);
|
|
267
|
+
} catch {
|
|
268
|
+
throw new Error(`rendered-edge artifact \`${field}\` must be an absolute URL (received ${JSON.stringify(value)})`);
|
|
269
|
+
}
|
|
270
|
+
return value;
|
|
271
|
+
};
|
|
272
|
+
const requiredInteger = (value, field, minimum) => {
|
|
273
|
+
if (!Number.isSafeInteger(value) || value < minimum) throw new Error(`rendered-edge artifact \`${field}\` must be an integer >= ${minimum} (received ${JSON.stringify(value)})`);
|
|
274
|
+
return value;
|
|
275
|
+
};
|
|
276
|
+
/**
|
|
277
|
+
* Decode a rendered-edge artifact from user input. Throws on any other shape —
|
|
278
|
+
* this is user input, not a provider payload. `root`, `bodyTruncated`,
|
|
279
|
+
* `truncatedPages`, `failures`, and `nodes` are optional so a hand-written edge
|
|
280
|
+
* dump that only carries provenance can still be replayed.
|
|
281
|
+
*/
|
|
282
|
+
const decodeRenderedEdgeArtifact = (input) => {
|
|
283
|
+
if (input === null || typeof input !== "object") throw new Error("rendered-edge artifact must be an object");
|
|
284
|
+
const value = input;
|
|
285
|
+
if (value["kind"] !== ARTIFACT_KIND) throw new Error(`rendered-edge artifact \`kind\` must be "${ARTIFACT_KIND}" (received ${JSON.stringify(value["kind"])})`);
|
|
286
|
+
if (value["schemaVersion"] !== 1) throw new Error(`unsupported rendered-edge artifact schemaVersion: ${JSON.stringify(value["schemaVersion"])}`);
|
|
287
|
+
const origin = requiredAbsoluteUrl(value["origin"], "origin");
|
|
288
|
+
const seed = requiredAbsoluteUrl(value["seed"], "seed");
|
|
289
|
+
const crawl = value["crawl"];
|
|
290
|
+
if (crawl === null || typeof crawl !== "object") throw new Error("rendered-edge artifact must carry a `crawl` object");
|
|
291
|
+
const provenance = crawl;
|
|
292
|
+
const truncatedPages = provenance["truncatedPages"];
|
|
293
|
+
if (truncatedPages !== void 0 && (!Array.isArray(truncatedPages) || !truncatedPages.every((url) => typeof url === "string"))) throw new Error("rendered-edge artifact `truncatedPages` must be an array of URLs");
|
|
294
|
+
const bodyTruncated = provenance["bodyTruncated"];
|
|
295
|
+
if (bodyTruncated !== void 0 && typeof bodyTruncated !== "boolean") throw new Error("rendered-edge artifact `bodyTruncated` must be a boolean");
|
|
296
|
+
if (typeof provenance["truncated"] !== "boolean") throw new Error("rendered-edge artifact `crawl.truncated` must be a boolean");
|
|
297
|
+
return {
|
|
298
|
+
kind: ARTIFACT_KIND,
|
|
299
|
+
schemaVersion: 1,
|
|
300
|
+
origin,
|
|
301
|
+
seed,
|
|
302
|
+
crawl: {
|
|
303
|
+
pages: requiredInteger(provenance["pages"], "crawl.pages", 0),
|
|
304
|
+
root: typeof provenance["root"] === "string" ? provenance["root"] : pathKeyOf(seed),
|
|
305
|
+
limit: requiredInteger(provenance["limit"], "crawl.limit", 1),
|
|
306
|
+
truncated: provenance["truncated"],
|
|
307
|
+
bodyTruncated: bodyTruncated === true,
|
|
308
|
+
truncatedPages: truncatedPages ?? [],
|
|
309
|
+
failures: decodeArtifactFailures(provenance["failures"])
|
|
310
|
+
},
|
|
311
|
+
nodes: decodeArtifactNodes(value["nodes"]),
|
|
312
|
+
edges: decodeArtifactEdges(value["edges"])
|
|
313
|
+
};
|
|
314
|
+
};
|
|
181
315
|
//#endregion
|
|
182
|
-
export {
|
|
316
|
+
export { extractAnchors as a, diffLinkGraph as i, buildRenderedGraph as n, normalizePath as o, decodeRenderedEdgeArtifact as r, renderedGraphFromEdges as s, RENDERED_EDGE_ARTIFACT_SCHEMA_VERSION as t };
|
|
183
317
|
|
|
184
|
-
//# sourceMappingURL=links-
|
|
318
|
+
//# sourceMappingURL=links-BFJc-pJ9.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"links-BFJc-pJ9.js","names":[],"sources":["../src/core/links.ts"],"sourcesContent":["/**\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}\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 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 },\n nodes: decodeArtifactNodes(value[\"nodes\"]),\n edges: decodeArtifactEdges(value[\"edges\"]),\n };\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pagegraph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Route-declared SEO graph and audit toolkit for TanStack Start: sitemap/robots, React head, JSON-LD, Vite coverage gate, live audit, and Jev-backed link decisions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"seo",
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { AnyRoute, AnyRouteMatch } from "@tanstack/react-router";
|
|
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
|
-
declare module "@tanstack/react-router" {
|
|
47
|
-
interface StaticDataRouteOption {
|
|
48
|
-
seo?: RouteSeo | undefined;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
//#endregion
|
|
52
|
-
//#region src/core/graph.d.ts
|
|
53
|
-
/**
|
|
54
|
-
* Where a node came from: `"route"` for a structural route declaration, or the
|
|
55
|
-
* `source` label of the collection that produced the instance (e.g. "blog").
|
|
56
|
-
*/
|
|
57
|
-
type SeoSource = string;
|
|
58
|
-
interface SeoNode {
|
|
59
|
-
/** Canonical path, no origin (e.g. "/pricing", "/blog/my-post"). */
|
|
60
|
-
path: string;
|
|
61
|
-
kind: SeoKind;
|
|
62
|
-
source: SeoSource;
|
|
63
|
-
/** Route-declared policy, or synthesized (kind + inherited sitemap) for instances. */
|
|
64
|
-
policy: RouteSeo;
|
|
65
|
-
instance?: {
|
|
66
|
-
title: string;
|
|
67
|
-
description?: string | undefined;
|
|
68
|
-
publishedAt?: string | undefined;
|
|
69
|
-
modifiedAt?: string | undefined;
|
|
70
|
-
} | undefined;
|
|
71
|
-
}
|
|
72
|
-
type SeoEdgeType = "crumb-parent" | "related" | "redirect" | "collection-member";
|
|
73
|
-
interface SeoEdge {
|
|
74
|
-
from: string;
|
|
75
|
-
to: string;
|
|
76
|
-
type: SeoEdgeType;
|
|
77
|
-
}
|
|
78
|
-
interface SeoGraph {
|
|
79
|
-
nodes: Map<string, SeoNode>;
|
|
80
|
-
edges: Array<SeoEdge>;
|
|
81
|
-
/** Exact-path ownership conflicts encountered while assembling graph sources. */
|
|
82
|
-
collisions?: ReadonlyArray<{
|
|
83
|
-
path: string;
|
|
84
|
-
sources: ReadonlyArray<SeoSource>;
|
|
85
|
-
}> | undefined;
|
|
86
|
-
}
|
|
87
|
-
/** One concrete page produced by a collection. */
|
|
88
|
-
interface SeoInstance {
|
|
89
|
-
/** Canonical path of the page (e.g. "/blog/my-post"). */
|
|
90
|
-
readonly path: string;
|
|
91
|
-
readonly title: string;
|
|
92
|
-
readonly description?: string | undefined;
|
|
93
|
-
readonly publishedAt?: string | undefined;
|
|
94
|
-
readonly modifiedAt?: string | undefined;
|
|
95
|
-
}
|
|
96
|
-
interface SeoCollection {
|
|
97
|
-
/**
|
|
98
|
-
* The param route these instances render through (e.g. "/blog/$slug"). Instances
|
|
99
|
-
* inherit this route's declared policy (kind + sitemap) — the graph reads it from
|
|
100
|
-
* the structural node, so declarations stay the single source of truth.
|
|
101
|
-
*/
|
|
102
|
-
readonly route: PublicPath;
|
|
103
|
-
/** The `source` stamped on every node this collection produces (e.g. "blog"). */
|
|
104
|
-
readonly source: SeoSource;
|
|
105
|
-
readonly instances: ReadonlyArray<SeoInstance>;
|
|
106
|
-
/** Edges the collection declares between its instances and the rest of the graph. */
|
|
107
|
-
readonly edges?: ReadonlyArray<SeoEdge> | undefined;
|
|
108
|
-
}
|
|
109
|
-
interface BuildSeoGraphInput {
|
|
110
|
-
readonly routeTree: AnyRoute;
|
|
111
|
-
readonly collections?: ReadonlyArray<SeoCollection> | undefined;
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Build the SEO graph from route declarations and content collections.
|
|
115
|
-
*
|
|
116
|
-
* Synchronous: the caller materializes its collections before calling, so there is
|
|
117
|
-
* no async work here. Callers own the origin.
|
|
118
|
-
*/
|
|
119
|
-
declare function buildSeoGraph(input: BuildSeoGraphInput): SeoGraph;
|
|
120
|
-
//#endregion
|
|
121
|
-
//#region src/core/checks.d.ts
|
|
122
|
-
type Severity = "structural" | "editorial";
|
|
123
|
-
interface Violation {
|
|
124
|
-
severity: Severity;
|
|
125
|
-
rule: string;
|
|
126
|
-
path?: string | undefined;
|
|
127
|
-
message: string;
|
|
128
|
-
fix?: string | undefined;
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* A contextual-link coverage rule: every sitemap-eligible page matching the
|
|
132
|
-
* `path` glob must have at least `minInbound` incoming `related` edges. Unlike
|
|
133
|
-
* the static rules, these come from project policy (a CLI flag or
|
|
134
|
-
* `seo.config.ts`), because "which pages are money pages" is app knowledge.
|
|
135
|
-
*/
|
|
136
|
-
interface CoverageRule {
|
|
137
|
-
/** Path glob: `*` matches within a segment, `**` matches across segments. */
|
|
138
|
-
readonly path: string;
|
|
139
|
-
/** Minimum incoming contextual (`related`) edges the matched page needs. */
|
|
140
|
-
readonly minInbound: number;
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Run every static rule against the graph, then any caller-supplied
|
|
144
|
-
* {@link CoverageRule}s, and return the flat list of violations. With no
|
|
145
|
-
* `coverage` option the result is exactly the static rule set.
|
|
146
|
-
*/
|
|
147
|
-
declare function checkGraph(graph: SeoGraph, options?: {
|
|
148
|
-
readonly coverage?: ReadonlyArray<CoverageRule> | undefined;
|
|
149
|
-
}): Array<Violation>;
|
|
150
|
-
/**
|
|
151
|
-
* Enforce contextual-link coverage rules: a named set of sitemap-eligible
|
|
152
|
-
* "money" pages each needs `minInbound` incoming `related` edges. Only
|
|
153
|
-
* `related` edges count — breadcrumb ancestry is navigation, not context.
|
|
154
|
-
*
|
|
155
|
-
* A rule that matches no sitemap-eligible page is itself a violation: a typo or
|
|
156
|
-
* a rule aimed at a noindex page would otherwise pass silently forever.
|
|
157
|
-
*/
|
|
158
|
-
declare function checkCoverage(graph: SeoGraph, rules: ReadonlyArray<CoverageRule>): Array<Violation>;
|
|
159
|
-
/** Structural violations fail `pagegraph check`; editorial-only stays green. */
|
|
160
|
-
declare const hasStructuralViolations: (violations: ReadonlyArray<Violation>) => boolean;
|
|
161
|
-
//#endregion
|
|
162
|
-
export { Register as _, checkGraph as a, SitemapPolicy as b, SeoCollection as c, SeoGraph as d, SeoInstance as f, PublicPath as g, buildSeoGraph as h, checkCoverage as i, SeoEdge as l, SeoSource as m, Severity as n, hasStructuralViolations as o, SeoNode as p, Violation as r, BuildSeoGraphInput as s, CoverageRule as t, SeoEdgeType as u, RouteSeo as v, SeoKind as y };
|
|
163
|
-
//# sourceMappingURL=checks-BfsQtKga.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"links-sGbLkl-7.js","names":[],"sources":["../src/core/links.ts"],"sourcesContent":["/**\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 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 * 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 rootKey = root.replace(/\\/+$/, \"\") || \"/\";\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 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]\n .filter((path) => path !== rootKey && !incoming.has(path))\n .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 { edges, internalEdges, contextualEdges, orphans, depthByPath, maxDepth };\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"],"mappings":"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"}
|