pagegraph 0.8.3 → 0.9.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 +84 -7
- package/dist/audit.d.ts +6 -0
- package/dist/audit.js +7 -3
- package/dist/audit.js.map +1 -1
- package/dist/{checks-6noG_xTS.d.ts → checks-_X8yjMvV.d.ts} +11 -1
- package/dist/cli.js +1556 -409
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +3 -1
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +28 -21
- package/dist/index.js.map +1 -1
- package/dist/{links-BFJc-pJ9.js → links-DAa8KVzg.js} +16 -2
- package/dist/links-DAa8KVzg.js.map +1 -0
- package/package.json +1 -1
- package/dist/links-BFJc-pJ9.js.map +0 -1
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { f as SeoGraph, t as CoverageRule } from "./checks-
|
|
1
|
+
import { f as SeoGraph, t as CoverageRule } from "./checks-_X8yjMvV.js";
|
|
2
2
|
import { PluginOption } from "vite";
|
|
3
3
|
//#region src/config/vite-graph-loader.d.ts
|
|
4
4
|
/** A graph, plus the release of whatever producing it acquired. */
|
|
@@ -49,6 +49,8 @@ interface SeoWorkflowOpenCodeConfig {
|
|
|
49
49
|
readonly defaultModel: string;
|
|
50
50
|
/** Per-workflow `provider/model` overrides. */
|
|
51
51
|
readonly models?: Readonly<Record<string, string>> | undefined;
|
|
52
|
+
/** Per-completion deadline in milliseconds, including JSON repair; defaults to 180000. */
|
|
53
|
+
readonly timeoutMs?: number | undefined;
|
|
52
54
|
}
|
|
53
55
|
interface SeoWorkflowContextConfig {
|
|
54
56
|
/** Durable project context included in every workflow prompt. */
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","names":[],"sources":["../src/config/vite-graph-loader.ts","../src/config/index.ts"],"sourcesContent":["/**\n * A graph loader that evaluates the host app's own graph module inside a\n * headless Vite server.\n *\n * The SEO graph is built *by the app*: its module reads the generated route\n * tree and whatever content collections it owns, so it pulls in the app's real\n * module graph — path aliases, virtual modules from content plugins, bare\n * imports that only exist inside a server runtime. A CLI cannot just `import()`\n * that from Node or Bun.\n *\n * So this boots a scoped, in-process Vite server (middleware mode, no HMR, no\n * watcher, no HTTP listener), `ssrLoadModule`s the app's graph module, and calls\n * its exported loader. The graph comes back as a live in-process object — there\n * is no serialization boundary for the commands to cross — and the server is\n * closed by {@link LoadedSeoGraph.dispose}.\n *\n * Everything host-shaped is an option: the app supplies its own {@link\n * ViteGraphLoaderOptions.plugins} (a content plugin, an MDX loader), {@link\n * ViteGraphLoaderOptions.stubs} for bare modules that only resolve inside its\n * server runtime, and {@link ViteGraphLoaderOptions.env} for variables its\n * modules parse at import. This file knows about none of them.\n */\n\nimport { createServer, type InlineConfig, type PluginOption } from \"vite\";\n\nimport type { SeoGraph } from \"../core/graph\";\n\n/** A graph, plus the release of whatever producing it acquired. */\nexport interface LoadedSeoGraph {\n readonly graph: SeoGraph;\n /** Called once the command is done with the graph, on success or failure. */\n readonly dispose: () => Promise<void>;\n}\n\n/**\n * Produces the SEO graph for the `pagegraph` CLI. A plain promise on purpose: a\n * config file must be writable without Effect, which the CLI keeps behind its\n * `bin` (an optional peer dependency).\n */\nexport type SeoGraphLoader = () => Promise<LoadedSeoGraph>;\n\nexport interface ViteGraphLoaderOptions {\n /** The app's Vite root — the directory its aliases and plugins resolve against. */\n readonly root: string;\n /** Module exporting the graph loader, root-relative (e.g. `/lib/seo/graph.ts`). */\n readonly entry: string;\n /** Named export on `entry` returning `Promise<SeoGraph>`. Defaults to `loadSeoGraph`. */\n readonly exportName?: string | undefined;\n /** Vite plugins the app's module graph needs — a content/MDX plugin, say. */\n readonly plugins?: ReadonlyArray<PluginOption> | undefined;\n /**\n * Bare or virtual module ids to replace with inert source, so a module that\n * only resolves inside the app's server runtime does not break the load.\n */\n readonly stubs?: Readonly<Record<string, string>> | undefined;\n /**\n * Variables to seed on `process.env` before the app's modules parse it. Vite\n * exposes prefixed values through `import.meta.env`, so this is how a module\n * that demands an env var at import time is satisfied. Existing values win —\n * a real `.env` is never clobbered.\n */\n readonly env?: Readonly<Record<string, string>> | undefined;\n}\n\nconst messageOf = (cause: unknown): string =>\n cause instanceof Error ? cause.message : String(cause);\n\n/**\n * Vite and its plugins write progress to stdout. The CLI reserves stdout for the\n * data plane, so redirect it to stderr for the duration of the load.\n *\n * Both the stream method and `console.log`/`info` are swapped: under Bun,\n * `console.log` writes to fd 1 natively rather than going through\n * `process.stdout.write`, so the stream swap alone would miss a plugin that\n * logs through the console.\n */\nconst withCleanStdout = async <A>(run: () => Promise<A>): Promise<A> => {\n const originalWrite = process.stdout.write.bind(process.stdout);\n const originalLog = console.log;\n const originalInfo = console.info;\n process.stdout.write = process.stderr.write.bind(process.stderr) as typeof process.stdout.write;\n console.log = (...args: Array<unknown>) => console.error(...args);\n console.info = (...args: Array<unknown>) => console.error(...args);\n try {\n return await run();\n } finally {\n process.stdout.write = originalWrite;\n console.log = originalLog;\n console.info = originalInfo;\n }\n};\n\n/** Turn `{ id: source }` into a Vite plugin that resolves and loads each id. */\nconst stubPlugin = (stubs: Readonly<Record<string, string>>): PluginOption => {\n const virtualIdOf = (id: string) => `\\0seo-cli-stub:${id}`;\n const sourceByVirtualId = new Map(\n Object.entries(stubs).map(([id, source]) => [virtualIdOf(id), source]),\n );\n return {\n name: \"seo:stubs\",\n resolveId: (id) => (id in stubs ? virtualIdOf(id) : null),\n load: (id) => sourceByVirtualId.get(id) ?? null,\n };\n};\n\nconst seedEnv = (env: Readonly<Record<string, string>>): void => {\n for (const [key, value] of Object.entries(env)) {\n // oxlint-disable-next-line node/no-process-env -- CLI boundary: seeds the raw env BEFORE the app's own env module parses it; that module is the consumer here, not an option\n process.env[key] ??= value;\n }\n};\n\nconst inlineConfigFor = (options: ViteGraphLoaderOptions): InlineConfig => ({\n configFile: false,\n root: options.root,\n mode: \"production\",\n logLevel: \"error\",\n appType: \"custom\",\n clearScreen: false,\n server: { middlewareMode: true, hmr: false, watch: null },\n resolve: { tsconfigPaths: true },\n plugins: [\n ...(options.stubs === undefined ? [] : [stubPlugin(options.stubs)]),\n ...(options.plugins ?? []),\n ],\n});\n\n/**\n * Build a {@link SeoGraphLoader} that evaluates `entry` in a headless Vite\n * server rooted at `root`, and returns what its `exportName` export resolves to.\n */\nexport const viteGraphLoader =\n (options: ViteGraphLoaderOptions): SeoGraphLoader =>\n async () => {\n const exportName = options.exportName ?? \"loadSeoGraph\";\n if (options.env) seedEnv(options.env);\n\n const server = await withCleanStdout(() => createServer(inlineConfigFor(options))).catch(\n (cause: unknown) => {\n throw new Error(`Could not start the Vite loader: ${messageOf(cause)}`);\n },\n );\n\n // The server is a resource from here on: a failed load must still close it,\n // and a successful one hands the close to the caller as `dispose`.\n const graph = await withCleanStdout(async () => {\n const module = (await server.ssrLoadModule(options.entry)) as Record<string, unknown>;\n const load = module[exportName];\n if (typeof load !== \"function\") {\n throw new Error(\n `${options.entry} has no \\`${exportName}\\` export (found: ${Object.keys(module).join(\", \") || \"nothing\"}).`,\n );\n }\n return (await load()) as SeoGraph;\n }).catch(async (cause: unknown) => {\n // Close on the way out, but never let a close failure bury the real\n // error: the reason the graph did not build is the useful one.\n await withCleanStdout(() => server.close()).catch(() => {});\n throw new Error(`Could not build the SEO graph: ${messageOf(cause)}`);\n });\n\n return { graph, dispose: () => withCleanStdout(() => server.close()) };\n };\n","/**\n * `pagegraph/config` — the `pagegraph` CLI's configuration surface.\n *\n * The CLI is a set of pure views over one graph, but *acquiring* that graph is\n * host knowledge: only the app knows where its graph module lives and what its\n * module needs to evaluate. So the app declares it once in a `seo.config.ts` at\n * its root, which the CLI discovers from the working directory:\n *\n * ```ts\n * // seo.config.ts\n * import { defineSeoConfig, viteGraphLoader } from \"pagegraph/config\";\n * import { routeConfig } from \"./lib/route-config\";\n *\n * export default defineSeoConfig({\n * origin: \"https://example.com\",\n * disallow: routeConfig.robotsExclusions,\n * contentSignal: \"search=yes, ai-input=yes, ai-train=yes\",\n * loadGraph: viteGraphLoader({ root: import.meta.dirname, entry: \"/lib/seo/graph.ts\" }),\n * });\n * ```\n *\n * Nothing here imports Effect. The CLI needs it (an optional peer dependency),\n * but a config file must not: it is the app's file, and the app may not be an\n * Effect app.\n */\n\nexport type { LoadedSeoGraph, SeoGraphLoader, ViteGraphLoaderOptions } from \"./vite-graph-loader\";\nexport { viteGraphLoader } from \"./vite-graph-loader\";\n\nimport type { CoverageRule } from \"../core/checks\";\nimport type { SeoGraphLoader } from \"./vite-graph-loader\";\n\nexport interface SeoWorkflowOpenCodeConfig {\n /** Project-owned OpenCode configuration directory, relative to the project root. */\n readonly configDirectory: string;\n /** Default `provider/model` used by agentic workflows. */\n readonly defaultModel: string;\n /** Per-workflow `provider/model` overrides. */\n readonly models?: Readonly<Record<string, string>> | undefined;\n}\n\nexport interface SeoWorkflowContextConfig {\n /** Durable project context included in every workflow prompt. */\n readonly files?: ReadonlyArray<string> | undefined;\n /** Additional context files keyed by workflow id. */\n readonly byWorkflow?: Readonly<Record<string, ReadonlyArray<string>>> | undefined;\n}\n\nexport interface SeoWorkflowConfig {\n readonly opencode: SeoWorkflowOpenCodeConfig;\n readonly context?: SeoWorkflowContextConfig | undefined;\n /** Ignored run-artifact directory, relative to the project root. */\n readonly runsDirectory?: string | undefined;\n}\n\nexport interface SeoCliConfig {\n /**\n * Canonical origin the sitemap and robots projections render under, no\n * trailing slash. The `--origin` flag overrides it per invocation.\n */\n readonly origin: string;\n /**\n * Path prefixes disallowed in robots.txt. Feed it the generated\n * `routeConfig.robotsExclusions` if you run the `pagegraph/vite` plugin.\n */\n readonly disallow: ReadonlyArray<string>;\n /**\n * Origin-wide Content-Signal preferences, forwarded to `renderRobots`.\n * Omit for none. Preview (`--no-indexable`) never emits the line.\n */\n readonly contentSignal?: string | undefined;\n /**\n * Extra robots.txt group lines, forwarded to `renderRobots` as `directives`.\n */\n readonly directives?: ReadonlyArray<string> | undefined;\n /**\n * Last-mile robots.txt override, forwarded to `renderRobots` as `transform`.\n * Runs for indexable and preview hosts.\n */\n readonly transform?: ((robots: string) => string) | undefined;\n /**\n * Contextual-link coverage policy for `pagegraph check`: every sitemap-eligible\n * page matching a rule's `path` glob needs at least `minInbound` incoming\n * `related` edges. A `--require-inbound` flag overrides this per invocation.\n */\n readonly coverage?: ReadonlyArray<CoverageRule> | undefined;\n /** Agentic workflow host, context, and artifact settings. */\n readonly workflows?: SeoWorkflowConfig | undefined;\n /** How the CLI gets the graph. {@link viteGraphLoader} covers the Vite-app case. */\n readonly loadGraph: SeoGraphLoader;\n}\n\n/** Identity — it exists for the type inference and the editor completions. */\nexport const defineSeoConfig = (config: SeoCliConfig): SeoCliConfig => config;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAgEA,MAAM,aAAa,UACjB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;;;;;;;;;;AAWvD,MAAM,kBAAkB,OAAU,QAAsC;CACtE,MAAM,gBAAgB,QAAQ,OAAO,MAAM,KAAK,QAAQ,MAAM;CAC9D,MAAM,cAAc,QAAQ;CAC5B,MAAM,eAAe,QAAQ;CAC7B,QAAQ,OAAO,QAAQ,QAAQ,OAAO,MAAM,KAAK,QAAQ,MAAM;CAC/D,QAAQ,OAAO,GAAG,SAAyB,QAAQ,MAAM,GAAG,IAAI;CAChE,QAAQ,QAAQ,GAAG,SAAyB,QAAQ,MAAM,GAAG,IAAI;CACjE,IAAI;EACF,OAAO,MAAM,IAAI;CACnB,UAAU;EACR,QAAQ,OAAO,QAAQ;EACvB,QAAQ,MAAM;EACd,QAAQ,OAAO;CACjB;AACF;;AAGA,MAAM,cAAc,UAA0D;CAC5E,MAAM,eAAe,OAAe,kBAAkB;CACtD,MAAM,oBAAoB,IAAI,IAC5B,OAAO,QAAQ,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,YAAY,CAAC,YAAY,EAAE,GAAG,MAAM,CAAC,CACvE;CACA,OAAO;EACL,MAAM;EACN,YAAY,OAAQ,MAAM,QAAQ,YAAY,EAAE,IAAI;EACpD,OAAO,OAAO,kBAAkB,IAAI,EAAE,KAAK;CAC7C;AACF;AAEA,MAAM,WAAW,QAAgD;CAC/D,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,GAAG,GAE3C,QAAQ,IAAI,SAAS;AAEzB;AAEA,MAAM,mBAAmB,aAAmD;CAC1E,YAAY;CACZ,MAAM,QAAQ;CACd,MAAM;CACN,UAAU;CACV,SAAS;CACT,aAAa;CACb,QAAQ;EAAE,gBAAgB;EAAM,KAAK;EAAO,OAAO;CAAK;CACxD,SAAS,EAAE,eAAe,KAAK;CAC/B,SAAS,CACP,GAAI,QAAQ,UAAU,KAAA,IAAY,CAAC,IAAI,CAAC,WAAW,QAAQ,KAAK,CAAC,GACjE,GAAI,QAAQ,WAAW,CAAC,CAC1B;AACF;;;;;AAMA,MAAa,mBACV,YACD,YAAY;CACV,MAAM,aAAa,QAAQ,cAAc;CACzC,IAAI,QAAQ,KAAK,QAAQ,QAAQ,GAAG;CAEpC,MAAM,SAAS,MAAM,sBAAsB,aAAa,gBAAgB,OAAO,CAAC,CAAC,CAAC,CAAC,OAChF,UAAmB;EAClB,MAAM,IAAI,MAAM,oCAAoC,UAAU,KAAK,GAAG;CACxE,CACF;CAoBA,OAAO;EAAE,OAAA,MAhBW,gBAAgB,YAAY;GAC9C,MAAM,SAAU,MAAM,OAAO,cAAc,QAAQ,KAAK;GACxD,MAAM,OAAO,OAAO;GACpB,IAAI,OAAO,SAAS,YAClB,MAAM,IAAI,MACR,GAAG,QAAQ,MAAM,YAAY,WAAW,oBAAoB,OAAO,KAAK,MAAM,CAAC,CAAC,KAAK,IAAI,KAAK,UAAU,GAC1G;GAEF,OAAQ,MAAM,KAAK;EACrB,CAAC,CAAC,CAAC,MAAM,OAAO,UAAmB;GAGjC,MAAM,sBAAsB,OAAO,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;GAC1D,MAAM,IAAI,MAAM,kCAAkC,UAAU,KAAK,GAAG;EACtE,CAAC;EAEe,eAAe,sBAAsB,OAAO,MAAM,CAAC;CAAE;AACvE;;;;ACrEF,MAAa,mBAAmB,WAAuC"}
|
|
1
|
+
{"version":3,"file":"config.js","names":[],"sources":["../src/config/vite-graph-loader.ts","../src/config/index.ts"],"sourcesContent":["/**\n * A graph loader that evaluates the host app's own graph module inside a\n * headless Vite server.\n *\n * The SEO graph is built *by the app*: its module reads the generated route\n * tree and whatever content collections it owns, so it pulls in the app's real\n * module graph — path aliases, virtual modules from content plugins, bare\n * imports that only exist inside a server runtime. A CLI cannot just `import()`\n * that from Node or Bun.\n *\n * So this boots a scoped, in-process Vite server (middleware mode, no HMR, no\n * watcher, no HTTP listener), `ssrLoadModule`s the app's graph module, and calls\n * its exported loader. The graph comes back as a live in-process object — there\n * is no serialization boundary for the commands to cross — and the server is\n * closed by {@link LoadedSeoGraph.dispose}.\n *\n * Everything host-shaped is an option: the app supplies its own {@link\n * ViteGraphLoaderOptions.plugins} (a content plugin, an MDX loader), {@link\n * ViteGraphLoaderOptions.stubs} for bare modules that only resolve inside its\n * server runtime, and {@link ViteGraphLoaderOptions.env} for variables its\n * modules parse at import. This file knows about none of them.\n */\n\nimport { createServer, type InlineConfig, type PluginOption } from \"vite\";\n\nimport type { SeoGraph } from \"../core/graph\";\n\n/** A graph, plus the release of whatever producing it acquired. */\nexport interface LoadedSeoGraph {\n readonly graph: SeoGraph;\n /** Called once the command is done with the graph, on success or failure. */\n readonly dispose: () => Promise<void>;\n}\n\n/**\n * Produces the SEO graph for the `pagegraph` CLI. A plain promise on purpose: a\n * config file must be writable without Effect, which the CLI keeps behind its\n * `bin` (an optional peer dependency).\n */\nexport type SeoGraphLoader = () => Promise<LoadedSeoGraph>;\n\nexport interface ViteGraphLoaderOptions {\n /** The app's Vite root — the directory its aliases and plugins resolve against. */\n readonly root: string;\n /** Module exporting the graph loader, root-relative (e.g. `/lib/seo/graph.ts`). */\n readonly entry: string;\n /** Named export on `entry` returning `Promise<SeoGraph>`. Defaults to `loadSeoGraph`. */\n readonly exportName?: string | undefined;\n /** Vite plugins the app's module graph needs — a content/MDX plugin, say. */\n readonly plugins?: ReadonlyArray<PluginOption> | undefined;\n /**\n * Bare or virtual module ids to replace with inert source, so a module that\n * only resolves inside the app's server runtime does not break the load.\n */\n readonly stubs?: Readonly<Record<string, string>> | undefined;\n /**\n * Variables to seed on `process.env` before the app's modules parse it. Vite\n * exposes prefixed values through `import.meta.env`, so this is how a module\n * that demands an env var at import time is satisfied. Existing values win —\n * a real `.env` is never clobbered.\n */\n readonly env?: Readonly<Record<string, string>> | undefined;\n}\n\nconst messageOf = (cause: unknown): string =>\n cause instanceof Error ? cause.message : String(cause);\n\n/**\n * Vite and its plugins write progress to stdout. The CLI reserves stdout for the\n * data plane, so redirect it to stderr for the duration of the load.\n *\n * Both the stream method and `console.log`/`info` are swapped: under Bun,\n * `console.log` writes to fd 1 natively rather than going through\n * `process.stdout.write`, so the stream swap alone would miss a plugin that\n * logs through the console.\n */\nconst withCleanStdout = async <A>(run: () => Promise<A>): Promise<A> => {\n const originalWrite = process.stdout.write.bind(process.stdout);\n const originalLog = console.log;\n const originalInfo = console.info;\n process.stdout.write = process.stderr.write.bind(process.stderr) as typeof process.stdout.write;\n console.log = (...args: Array<unknown>) => console.error(...args);\n console.info = (...args: Array<unknown>) => console.error(...args);\n try {\n return await run();\n } finally {\n process.stdout.write = originalWrite;\n console.log = originalLog;\n console.info = originalInfo;\n }\n};\n\n/** Turn `{ id: source }` into a Vite plugin that resolves and loads each id. */\nconst stubPlugin = (stubs: Readonly<Record<string, string>>): PluginOption => {\n const virtualIdOf = (id: string) => `\\0seo-cli-stub:${id}`;\n const sourceByVirtualId = new Map(\n Object.entries(stubs).map(([id, source]) => [virtualIdOf(id), source]),\n );\n return {\n name: \"seo:stubs\",\n resolveId: (id) => (id in stubs ? virtualIdOf(id) : null),\n load: (id) => sourceByVirtualId.get(id) ?? null,\n };\n};\n\nconst seedEnv = (env: Readonly<Record<string, string>>): void => {\n for (const [key, value] of Object.entries(env)) {\n // oxlint-disable-next-line node/no-process-env -- CLI boundary: seeds the raw env BEFORE the app's own env module parses it; that module is the consumer here, not an option\n process.env[key] ??= value;\n }\n};\n\nconst inlineConfigFor = (options: ViteGraphLoaderOptions): InlineConfig => ({\n configFile: false,\n root: options.root,\n mode: \"production\",\n logLevel: \"error\",\n appType: \"custom\",\n clearScreen: false,\n server: { middlewareMode: true, hmr: false, watch: null },\n resolve: { tsconfigPaths: true },\n plugins: [\n ...(options.stubs === undefined ? [] : [stubPlugin(options.stubs)]),\n ...(options.plugins ?? []),\n ],\n});\n\n/**\n * Build a {@link SeoGraphLoader} that evaluates `entry` in a headless Vite\n * server rooted at `root`, and returns what its `exportName` export resolves to.\n */\nexport const viteGraphLoader =\n (options: ViteGraphLoaderOptions): SeoGraphLoader =>\n async () => {\n const exportName = options.exportName ?? \"loadSeoGraph\";\n if (options.env) seedEnv(options.env);\n\n const server = await withCleanStdout(() => createServer(inlineConfigFor(options))).catch(\n (cause: unknown) => {\n throw new Error(`Could not start the Vite loader: ${messageOf(cause)}`);\n },\n );\n\n // The server is a resource from here on: a failed load must still close it,\n // and a successful one hands the close to the caller as `dispose`.\n const graph = await withCleanStdout(async () => {\n const module = (await server.ssrLoadModule(options.entry)) as Record<string, unknown>;\n const load = module[exportName];\n if (typeof load !== \"function\") {\n throw new Error(\n `${options.entry} has no \\`${exportName}\\` export (found: ${Object.keys(module).join(\", \") || \"nothing\"}).`,\n );\n }\n return (await load()) as SeoGraph;\n }).catch(async (cause: unknown) => {\n // Close on the way out, but never let a close failure bury the real\n // error: the reason the graph did not build is the useful one.\n await withCleanStdout(() => server.close()).catch(() => {});\n throw new Error(`Could not build the SEO graph: ${messageOf(cause)}`);\n });\n\n return { graph, dispose: () => withCleanStdout(() => server.close()) };\n };\n","/**\n * `pagegraph/config` — the `pagegraph` CLI's configuration surface.\n *\n * The CLI is a set of pure views over one graph, but *acquiring* that graph is\n * host knowledge: only the app knows where its graph module lives and what its\n * module needs to evaluate. So the app declares it once in a `seo.config.ts` at\n * its root, which the CLI discovers from the working directory:\n *\n * ```ts\n * // seo.config.ts\n * import { defineSeoConfig, viteGraphLoader } from \"pagegraph/config\";\n * import { routeConfig } from \"./lib/route-config\";\n *\n * export default defineSeoConfig({\n * origin: \"https://example.com\",\n * disallow: routeConfig.robotsExclusions,\n * contentSignal: \"search=yes, ai-input=yes, ai-train=yes\",\n * loadGraph: viteGraphLoader({ root: import.meta.dirname, entry: \"/lib/seo/graph.ts\" }),\n * });\n * ```\n *\n * Nothing here imports Effect. The CLI needs it (an optional peer dependency),\n * but a config file must not: it is the app's file, and the app may not be an\n * Effect app.\n */\n\nexport type { LoadedSeoGraph, SeoGraphLoader, ViteGraphLoaderOptions } from \"./vite-graph-loader\";\nexport { viteGraphLoader } from \"./vite-graph-loader\";\n\nimport type { CoverageRule } from \"../core/checks\";\nimport type { SeoGraphLoader } from \"./vite-graph-loader\";\n\nexport interface SeoWorkflowOpenCodeConfig {\n /** Project-owned OpenCode configuration directory, relative to the project root. */\n readonly configDirectory: string;\n /** Default `provider/model` used by agentic workflows. */\n readonly defaultModel: string;\n /** Per-workflow `provider/model` overrides. */\n readonly models?: Readonly<Record<string, string>> | undefined;\n /** Per-completion deadline in milliseconds, including JSON repair; defaults to 180000. */\n readonly timeoutMs?: number | undefined;\n}\n\nexport interface SeoWorkflowContextConfig {\n /** Durable project context included in every workflow prompt. */\n readonly files?: ReadonlyArray<string> | undefined;\n /** Additional context files keyed by workflow id. */\n readonly byWorkflow?: Readonly<Record<string, ReadonlyArray<string>>> | undefined;\n}\n\nexport interface SeoWorkflowConfig {\n readonly opencode: SeoWorkflowOpenCodeConfig;\n readonly context?: SeoWorkflowContextConfig | undefined;\n /** Ignored run-artifact directory, relative to the project root. */\n readonly runsDirectory?: string | undefined;\n}\n\nexport interface SeoCliConfig {\n /**\n * Canonical origin the sitemap and robots projections render under, no\n * trailing slash. The `--origin` flag overrides it per invocation.\n */\n readonly origin: string;\n /**\n * Path prefixes disallowed in robots.txt. Feed it the generated\n * `routeConfig.robotsExclusions` if you run the `pagegraph/vite` plugin.\n */\n readonly disallow: ReadonlyArray<string>;\n /**\n * Origin-wide Content-Signal preferences, forwarded to `renderRobots`.\n * Omit for none. Preview (`--no-indexable`) never emits the line.\n */\n readonly contentSignal?: string | undefined;\n /**\n * Extra robots.txt group lines, forwarded to `renderRobots` as `directives`.\n */\n readonly directives?: ReadonlyArray<string> | undefined;\n /**\n * Last-mile robots.txt override, forwarded to `renderRobots` as `transform`.\n * Runs for indexable and preview hosts.\n */\n readonly transform?: ((robots: string) => string) | undefined;\n /**\n * Contextual-link coverage policy for `pagegraph check`: every sitemap-eligible\n * page matching a rule's `path` glob needs at least `minInbound` incoming\n * `related` edges. A `--require-inbound` flag overrides this per invocation.\n */\n readonly coverage?: ReadonlyArray<CoverageRule> | undefined;\n /** Agentic workflow host, context, and artifact settings. */\n readonly workflows?: SeoWorkflowConfig | undefined;\n /** How the CLI gets the graph. {@link viteGraphLoader} covers the Vite-app case. */\n readonly loadGraph: SeoGraphLoader;\n}\n\n/** Identity — it exists for the type inference and the editor completions. */\nexport const defineSeoConfig = (config: SeoCliConfig): SeoCliConfig => config;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAgEA,MAAM,aAAa,UACjB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;;;;;;;;;;AAWvD,MAAM,kBAAkB,OAAU,QAAsC;CACtE,MAAM,gBAAgB,QAAQ,OAAO,MAAM,KAAK,QAAQ,MAAM;CAC9D,MAAM,cAAc,QAAQ;CAC5B,MAAM,eAAe,QAAQ;CAC7B,QAAQ,OAAO,QAAQ,QAAQ,OAAO,MAAM,KAAK,QAAQ,MAAM;CAC/D,QAAQ,OAAO,GAAG,SAAyB,QAAQ,MAAM,GAAG,IAAI;CAChE,QAAQ,QAAQ,GAAG,SAAyB,QAAQ,MAAM,GAAG,IAAI;CACjE,IAAI;EACF,OAAO,MAAM,IAAI;CACnB,UAAU;EACR,QAAQ,OAAO,QAAQ;EACvB,QAAQ,MAAM;EACd,QAAQ,OAAO;CACjB;AACF;;AAGA,MAAM,cAAc,UAA0D;CAC5E,MAAM,eAAe,OAAe,kBAAkB;CACtD,MAAM,oBAAoB,IAAI,IAC5B,OAAO,QAAQ,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,YAAY,CAAC,YAAY,EAAE,GAAG,MAAM,CAAC,CACvE;CACA,OAAO;EACL,MAAM;EACN,YAAY,OAAQ,MAAM,QAAQ,YAAY,EAAE,IAAI;EACpD,OAAO,OAAO,kBAAkB,IAAI,EAAE,KAAK;CAC7C;AACF;AAEA,MAAM,WAAW,QAAgD;CAC/D,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,GAAG,GAE3C,QAAQ,IAAI,SAAS;AAEzB;AAEA,MAAM,mBAAmB,aAAmD;CAC1E,YAAY;CACZ,MAAM,QAAQ;CACd,MAAM;CACN,UAAU;CACV,SAAS;CACT,aAAa;CACb,QAAQ;EAAE,gBAAgB;EAAM,KAAK;EAAO,OAAO;CAAK;CACxD,SAAS,EAAE,eAAe,KAAK;CAC/B,SAAS,CACP,GAAI,QAAQ,UAAU,KAAA,IAAY,CAAC,IAAI,CAAC,WAAW,QAAQ,KAAK,CAAC,GACjE,GAAI,QAAQ,WAAW,CAAC,CAC1B;AACF;;;;;AAMA,MAAa,mBACV,YACD,YAAY;CACV,MAAM,aAAa,QAAQ,cAAc;CACzC,IAAI,QAAQ,KAAK,QAAQ,QAAQ,GAAG;CAEpC,MAAM,SAAS,MAAM,sBAAsB,aAAa,gBAAgB,OAAO,CAAC,CAAC,CAAC,CAAC,OAChF,UAAmB;EAClB,MAAM,IAAI,MAAM,oCAAoC,UAAU,KAAK,GAAG;CACxE,CACF;CAoBA,OAAO;EAAE,OAAA,MAhBW,gBAAgB,YAAY;GAC9C,MAAM,SAAU,MAAM,OAAO,cAAc,QAAQ,KAAK;GACxD,MAAM,OAAO,OAAO;GACpB,IAAI,OAAO,SAAS,YAClB,MAAM,IAAI,MACR,GAAG,QAAQ,MAAM,YAAY,WAAW,oBAAoB,OAAO,KAAK,MAAM,CAAC,CAAC,KAAK,IAAI,KAAK,UAAU,GAC1G;GAEF,OAAQ,MAAM,KAAK;EACrB,CAAC,CAAC,CAAC,MAAM,OAAO,UAAmB;GAGjC,MAAM,sBAAsB,OAAO,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;GAC1D,MAAM,IAAI,MAAM,kCAAkC,UAAU,KAAK,GAAG;EACtE,CAAC;EAEe,eAAe,sBAAsB,OAAO,MAAM,CAAC;CAAE;AACvE;;;;ACnEF,MAAa,mBAAmB,WAAuC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as RenderedGraph, C as AnchorRegion, D as RenderedEdgeArtifact, E as RENDERED_EDGE_ARTIFACT_SCHEMA_VERSION, F as diffLinkGraph, I as extractAnchors, L as normalizePath, M as SimpleEdge, N as buildRenderedGraph, O as RenderedEdgeArtifactCrawl, P as decodeRenderedEdgeArtifact, R as renderedGraphFromEdges, S as Anchor, T as LinkGraphDiff, _ as PublicPath, a as checkGraph, b as SeoKind, c as BuildSeoGraphInput, d as SeoEdgeType, f as SeoGraph, g as buildSeoGraph, h as SeoSource, i as checkCoverage, j as RenderedPage, k as RenderedEdgeFailure, l as SeoCollection, m as SeoNode, n as Severity, o as checkRenderedCoverage, p as SeoInstance, r as Violation, s as hasStructuralViolations, t as CoverageRule, u as SeoEdge, v as Register, w as LinkEdge, x as SitemapPolicy, y as RouteSeo } from "./checks-
|
|
1
|
+
import { A as RenderedGraph, C as AnchorRegion, D as RenderedEdgeArtifact, E as RENDERED_EDGE_ARTIFACT_SCHEMA_VERSION, F as diffLinkGraph, I as extractAnchors, L as normalizePath, M as SimpleEdge, N as buildRenderedGraph, O as RenderedEdgeArtifactCrawl, P as decodeRenderedEdgeArtifact, R as renderedGraphFromEdges, S as Anchor, T as LinkGraphDiff, _ as PublicPath, a as checkGraph, b as SeoKind, c as BuildSeoGraphInput, d as SeoEdgeType, f as SeoGraph, g as buildSeoGraph, h as SeoSource, i as checkCoverage, j as RenderedPage, k as RenderedEdgeFailure, l as SeoCollection, m as SeoNode, n as Severity, o as checkRenderedCoverage, p as SeoInstance, r as Violation, s as hasStructuralViolations, t as CoverageRule, u as SeoEdge, v as Register, w as LinkEdge, x as SitemapPolicy, y as RouteSeo } from "./checks-_X8yjMvV.js";
|
|
2
2
|
import { AnyRouter } from "@tanstack/react-router";
|
|
3
3
|
//#region src/core/projections.d.ts
|
|
4
4
|
interface ProjectionConfig {
|
|
@@ -83,8 +83,10 @@ interface LinkCandidateOptions {
|
|
|
83
83
|
readonly limit?: number;
|
|
84
84
|
/** Restrict to these clusters; a filter may be a section or a bare kind. */
|
|
85
85
|
readonly clusters?: ReadonlyArray<string>;
|
|
86
|
-
/** Anchors already served in HTML
|
|
87
|
-
readonly renderedEdges?: ReadonlyArray<SimpleEdge
|
|
86
|
+
/** Anchors already served in HTML. Only body-region edges exclude their own direction. */
|
|
87
|
+
readonly renderedEdges?: ReadonlyArray<SimpleEdge & {
|
|
88
|
+
readonly region?: AnchorRegion;
|
|
89
|
+
}>;
|
|
88
90
|
}
|
|
89
91
|
/** One cluster and how many candidate pairs it contributes (before `limit`). */
|
|
90
92
|
interface LinkClusterSummary {
|
|
@@ -118,7 +120,9 @@ declare const generateLinkCandidates: (graph: SeoGraph, options?: LinkCandidateO
|
|
|
118
120
|
* `{ from, to }` edges, or an object carrying `edges` (or `internalEdges`).
|
|
119
121
|
* Throws on any other shape — this is user input, not a provider payload.
|
|
120
122
|
*/
|
|
121
|
-
declare const decodeRenderedEdges: (input: unknown) => ReadonlyArray<SimpleEdge
|
|
123
|
+
declare const decodeRenderedEdges: (input: unknown) => ReadonlyArray<SimpleEdge & {
|
|
124
|
+
readonly region?: AnchorRegion;
|
|
125
|
+
}>;
|
|
122
126
|
//#endregion
|
|
123
127
|
//#region src/core/inspect-html.d.ts
|
|
124
128
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as extractAnchors, i as diffLinkGraph, n as buildRenderedGraph, o as normalizePath, r as decodeRenderedEdgeArtifact, s as renderedGraphFromEdges, t as RENDERED_EDGE_ARTIFACT_SCHEMA_VERSION } from "./links-
|
|
1
|
+
import { a as extractAnchors, i as diffLinkGraph, n as buildRenderedGraph, o as normalizePath, r as decodeRenderedEdgeArtifact, s as renderedGraphFromEdges, t as RENDERED_EDGE_ARTIFACT_SCHEMA_VERSION } from "./links-DAa8KVzg.js";
|
|
2
2
|
//#region src/core/graph.ts
|
|
3
3
|
/** Kind for instances whose collection route carries no declaration to inherit. */
|
|
4
4
|
const FALLBACK_KIND = "page";
|
|
@@ -538,6 +538,7 @@ const undirectedEdgeKey = (from, to) => {
|
|
|
538
538
|
const [a, b] = [normalize(from), normalize(to)].sort();
|
|
539
539
|
return `${a}\u0000${b}`;
|
|
540
540
|
};
|
|
541
|
+
const directedEdgeKey = (from, to) => `${normalize(from)}\u0000${normalize(to)}`;
|
|
541
542
|
/** First path segment, or undefined for the root page. */
|
|
542
543
|
const topSegment = (path) => {
|
|
543
544
|
const segment = path.split("/").filter(Boolean)[0];
|
|
@@ -550,7 +551,7 @@ const isRootLevel = (path) => path.split("/").filter(Boolean).length <= 1;
|
|
|
550
551
|
* a shared top-level section wins. The kind fallback is local to root-level
|
|
551
552
|
* pages, so it never pairs two nested pages from different sections.
|
|
552
553
|
*/
|
|
553
|
-
const
|
|
554
|
+
const candidateClusterOf = (a, b) => {
|
|
554
555
|
const aSegment = topSegment(a.path);
|
|
555
556
|
const bSegment = topSegment(b.path);
|
|
556
557
|
if (aSegment !== void 0 && aSegment === bSegment) return {
|
|
@@ -581,30 +582,26 @@ const candidateSourceText = (node) => node.instance?.description?.trim() || node
|
|
|
581
582
|
const generateLinkCandidates = (graph, options = {}) => {
|
|
582
583
|
const limit = options.limit ?? 50;
|
|
583
584
|
const filters = options.clusters ?? [];
|
|
584
|
-
const rendered = new Set((options.renderedEdges ?? []).map((edge) =>
|
|
585
|
-
const declared = new Set(graph.edges.filter((edge) => edge.type === "related").map((edge) =>
|
|
585
|
+
const rendered = new Set((options.renderedEdges ?? []).filter((edge) => edge.region === void 0 || edge.region === "body").map((edge) => directedEdgeKey(edge.from, edge.to)));
|
|
586
|
+
const declared = new Set(graph.edges.filter((edge) => edge.type === "related").map((edge) => directedEdgeKey(edge.from, edge.to)));
|
|
586
587
|
const eligible = [...graph.nodes.values()].filter(isSitemapEligible);
|
|
587
588
|
const all = [];
|
|
588
589
|
for (let i = 0; i < eligible.length; i++) for (let j = i + 1; j < eligible.length; j++) {
|
|
589
590
|
const a = eligible[i];
|
|
590
591
|
const b = eligible[j];
|
|
591
|
-
const cluster =
|
|
592
|
+
const cluster = candidateClusterOf(a, b);
|
|
592
593
|
if (cluster === void 0) continue;
|
|
593
594
|
if (filters.length > 0 && !filters.some((filter) => matchesClusterFilter(cluster.key, filter))) continue;
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
destination: a.path,
|
|
605
|
-
cluster: cluster.key,
|
|
606
|
-
reason: cluster.reason
|
|
607
|
-
});
|
|
595
|
+
for (const [source, destination] of [[a.path, b.path], [b.path, a.path]]) {
|
|
596
|
+
const key = directedEdgeKey(source, destination);
|
|
597
|
+
if (declared.has(key) || rendered.has(key)) continue;
|
|
598
|
+
all.push({
|
|
599
|
+
source,
|
|
600
|
+
destination,
|
|
601
|
+
cluster: cluster.key,
|
|
602
|
+
reason: cluster.reason
|
|
603
|
+
});
|
|
604
|
+
}
|
|
608
605
|
}
|
|
609
606
|
const compare = (x, y) => x.cluster < y.cluster ? -1 : x.cluster > y.cluster ? 1 : x.source < y.source ? -1 : x.source > y.source ? 1 : x.destination < y.destination ? -1 : x.destination > y.destination ? 1 : 0;
|
|
610
607
|
all.sort(compare);
|
|
@@ -631,11 +628,21 @@ const decodeRenderedEdges = (input) => {
|
|
|
631
628
|
if (array === void 0) throw new Error("expected an array of { from, to } edges, or an object with an `edges` array");
|
|
632
629
|
return array.map((item) => {
|
|
633
630
|
if (item === null || typeof item !== "object") throw new Error("each rendered edge must be an object with string `from` and `to`");
|
|
634
|
-
const { from, to } = item;
|
|
631
|
+
const { from, to, region } = item;
|
|
635
632
|
if (typeof from !== "string" || typeof to !== "string") throw new Error("each rendered edge must have string `from` and `to`");
|
|
636
|
-
|
|
633
|
+
if (region !== void 0 && ![
|
|
634
|
+
"nav",
|
|
635
|
+
"header",
|
|
636
|
+
"footer",
|
|
637
|
+
"body"
|
|
638
|
+
].includes(region)) throw new Error("rendered edge region must be nav, header, footer, or body");
|
|
639
|
+
return region === void 0 ? {
|
|
637
640
|
from,
|
|
638
641
|
to
|
|
642
|
+
} : {
|
|
643
|
+
from,
|
|
644
|
+
to,
|
|
645
|
+
region
|
|
639
646
|
};
|
|
640
647
|
});
|
|
641
648
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/core/graph.ts","../src/core/projections.ts","../src/core/checks.ts","../src/core/link-candidates.ts","../src/core/inspect-html.ts","../src/core/resolve-route-link.ts"],"sourcesContent":["/**\n * SEO graph — the derived model that projections (sitemap, robots), the CLI, and\n * the check engine all read from. Built from route declarations (`staticData.seo`)\n * plus consumer-supplied content collections. This module is pure: no React, no\n * env, no knowledge of where instances come from. Origins and env-derived values\n * are injected by the callers of the projections, never read here.\n *\n * A node is one of:\n * - a structural route (`source: \"route\"`), keyed by its normalized full path,\n * merging a layout route's declaration (crumb) with its index child's (kind,\n * sitemap policy) when both resolve to the same URL;\n * - a content instance (`source` = the collection's label), keyed by the page URL\n * and carrying page-level metadata.\n *\n * Edges: `crumb-parent` (breadcrumb ancestry), `related` (deliberate cross-links),\n * `collection-member` (membership in a curated set), and `redirect` (route aliases).\n * The route walk emits crumb/related/redirect edges from declarations; a collection\n * may declare any additional edges its instances need.\n */\n\nimport type { AnyRoute } from \"@tanstack/react-router\";\n\nimport type { PublicPath, RouteSeo, SeoKind } from \"./declare\";\n\n/**\n * Where a node came from: `\"route\"` for a structural route declaration, or the\n * `source` label of the collection that produced the instance (e.g. \"blog\").\n */\nexport type SeoSource = string;\n\nexport interface SeoNode {\n /** Canonical path, no origin (e.g. \"/pricing\", \"/blog/my-post\"). */\n path: string;\n kind: SeoKind;\n source: SeoSource;\n /** Route-declared policy, or synthesized (kind + inherited sitemap) for instances. */\n policy: RouteSeo;\n instance?:\n | {\n title: string;\n description?: string | undefined;\n publishedAt?: string | undefined;\n modifiedAt?: string | undefined;\n }\n | undefined;\n}\n\nexport type SeoEdgeType = \"crumb-parent\" | \"related\" | \"redirect\" | \"collection-member\";\n\nexport interface SeoEdge {\n from: string;\n to: string;\n type: SeoEdgeType;\n}\n\nexport interface SeoGraph {\n nodes: Map<string, SeoNode>;\n edges: Array<SeoEdge>;\n /** Exact-path ownership conflicts encountered while assembling graph sources. */\n collisions?: ReadonlyArray<{ path: string; sources: ReadonlyArray<SeoSource> }> | undefined;\n}\n\n/** One concrete page produced by a collection. */\nexport interface SeoInstance {\n /** Canonical path of the page (e.g. \"/blog/my-post\"). */\n readonly path: string;\n readonly title: string;\n readonly description?: string | undefined;\n readonly publishedAt?: string | undefined;\n readonly modifiedAt?: string | undefined;\n}\n\nexport interface SeoCollection {\n /**\n * The param route these instances render through (e.g. \"/blog/$slug\"). Instances\n * inherit this route's declared policy (kind + sitemap) — the graph reads it from\n * the structural node, so declarations stay the single source of truth.\n */\n readonly route: PublicPath;\n /** The `source` stamped on every node this collection produces (e.g. \"blog\"). */\n readonly source: SeoSource;\n readonly instances: ReadonlyArray<SeoInstance>;\n /** Edges the collection declares between its instances and the rest of the graph. */\n readonly edges?: ReadonlyArray<SeoEdge> | undefined;\n}\n\nexport interface BuildSeoGraphInput {\n readonly routeTree: AnyRoute;\n readonly collections?: ReadonlyArray<SeoCollection> | undefined;\n}\n\n/** Kind for instances whose collection route carries no declaration to inherit. */\nconst FALLBACK_KIND: SeoKind = \"page\";\n\n/** Structural view of a route we walk — the fields present before router init(). */\ninterface WalkableRoute {\n readonly options: {\n readonly path?: string | undefined;\n readonly staticData?: { readonly seo?: RouteSeo | undefined } | undefined;\n };\n readonly children?: ReadonlyArray<WalkableRoute> | undefined;\n}\n\n/**\n * Join a child's local path onto its parent's computed full path with the same\n * semantics as TanStack's route init (relative segments, index \"/\" inherits the\n * parent, pathless/group routes are transparent), then normalize trailing slashes.\n */\nfunction joinPath(parent: string, seg: string | undefined): string {\n if (seg === undefined) return parent; // pathless layout / route group\n if (seg === \"/\") return parent; // index route resolves to its parent's URL\n const trimmed = seg.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\");\n const joined = `${parent === \"/\" ? \"\" : parent}/${trimmed}`;\n return joined.replace(/\\/{2,}/g, \"/\");\n}\n\n/** Merge a route's declaration into an existing same-path node (deeper route wins). */\nfunction mergeSeo(base: RouteSeo, override: RouteSeo): RouteSeo {\n return {\n kind: override.kind,\n crumb: override.crumb ?? base.crumb,\n sitemap: override.sitemap ?? base.sitemap,\n robots: override.robots ?? base.robots,\n related: override.related ?? base.related,\n link: override.link ?? base.link,\n redirectTo: override.redirectTo ?? base.redirectTo,\n };\n}\n\n/**\n * Walk the route tree building structural nodes and crumb-parent edges. `crumbStack`\n * holds the paths of crumb-declaring ancestors so each crumb node links to its nearest\n * crumb ancestor down the real route-parent chain (matching render-time breadcrumbs).\n */\nfunction walkRoutes(\n route: WalkableRoute,\n parentPath: string,\n isRoot: boolean,\n nodes: Map<string, SeoNode>,\n edges: Array<SeoEdge>,\n crumbStack: Array<string>,\n): void {\n const path = isRoot ? \"/\" : joinPath(parentPath, route.options.path);\n const seo = route.options.staticData?.seo;\n\n if (seo) {\n const existing = nodes.get(path);\n if (existing) {\n existing.policy = mergeSeo(existing.policy, seo);\n existing.kind = existing.policy.kind;\n } else {\n nodes.set(path, { path, kind: seo.kind, source: \"route\", policy: { ...seo } });\n }\n\n const nearestCrumbAncestor = crumbStack[crumbStack.length - 1];\n if (seo.crumb !== undefined && nearestCrumbAncestor !== undefined) {\n edges.push({ from: path, to: nearestCrumbAncestor, type: \"crumb-parent\" });\n }\n }\n\n const pushedCrumb = seo?.crumb !== undefined;\n if (pushedCrumb) crumbStack.push(path);\n for (const child of route.children ?? []) {\n walkRoutes(child, path, false, nodes, edges, crumbStack);\n }\n if (pushedCrumb) crumbStack.pop();\n}\n\n/**\n * Add one collection's instance nodes, inheriting kind + sitemap policy from the\n * collection route's declaration, then append the edges it declares. A path already\n * owned by another node is a collision: the first owner keeps the path and the\n * conflict is reported (the `path-owner-collision` check turns it into a violation).\n */\nfunction addCollection(\n nodes: Map<string, SeoNode>,\n edges: Array<SeoEdge>,\n collisions: Array<{ path: string; sources: ReadonlyArray<SeoSource> }>,\n collection: SeoCollection,\n): void {\n const collectionNode = nodes.get(collection.route);\n const kind = collectionNode?.kind ?? FALLBACK_KIND;\n const sitemap = collectionNode?.policy.sitemap;\n\n /**\n * Paths this collection lost to an earlier owner. Their instances never enter\n * the graph, so any edge declared out of them would dangle — and the dead-edge\n * check only validates an edge's `to`, so nothing downstream would catch it.\n */\n const rejected = new Set<string>();\n\n for (const instance of collection.instances) {\n const existing = nodes.get(instance.path);\n if (existing) {\n collisions.push({ path: instance.path, sources: [existing.source, collection.source] });\n rejected.add(instance.path);\n continue;\n }\n nodes.set(instance.path, {\n path: instance.path,\n kind,\n source: collection.source,\n policy: { kind, sitemap },\n instance: {\n title: instance.title,\n description: instance.description,\n publishedAt: instance.publishedAt,\n modifiedAt: instance.modifiedAt,\n },\n });\n }\n\n for (const edge of collection.edges ?? []) {\n if (rejected.has(edge.from)) continue;\n edges.push(edge);\n }\n}\n\n/**\n * Build the SEO graph from route declarations and content collections.\n *\n * Synchronous: the caller materializes its collections before calling, so there is\n * no async work here. Callers own the origin.\n */\nexport function buildSeoGraph(input: BuildSeoGraphInput): SeoGraph {\n const nodes = new Map<string, SeoNode>();\n const edges: Array<SeoEdge> = [];\n const collisions: Array<{ path: string; sources: ReadonlyArray<SeoSource> }> = [];\n\n walkRoutes(input.routeTree as unknown as WalkableRoute, \"/\", true, nodes, edges, []);\n\n for (const collection of input.collections ?? []) {\n addCollection(nodes, edges, collisions, collection);\n }\n\n for (const node of nodes.values()) {\n if (node.source !== \"route\") continue;\n for (const to of node.policy.related ?? []) {\n edges.push({ from: node.path, to, type: \"related\" });\n }\n if (node.policy.redirectTo !== undefined) {\n edges.push({ from: node.path, to: node.policy.redirectTo, type: \"redirect\" });\n }\n }\n\n return { nodes, edges, collisions };\n}\n","/**\n * Projections of the SEO graph: sitemap.xml, robots.txt, and single-node\n * inspection. Pure functions — the origin, the host's indexability, and the\n * robots disallow list are injected by the caller, never read from the\n * environment or a generated file.\n */\n\nimport type { SeoEdge, SeoGraph, SeoNode } from \"./graph\";\n\nexport interface ProjectionConfig {\n origin: string;\n indexable: boolean;\n}\n\nexport interface RobotsConfig extends ProjectionConfig {\n /** Path prefixes to disallow on an indexable host (e.g. the app-only groups). */\n disallow: ReadonlyArray<string>;\n /**\n * Origin-wide Content-Signal preferences\n * (https://contentsignals.org/), emitted as `Content-Signal: <value>` under\n * `User-agent: *` on an indexable host. Omit for none. The plugin never\n * invents a default — pass the policy you want, e.g.\n * `\"search=yes, ai-input=yes, ai-train=yes\"`.\n */\n contentSignal?: string | undefined;\n /**\n * Extra full lines in the indexable `User-agent: *` group, after\n * Content-Signal (if any) and before Allow/Disallow. Use for directives the\n * plugin does not model, or to compose {@link contentSignal} yourself.\n */\n directives?: ReadonlyArray<string> | undefined;\n /**\n * Last-mile override: receives the rendered robots.txt and returns the file\n * to emit. Runs for indexable and preview hosts. Use when you need to wrap\n * or replace the default body rather than add group lines.\n */\n transform?: ((robots: string) => string) | undefined;\n}\n\nexport interface NodeReport {\n node: SeoNode;\n inSitemap: boolean;\n incoming: Array<SeoEdge>;\n outgoing: Array<SeoEdge>;\n}\n\nconst escapeXml = (value: string): string =>\n value\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n\n/**\n * A node belongs in the sitemap when it declares a positive sitemap policy, is not\n * a redirect, is not robots-noindexed, and is not a param template (a route whose\n * path still contains a `$` segment — those exist only so their instances inherit).\n */\nexport function isSitemapEligible(node: SeoNode): boolean {\n if (node.policy.redirectTo !== undefined) return false;\n if (node.policy.robots?.includes(\"noindex\")) return false;\n if (!node.policy.sitemap) return false; // false or absent\n if (node.path.includes(\"$\")) return false;\n return true;\n}\n\n/** Canonical absolute URL for a node under the given origin. */\nfunction urlForNode(origin: string, node: SeoNode): string {\n return node.path === \"/\" ? origin : `${origin}${node.path}`;\n}\n\n/**\n * Instance lastmod: the most recent date the page's frontmatter carries. A\n * collection whose instances carry no dates (docs, a manifest-driven gallery)\n * emits no `<lastmod>` at all.\n */\nfunction instanceLastmod(node: SeoNode): string | undefined {\n const instance = node.instance;\n if (!instance) return undefined;\n const date = instance.modifiedAt ?? instance.publishedAt;\n return date ? new Date(date).toISOString() : undefined;\n}\n\nfunction renderUrlEntry(url: string, lastmod: string | undefined, node: SeoNode): string {\n const sitemap = node.policy.sitemap;\n // isSitemapEligible guarantees a positive policy before this runs.\n const { changeFrequency, priority } = sitemap as { changeFrequency: string; priority: number };\n const lines = [` <url>`, ` <loc>${escapeXml(url)}</loc>`];\n if (lastmod) lines.push(` <lastmod>${lastmod}</lastmod>`);\n lines.push(\n ` <changefreq>${changeFrequency}</changefreq>`,\n ` <priority>${priority.toFixed(1)}</priority>`,\n ` </url>`,\n );\n return lines.join(\"\\n\");\n}\n\n/**\n * Render sitemap.xml from the graph. Structural route entries emit no `<lastmod>`\n * (a route has no publish date); content instances emit it from their frontmatter.\n * Route entries are sorted by path, then instances follow in collection order.\n * `indexable` is intentionally unused — the sitemap body is host-independent;\n * robots.txt is what gates crawling.\n */\nexport function renderSitemap(graph: SeoGraph, cfg: ProjectionConfig): string {\n const nodes = [...graph.nodes.values()];\n const staticNodes = nodes\n .filter((node) => node.source === \"route\" && isSitemapEligible(node))\n .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));\n const instanceNodes = nodes.filter((node) => node.source !== \"route\" && isSitemapEligible(node));\n\n const seen = new Set<string>();\n const entries: Array<string> = [];\n for (const node of [...staticNodes, ...instanceNodes]) {\n const url = urlForNode(cfg.origin, node);\n const key = url.toLowerCase().replace(/\\/$/, \"\");\n if (seen.has(key)) continue;\n seen.add(key);\n entries.push(renderUrlEntry(url, instanceLastmod(node), node));\n }\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\\n${entries.join(\n \"\\n\",\n )}\\n</urlset>\\n`;\n}\n\n/** Format a Content-Signal robots.txt directive from the preference list. */\nexport function contentSignal(value: string): string {\n return `Content-Signal: ${value}`;\n}\n\nconst groupLines = (cfg: RobotsConfig): Array<string> => {\n const lines: Array<string> = [];\n if (cfg.contentSignal !== undefined && cfg.contentSignal !== \"\") {\n lines.push(contentSignal(cfg.contentSignal));\n }\n for (const directive of cfg.directives ?? []) {\n if (directive !== \"\") lines.push(directive);\n }\n return lines;\n};\n\n/**\n * Render robots.txt. A non-indexable host (previews) gets a disallow-all\n * with no Sitemap line; an indexable host disallows exactly the prefixes the caller\n * passes. Pages that declare `robots: noindex` are intentionally NOT added as\n * Disallow entries — a Disallow would stop crawlers reaching the page to read its\n * `noindex, follow` meta, so the graph's per-node robots policy never feeds this\n * list. `graph` is unused — kept for signature parity with the other projections,\n * which callers load the graph once for and pass to each.\n *\n * Origin-wide group directives (`contentSignal`, `directives`) are indexable-host\n * only. {@link RobotsConfig.transform} always runs last so a consumer can override\n * the whole file.\n */\nexport function renderRobots(_graph: SeoGraph, cfg: RobotsConfig): string {\n const rendered = cfg.indexable\n ? [\n \"User-agent: *\",\n ...groupLines(cfg),\n \"Allow: /\",\n ...cfg.disallow.map((path) => `Disallow: ${path}`),\n \"\",\n `Sitemap: ${cfg.origin}/sitemap.xml`,\n `Host: ${cfg.origin}`,\n \"\",\n ].join(\"\\n\")\n : [\"User-agent: *\", \"Disallow: /\", \"\"].join(\"\\n\");\n return cfg.transform === undefined ? rendered : cfg.transform(rendered);\n}\n\n/** Inspect a single node: its declaration, sitemap eligibility, and edges. */\nexport function inspectNode(graph: SeoGraph, path: string): NodeReport | undefined {\n const node = graph.nodes.get(path);\n if (!node) return undefined;\n return {\n node,\n inSitemap: isSitemapEligible(node),\n incoming: graph.edges.filter((edge) => edge.to === path),\n outgoing: graph.edges.filter((edge) => edge.from === path),\n };\n}\n","/**\n * The SEO check engine: a set of rules run against the derived {@link SeoGraph}.\n * Pure — no I/O, no `clientEnv`, no React. The CLI's `pagegraph check` command and the\n * vitest suite both call {@link checkGraph}; nothing else derives correctness.\n *\n * Two severities, mapped to the CLI's exit contract:\n * - `structural` — a declaration is internally broken (a link points nowhere, a\n * card would render empty, a page contradicts its own robots/sitemap intent).\n * Any structural violation fails `pagegraph check` (exit 1); these must not ship.\n * - `editorial` — a quality smell (duplicate or mis-sized titles/descriptions).\n * Reported as warnings; `pagegraph check` still exits 0 when only these are present.\n *\n * The graph only knows what declarations and frontmatter carry, so the rules are\n * scoped to that: per-node title/description live only on collection *instances*,\n * never on structural route nodes (whose head tags are composed at render time and\n * are not in the graph). Rules are data-driven and listed once in\n * {@link CHECK_RULES}.\n */\n\nimport type { SitemapPolicy } from \"./declare\";\nimport type { SeoGraph, SeoNode } from \"./graph\";\nimport type { LinkEdge } from \"./links\";\nimport { isSitemapEligible } from \"./projections\";\n\nexport type Severity = \"structural\" | \"editorial\";\n\nexport interface Violation {\n severity: Severity;\n rule: string;\n path?: string | undefined;\n message: string;\n fix?: string | undefined;\n}\n\n/**\n * A contextual-link coverage rule: every sitemap-eligible page matching the\n * `path` glob must have at least `minInbound` incoming contextual links. Unlike\n * the static rules, these come from project policy (a CLI flag or\n * `seo.config.ts`), because \"which pages are money pages\" is app knowledge.\n *\n * The same rule shape is evaluated twice: `checkCoverage` counts declared\n * `related` edges, and `checkRenderedCoverage` counts the contextual anchors a\n * crawl actually served. The target universe is always the declared graph's\n * sitemap-eligible pages — the rendered pass only swaps the edge source.\n */\nexport interface CoverageRule {\n /** Path glob: `*` matches within a segment, `**` matches across segments. */\n readonly path: string;\n /** Minimum incoming contextual links the matched page needs. */\n readonly minInbound: number;\n}\n\n/** A single finding before its rule's `severity`/`rule` name are attached. */\ninterface RawViolation {\n path?: string | undefined;\n message: string;\n fix?: string | undefined;\n}\n\ninterface CheckRule {\n readonly name: string;\n readonly severity: Severity;\n readonly evaluate: (graph: SeoGraph) => ReadonlyArray<RawViolation>;\n}\n\n/** A positive sitemap policy — the author asked for this page to be indexed. */\nconst hasPositiveSitemap = (sitemap: SitemapPolicy | false | undefined): sitemap is SitemapPolicy =>\n sitemap !== undefined && sitemap !== false;\n\n/** Content and manifest instances carry page-level title/description. */\nconst isInstance = (node: SeoNode): boolean => node.source !== \"route\";\n\n/** Group instance nodes by a present, non-empty string field for duplicate detection. */\nconst groupInstancesBy = (\n graph: SeoGraph,\n field: (node: SeoNode) => string | undefined,\n): Map<string, Array<SeoNode>> => {\n const groups = new Map<string, Array<SeoNode>>();\n for (const node of graph.nodes.values()) {\n if (!isInstance(node)) continue;\n const value = field(node)?.trim();\n if (!value) continue;\n const bucket = groups.get(value);\n if (bucket) bucket.push(node);\n else groups.set(value, [node]);\n }\n return groups;\n};\n\nconst DESCRIPTION_MAX = 160;\nconst DESCRIPTION_MIN = 50;\n\n/**\n * Every check, in one place. `checkGraph` runs them in order and stamps each\n * finding with its rule name and severity, so the output is grouped by rule and\n * deterministic (nodes iterate in graph insertion order, edges in array order).\n */\nconst CHECK_RULES: ReadonlyArray<CheckRule> = [\n {\n name: \"path-owner-collision\",\n severity: \"structural\",\n evaluate: (graph) =>\n (graph.collisions ?? []).map((collision) => ({\n path: collision.path,\n message: `Canonical path \"${collision.path}\" is owned by multiple sources: ${collision.sources.join(\", \")}.`,\n fix: \"Give every concrete page one canonical path and one graph owner.\",\n })),\n },\n {\n name: \"canonical-path-collision\",\n severity: \"structural\",\n evaluate: (graph) => {\n const groups = new Map<string, Array<string>>();\n for (const path of graph.nodes.keys()) {\n const canonical = path.toLowerCase().replace(/\\/$/, \"\") || \"/\";\n const paths = groups.get(canonical);\n if (paths) paths.push(path);\n else groups.set(canonical, [path]);\n }\n return [...groups.values()]\n .filter((paths) => paths.length > 1)\n .flatMap((paths) =>\n paths.map((path) => ({\n path,\n message: `Canonical path collides with ${paths.filter((candidate) => candidate !== path).join(\", \")}.`,\n fix: \"Use one lowercase, trailing-slash-normalized canonical path.\",\n })),\n );\n },\n },\n {\n name: \"self-edge\",\n severity: \"structural\",\n evaluate: (graph) =>\n graph.edges\n .filter((edge) => edge.from === edge.to)\n .map((edge) => ({\n path: edge.from,\n message: `${edge.type} edge points back to its own source node.`,\n fix: \"Remove the self-reference from the canonical manifest or route declaration.\",\n })),\n },\n {\n name: \"duplicate-edge\",\n severity: \"structural\",\n evaluate: (graph) => {\n const seen = new Set<string>();\n const duplicates: Array<RawViolation> = [];\n for (const edge of graph.edges) {\n const key = `${edge.from}\\u0000${edge.to}\\u0000${edge.type}`;\n if (seen.has(key)) {\n duplicates.push({\n path: edge.from,\n message: `Duplicate ${edge.type} edge from \"${edge.from}\" to \"${edge.to}\".`,\n fix: \"Declare each graph relationship exactly once.\",\n });\n } else {\n seen.add(key);\n }\n }\n return duplicates;\n },\n },\n {\n // A `related` or `redirect` edge points at a path with no node — the target\n // route/page was renamed or deleted and the declaration wasn't updated.\n name: \"dead-edge\",\n severity: \"structural\",\n evaluate: (graph) =>\n graph.edges\n .filter((edge) => edge.type !== \"crumb-parent\" && !graph.nodes.has(edge.to))\n .map((edge) => ({\n path: edge.from,\n message: `${edge.type} edge from \"${edge.from}\" points at \"${edge.to}\", which is not a node in the graph.`,\n fix: `Update the ${edge.type === \"redirect\" ? \"redirectTo\" : \"related\"} target on the \"${edge.from}\" route, or restore \"${edge.to}\".`,\n })),\n },\n {\n // A content instance with no usable title can't render a legible <title> or card.\n name: \"instance-missing-title\",\n severity: \"structural\",\n evaluate: (graph) =>\n [...graph.nodes.values()]\n .filter((node) => isInstance(node) && !node.instance?.title.trim())\n .map((node) => ({\n path: node.path,\n message: `Content page \"${node.path}\" has no title.`,\n fix: \"Add a `title` to the page frontmatter.\",\n })),\n },\n {\n // The declaration asks for the page to be in the sitemap yet also marks it\n // noindex — contradictory intent. The projection resolves it (noindex wins,\n // excluded), but the declaration should say one thing.\n name: \"sitemap-noindex-contradiction\",\n severity: \"structural\",\n evaluate: (graph) =>\n [...graph.nodes.values()]\n .filter(\n (node) =>\n hasPositiveSitemap(node.policy.sitemap) &&\n node.policy.robots?.toLowerCase().includes(\"noindex\"),\n )\n .map((node) => ({\n path: node.path,\n message: `\"${node.path}\" declares a sitemap policy but its robots value is \"${node.policy.robots}\".`,\n fix: \"Drop the sitemap policy (or set `sitemap: false`) on a noindex page, or remove the noindex robots value.\",\n })),\n },\n {\n // Robots declarations are house-convention lowercase: the sitemap projection\n // matches `includes(\"noindex\")` literally, so a miscased value (\"Noindex\")\n // would silently stay sitemap-eligible. This gate makes that unrepresentable.\n name: \"robots-not-lowercase\",\n severity: \"structural\",\n evaluate: (graph) =>\n [...graph.nodes.values()]\n .filter(\n (node) =>\n node.policy.robots !== undefined &&\n node.policy.robots !== node.policy.robots.toLowerCase(),\n )\n .map((node) => ({\n path: node.path,\n message: `\"${node.path}\" declares robots \"${node.policy.robots}\" — robots values must be lowercase.`,\n fix: 'Lowercase the robots declaration (e.g. \"noindex, follow\").',\n })),\n },\n {\n // A `related` card for a route target renders from that route's `link`\n // metadata; without it the card has no title/description and renders empty.\n // (Content-instance targets render from their frontmatter, so they're exempt.)\n name: \"related-target-missing-link\",\n severity: \"structural\",\n evaluate: (graph) => {\n const linklessTargets = new Set<string>();\n for (const edge of graph.edges) {\n if (edge.type !== \"related\") continue;\n const target = graph.nodes.get(edge.to);\n if (target && target.source === \"route\" && target.policy.link === undefined) {\n linklessTargets.add(edge.to);\n }\n }\n return [...linklessTargets].map((path) => ({\n path,\n message: `Route \"${path}\" is a related-link target but declares no link metadata; its card would render empty.`,\n fix: `Add \\`link: { title, description }\\` to the \"${path}\" route's staticData.seo.`,\n }));\n },\n },\n {\n // A redirect/alias node should never advertise itself in the sitemap.\n name: \"redirect-in-sitemap\",\n severity: \"structural\",\n evaluate: (graph) =>\n [...graph.nodes.values()]\n .filter(\n (node) => node.policy.redirectTo !== undefined && hasPositiveSitemap(node.policy.sitemap),\n )\n .map((node) => ({\n path: node.path,\n message: `Redirect \"${node.path}\" (→ \"${node.policy.redirectTo}\") also declares a sitemap policy.`,\n fix: \"Remove the sitemap policy from the redirect route; only its target belongs in the sitemap.\",\n })),\n },\n {\n name: \"duplicate-title\",\n severity: \"editorial\",\n evaluate: (graph) =>\n [...groupInstancesBy(graph, (node) => node.instance?.title).entries()]\n .filter(([, nodes]) => nodes.length > 1)\n .flatMap(([title, nodes]) =>\n nodes.map((node) => ({\n path: node.path,\n message: `Title \"${title}\" is shared by ${nodes.length} pages.`,\n fix: \"Give each page a distinct title.\",\n })),\n ),\n },\n {\n name: \"duplicate-description\",\n severity: \"editorial\",\n evaluate: (graph) =>\n [...groupInstancesBy(graph, (node) => node.instance?.description).entries()]\n .filter(([, nodes]) => nodes.length > 1)\n .flatMap(([, nodes]) =>\n nodes.map((node) => ({\n path: node.path,\n message: `Description is shared by ${nodes.length} pages.`,\n fix: \"Write a distinct meta description for each page.\",\n })),\n ),\n },\n {\n // Meta descriptions outside ~50–160 chars either get truncated in SERPs or\n // read as too thin.\n name: \"description-length\",\n severity: \"editorial\",\n evaluate: (graph) =>\n [...graph.nodes.values()].flatMap((node) => {\n if (!isInstance(node)) return [];\n const description = node.instance?.description?.trim();\n if (!description) return [];\n if (description.length > DESCRIPTION_MAX) {\n return [\n {\n path: node.path,\n message: `Description is ${description.length} chars (max ${DESCRIPTION_MAX}); it will be truncated in results.`,\n fix: `Trim the description to ${DESCRIPTION_MAX} characters or fewer.`,\n },\n ];\n }\n if (description.length < DESCRIPTION_MIN) {\n return [\n {\n path: node.path,\n message: `Description is ${description.length} chars (min ${DESCRIPTION_MIN}); it reads as thin.`,\n fix: `Expand the description to at least ${DESCRIPTION_MIN} characters.`,\n },\n ];\n }\n return [];\n }),\n },\n];\n\n/**\n * Run every static rule against the graph, then any caller-supplied\n * {@link CoverageRule}s, and return the flat list of violations. With no\n * `coverage` option the result is exactly the static rule set.\n */\nexport function checkGraph(\n graph: SeoGraph,\n options: { readonly coverage?: ReadonlyArray<CoverageRule> | undefined } = {},\n): Array<Violation> {\n const violations: Array<Violation> = CHECK_RULES.flatMap((rule) =>\n rule.evaluate(graph).map((raw): Violation => ({\n severity: rule.severity,\n rule: rule.name,\n path: raw.path,\n message: raw.message,\n fix: raw.fix,\n })),\n );\n const coverage = options.coverage;\n if (coverage !== undefined && coverage.length > 0) {\n violations.push(...checkCoverage(graph, coverage));\n }\n return violations;\n}\n\n/** Escape a glob for `RegExp`, then expand `**`, `*`, and `?` to path-aware forms. */\nconst globToRegExp = (glob: string): RegExp => {\n const escaped = glob.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const source = escaped\n .replace(/\\*\\*/g, \"\\u0000\")\n .replace(/\\*/g, \"[^/]*\")\n .replace(/\\?/g, \"[^/]\")\n .replace(/\\u0000/g, \".*\");\n return new RegExp(`^${source}$`);\n};\n\n/**\n * The minimal view a coverage rule needs: which paths exist (for the glob), which\n * of them are sitemap-eligible (the enforced set), and how many contextual links\n * each receives. Declared and rendered coverage differ only in how this surface\n * is built, so the rule engine itself stays single-sourced.\n */\ninterface CoverageSurface {\n readonly paths: ReadonlyArray<string>;\n readonly eligiblePaths: ReadonlyArray<string>;\n readonly inbound: (path: string) => number;\n}\n\n/**\n * Enforce contextual-link coverage rules over an explicit surface.\n *\n * A rule that matches no sitemap-eligible page is itself a violation: a typo or\n * a rule aimed at a noindex page would otherwise pass silently forever.\n */\nconst checkCoverageRules = (\n surface: CoverageSurface,\n rules: ReadonlyArray<CoverageRule>,\n): Array<Violation> => {\n const violations: Array<Violation> = [];\n for (const rule of rules) {\n const matcher = globToRegExp(rule.path);\n const matched = surface.paths.filter((path) => matcher.test(path));\n const eligible = surface.eligiblePaths.filter((path) => matcher.test(path));\n\n if (eligible.length === 0) {\n violations.push({\n severity: \"structural\",\n rule: \"coverage-rule-unmatched\",\n message:\n matched.length === 0\n ? `Coverage rule \"${rule.path}\" matches no page in the graph.`\n : `Coverage rule \"${rule.path}\" matches only pages that are not sitemap-eligible.`,\n fix: \"Point the rule at a sitemap-eligible path, or drop it.\",\n });\n continue;\n }\n\n for (const path of eligible) {\n const inbound = surface.inbound(path);\n if (inbound >= rule.minInbound) continue;\n violations.push({\n severity: \"structural\",\n rule: \"inbound-link-coverage\",\n path,\n message: `\"${path}\" has ${inbound} incoming contextual link(s); the coverage rule requires ${rule.minInbound}.`,\n fix: `Add ${rule.minInbound - inbound} contextual link(s) pointing at \"${path}\".`,\n });\n }\n }\n return violations;\n};\n\n/** The sitemap-eligible path set of a declared graph, in graph insertion order. */\nconst eligiblePathsOf = (graph: SeoGraph): ReadonlyArray<string> =>\n [...graph.nodes.values()].filter(isSitemapEligible).map((node) => node.path);\n\n/**\n * Enforce contextual-link coverage against the *declared* graph: only `related`\n * edges count — breadcrumb ancestry is navigation, not context.\n */\nexport function checkCoverage(\n graph: SeoGraph,\n rules: ReadonlyArray<CoverageRule>,\n): Array<Violation> {\n return checkCoverageRules(\n {\n paths: [...graph.nodes.keys()],\n eligiblePaths: eligiblePathsOf(graph),\n inbound: (path) =>\n graph.edges.filter((edge) => edge.type === \"related\" && edge.to === path).length,\n },\n rules,\n );\n}\n\n/**\n * Enforce contextual-link coverage against the *rendered* graph: only same-origin\n * body-region anchors count. Nav, header, and footer anchors are site chrome, so\n * a page linked solely from them has no contextual inbound link — the same\n * distinction the declared pass draws by counting `related` edges alone.\n *\n * The target universe is the declared graph's sitemap-eligible pages: the globs\n * in `seo.config.ts` are written against declared paths, and the crawl is what\n * supplies the served edges. Pass the crawl's `internalEdges` (each edge carries\n * its `region`).\n */\nexport function checkRenderedCoverage(\n graph: SeoGraph,\n renderedEdges: ReadonlyArray<LinkEdge>,\n rules: ReadonlyArray<CoverageRule>,\n): Array<Violation> {\n const inbound = new Map<string, number>();\n for (const edge of renderedEdges) {\n if (edge.region !== \"body\") continue;\n inbound.set(edge.to, (inbound.get(edge.to) ?? 0) + 1);\n }\n return checkCoverageRules(\n {\n paths: [...graph.nodes.keys()],\n eligiblePaths: eligiblePathsOf(graph),\n inbound: (path) => inbound.get(path) ?? 0,\n },\n rules,\n );\n}\n\n/** Structural violations fail `pagegraph check`; editorial-only stays green. */\nexport const hasStructuralViolations = (violations: ReadonlyArray<Violation>): boolean =>\n violations.some((violation) => violation.severity === \"structural\");\n","/**\n * Pure candidate generation for contextual cross-links.\n *\n * Given the declared graph, enumerate `(source, destination)` pairs that are\n * *plausible* contextual links and *not already connected*. A pair is plausible\n * when the two pages sit in the same cluster: a shared top-level section (e.g.\n * `/blog/a` and `/blog/b` under `/blog`) or, for root-level pages that share no\n * section, the same `kind`. Only sitemap-eligible pages are candidates — a\n * noindex, redirect, or param template is not a link surface.\n *\n * The plan is a proposal, never an application: nothing here writes a\n * declaration. Effect- and framework-free so it runs in the CLI, a Worker, or a\n * test on the same graph.\n */\n\nimport type { SeoGraph, SeoNode } from \"./graph\";\nimport type { SimpleEdge } from \"./links\";\nimport { isSitemapEligible } from \"./projections\";\n\n/** One proposed contextual link, in the direction the link would be authored. */\nexport interface LinkCandidatePair {\n readonly source: string;\n readonly destination: string;\n /** The cluster the pair shares: a top-level section, or `kind:<kind>`. */\n readonly cluster: string;\n /** Why the pair is plausible, e.g. `same top-level section \"/blog\"`. */\n readonly reason: string;\n}\n\nexport interface LinkCandidateOptions {\n /** Maximum candidates to return after ordering (default 50). */\n readonly limit?: number;\n /** Restrict to these clusters; a filter may be a section or a bare kind. */\n readonly clusters?: ReadonlyArray<string>;\n /** Anchors already served in HTML; their pairs are excluded when supplied. */\n readonly renderedEdges?: ReadonlyArray<SimpleEdge>;\n}\n\n/** One cluster and how many candidate pairs it contributes (before `limit`). */\nexport interface LinkClusterSummary {\n readonly key: string;\n readonly candidates: number;\n}\n\nexport interface LinkCandidateResult {\n readonly candidates: ReadonlyArray<LinkCandidatePair>;\n /** Candidate pairs before `limit` was applied (after cluster filters). */\n readonly total: number;\n readonly truncated: boolean;\n readonly clusters: ReadonlyArray<LinkClusterSummary>;\n}\n\n/**\n * Canonical path key: query and hash stripped, trailing slashes removed, \"/\"\n * preserved. This is the same path key the graph and the rendered-link core\n * use, so a served anchor like `/blog/a?ref=nav` still matches the graph pair\n * `/blog/a`.\n */\nconst normalize = (path: string): string => {\n const pathname = path.split(/[?#]/, 1)[0] ?? \"\";\n const trimmed = pathname.replace(/\\/+$/, \"\");\n return trimmed === \"\" ? \"/\" : trimmed;\n};\n\n/** Directionless pair key, so an edge in either direction means \"connected\". */\nexport const undirectedEdgeKey = (from: string, to: string): string => {\n const [a, b] = [normalize(from), normalize(to)].sort();\n return `${a}\\u0000${b}`;\n};\n\n/** First path segment, or undefined for the root page. */\nconst topSegment = (path: string): string | undefined => {\n const segment = path.split(\"/\").filter(Boolean)[0];\n return segment === undefined || segment === \"\" ? undefined : segment;\n};\n\n/** A page with no nested segment (`/`, `/pricing`, `/blog`) is root-level. */\nconst isRootLevel = (path: string): boolean => path.split(\"/\").filter(Boolean).length <= 1;\n\n/**\n * The cluster a pair shares, or undefined when they share none. Section-first:\n * a shared top-level section wins. The kind fallback is local to root-level\n * pages, so it never pairs two nested pages from different sections.\n */\nconst clusterOf = (a: SeoNode, b: SeoNode): { key: string; reason: string } | undefined => {\n const aSegment = topSegment(a.path);\n const bSegment = topSegment(b.path);\n if (aSegment !== undefined && aSegment === bSegment) {\n return { key: aSegment, reason: `same top-level section \"/${aSegment}\"` };\n }\n if (isRootLevel(a.path) && isRootLevel(b.path) && a.kind === b.kind) {\n return { key: `kind:${a.kind}`, reason: `same kind \"${a.kind}\"` };\n }\n return undefined;\n};\n\n/** A `--cluster` filter matches a section by name, or a kind via its bare label. */\nexport const matchesClusterFilter = (cluster: string, filter: string): boolean => {\n const normalized = filter.replace(/^\\/+/, \"\").toLowerCase();\n const key = cluster.toLowerCase();\n return key === normalized || key === `kind:${normalized}`;\n};\n\n/** Human reason string for a candidate's source page, used when handing a plan to Jev. */\nexport const candidateSourceText = (node: SeoNode): string =>\n node.instance?.description?.trim() ||\n node.instance?.title?.trim() ||\n node.policy.link?.description?.trim() ||\n node.policy.link?.title?.trim() ||\n node.path;\n\n/**\n * Enumerate reviewable contextual-link candidates from the declared graph.\n *\n * Excluded: self-pairs, pages that are not sitemap-eligible, pairs already\n * declared as a `related` edge, and — when {@link LinkCandidateOptions.renderedEdges}\n * is supplied — pairs already rendered as an anchor. Queries, hashes, and\n * trailing slashes are normalized so both sides compare by the graph's path key.\n */\nexport const generateLinkCandidates = (\n graph: SeoGraph,\n options: LinkCandidateOptions = {},\n): LinkCandidateResult => {\n const limit = options.limit ?? 50;\n const filters = options.clusters ?? [];\n const rendered = new Set(\n (options.renderedEdges ?? []).map((edge) => undirectedEdgeKey(edge.from, edge.to)),\n );\n const declared = new Set(\n graph.edges\n .filter((edge) => edge.type === \"related\")\n .map((edge) => undirectedEdgeKey(edge.from, edge.to)),\n );\n\n const eligible = [...graph.nodes.values()].filter(isSitemapEligible);\n\n const all: Array<LinkCandidatePair> = [];\n for (let i = 0; i < eligible.length; i++) {\n for (let j = i + 1; j < eligible.length; j++) {\n const a = eligible[i]!;\n const b = eligible[j]!;\n const cluster = clusterOf(a, b);\n if (cluster === undefined) continue;\n if (filters.length > 0 && !filters.some((filter) => matchesClusterFilter(cluster.key, filter))) {\n continue;\n }\n const key = undirectedEdgeKey(a.path, b.path);\n if (declared.has(key) || rendered.has(key)) continue;\n all.push({ source: a.path, destination: b.path, cluster: cluster.key, reason: cluster.reason });\n all.push({ source: b.path, destination: a.path, cluster: cluster.key, reason: cluster.reason });\n }\n }\n\n const compare = (x: LinkCandidatePair, y: LinkCandidatePair): number =>\n x.cluster < y.cluster\n ? -1\n : x.cluster > y.cluster\n ? 1\n : x.source < y.source\n ? -1\n : x.source > y.source\n ? 1\n : x.destination < y.destination\n ? -1\n : x.destination > y.destination\n ? 1\n : 0;\n all.sort(compare);\n\n const counts = new Map<string, number>();\n for (const pair of all) counts.set(pair.cluster, (counts.get(pair.cluster) ?? 0) + 1);\n\n const candidates = all.slice(0, limit);\n return {\n candidates,\n total: all.length,\n truncated: all.length > candidates.length,\n clusters: [...counts.entries()].map(([key, count]) => ({ key, candidates: count })),\n };\n};\n\n/**\n * Decode a rendered-edge dump supplied to the CLI: either a bare array of\n * `{ from, to }` edges, or an object carrying `edges` (or `internalEdges`).\n * Throws on any other shape — this is user input, not a provider payload.\n */\nexport const decodeRenderedEdges = (input: unknown): ReadonlyArray<SimpleEdge> => {\n const array =\n Array.isArray(input)\n ? input\n : input !== null && typeof input === \"object\" && Array.isArray((input as { edges?: unknown }).edges)\n ? (input as { edges: ReadonlyArray<unknown> }).edges\n : input !== null &&\n typeof input === \"object\" &&\n Array.isArray((input as { internalEdges?: unknown }).internalEdges)\n ? (input as { internalEdges: ReadonlyArray<unknown> }).internalEdges\n : undefined;\n\n if (array === undefined) {\n throw new Error(\n \"expected an array of { from, to } edges, or an object with an `edges` array\",\n );\n }\n\n return array.map((item) => {\n if (item === null || typeof item !== \"object\") {\n throw new Error(\"each rendered edge must be an object with string `from` and `to`\");\n }\n const { from, to } = item as { from?: unknown; to?: unknown };\n if (typeof from !== \"string\" || typeof to !== \"string\") {\n throw new Error(\"each rendered edge must have string `from` and `to`\");\n }\n return { from, to };\n });\n};\n","/**\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","import type { AnyRoute, AnyRouter } from \"@tanstack/react-router\";\n\nimport type { RouteSeo } from \"./declare\";\n\ninterface RouteMaps {\n routesByPath: Record<string, AnyRoute | undefined>;\n routesById: Record<string, AnyRoute>;\n}\n\n/**\n * Resolve a route's declared `seo.link` card (title + description) by its full\n * path.\n *\n * `useRouter()` returns a Router typed to this app's exact route tree, so\n * `routesByPath` is keyed by the literal `FileRouteTypes[\"fullPaths\"]` union —\n * but callers here (declared `related` targets) hold arbitrary runtime path\n * strings, not that literal type. `routesByPath` and `routesById` are plain\n * Records on every Router instance regardless of which route tree it's\n * parameterized over, so this narrows to that structural shape once, at this\n * boundary, instead of threading an `AnyRoute` cast through every call site.\n */\nexport function resolveRouteLink(router: AnyRouter, path: string): RouteSeo[\"link\"] {\n const { routesByPath, routesById } = router as unknown as RouteMaps;\n\n const direct = routesByPath[path];\n if (direct) return direct.options.staticData?.seo?.link;\n\n const byFullPath = Object.values(routesById).find((route) => route.fullPath === path);\n return byFullPath?.options.staticData?.seo?.link;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/core/graph.ts","../src/core/projections.ts","../src/core/checks.ts","../src/core/link-candidates.ts","../src/core/inspect-html.ts","../src/core/resolve-route-link.ts"],"sourcesContent":["/**\n * SEO graph — the derived model that projections (sitemap, robots), the CLI, and\n * the check engine all read from. Built from route declarations (`staticData.seo`)\n * plus consumer-supplied content collections. This module is pure: no React, no\n * env, no knowledge of where instances come from. Origins and env-derived values\n * are injected by the callers of the projections, never read here.\n *\n * A node is one of:\n * - a structural route (`source: \"route\"`), keyed by its normalized full path,\n * merging a layout route's declaration (crumb) with its index child's (kind,\n * sitemap policy) when both resolve to the same URL;\n * - a content instance (`source` = the collection's label), keyed by the page URL\n * and carrying page-level metadata.\n *\n * Edges: `crumb-parent` (breadcrumb ancestry), `related` (deliberate cross-links),\n * `collection-member` (membership in a curated set), and `redirect` (route aliases).\n * The route walk emits crumb/related/redirect edges from declarations; a collection\n * may declare any additional edges its instances need.\n */\n\nimport type { AnyRoute } from \"@tanstack/react-router\";\n\nimport type { PublicPath, RouteSeo, SeoKind } from \"./declare\";\n\n/**\n * Where a node came from: `\"route\"` for a structural route declaration, or the\n * `source` label of the collection that produced the instance (e.g. \"blog\").\n */\nexport type SeoSource = string;\n\nexport interface SeoNode {\n /** Canonical path, no origin (e.g. \"/pricing\", \"/blog/my-post\"). */\n path: string;\n kind: SeoKind;\n source: SeoSource;\n /** Route-declared policy, or synthesized (kind + inherited sitemap) for instances. */\n policy: RouteSeo;\n instance?:\n | {\n title: string;\n description?: string | undefined;\n publishedAt?: string | undefined;\n modifiedAt?: string | undefined;\n }\n | undefined;\n}\n\nexport type SeoEdgeType = \"crumb-parent\" | \"related\" | \"redirect\" | \"collection-member\";\n\nexport interface SeoEdge {\n from: string;\n to: string;\n type: SeoEdgeType;\n}\n\nexport interface SeoGraph {\n nodes: Map<string, SeoNode>;\n edges: Array<SeoEdge>;\n /** Exact-path ownership conflicts encountered while assembling graph sources. */\n collisions?: ReadonlyArray<{ path: string; sources: ReadonlyArray<SeoSource> }> | undefined;\n}\n\n/** One concrete page produced by a collection. */\nexport interface SeoInstance {\n /** Canonical path of the page (e.g. \"/blog/my-post\"). */\n readonly path: string;\n readonly title: string;\n readonly description?: string | undefined;\n readonly publishedAt?: string | undefined;\n readonly modifiedAt?: string | undefined;\n}\n\nexport interface SeoCollection {\n /**\n * The param route these instances render through (e.g. \"/blog/$slug\"). Instances\n * inherit this route's declared policy (kind + sitemap) — the graph reads it from\n * the structural node, so declarations stay the single source of truth.\n */\n readonly route: PublicPath;\n /** The `source` stamped on every node this collection produces (e.g. \"blog\"). */\n readonly source: SeoSource;\n readonly instances: ReadonlyArray<SeoInstance>;\n /** Edges the collection declares between its instances and the rest of the graph. */\n readonly edges?: ReadonlyArray<SeoEdge> | undefined;\n}\n\nexport interface BuildSeoGraphInput {\n readonly routeTree: AnyRoute;\n readonly collections?: ReadonlyArray<SeoCollection> | undefined;\n}\n\n/** Kind for instances whose collection route carries no declaration to inherit. */\nconst FALLBACK_KIND: SeoKind = \"page\";\n\n/** Structural view of a route we walk — the fields present before router init(). */\ninterface WalkableRoute {\n readonly options: {\n readonly path?: string | undefined;\n readonly staticData?: { readonly seo?: RouteSeo | undefined } | undefined;\n };\n readonly children?: ReadonlyArray<WalkableRoute> | undefined;\n}\n\n/**\n * Join a child's local path onto its parent's computed full path with the same\n * semantics as TanStack's route init (relative segments, index \"/\" inherits the\n * parent, pathless/group routes are transparent), then normalize trailing slashes.\n */\nfunction joinPath(parent: string, seg: string | undefined): string {\n if (seg === undefined) return parent; // pathless layout / route group\n if (seg === \"/\") return parent; // index route resolves to its parent's URL\n const trimmed = seg.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\");\n const joined = `${parent === \"/\" ? \"\" : parent}/${trimmed}`;\n return joined.replace(/\\/{2,}/g, \"/\");\n}\n\n/** Merge a route's declaration into an existing same-path node (deeper route wins). */\nfunction mergeSeo(base: RouteSeo, override: RouteSeo): RouteSeo {\n return {\n kind: override.kind,\n crumb: override.crumb ?? base.crumb,\n sitemap: override.sitemap ?? base.sitemap,\n robots: override.robots ?? base.robots,\n related: override.related ?? base.related,\n link: override.link ?? base.link,\n redirectTo: override.redirectTo ?? base.redirectTo,\n };\n}\n\n/**\n * Walk the route tree building structural nodes and crumb-parent edges. `crumbStack`\n * holds the paths of crumb-declaring ancestors so each crumb node links to its nearest\n * crumb ancestor down the real route-parent chain (matching render-time breadcrumbs).\n */\nfunction walkRoutes(\n route: WalkableRoute,\n parentPath: string,\n isRoot: boolean,\n nodes: Map<string, SeoNode>,\n edges: Array<SeoEdge>,\n crumbStack: Array<string>,\n): void {\n const path = isRoot ? \"/\" : joinPath(parentPath, route.options.path);\n const seo = route.options.staticData?.seo;\n\n if (seo) {\n const existing = nodes.get(path);\n if (existing) {\n existing.policy = mergeSeo(existing.policy, seo);\n existing.kind = existing.policy.kind;\n } else {\n nodes.set(path, { path, kind: seo.kind, source: \"route\", policy: { ...seo } });\n }\n\n const nearestCrumbAncestor = crumbStack[crumbStack.length - 1];\n if (seo.crumb !== undefined && nearestCrumbAncestor !== undefined) {\n edges.push({ from: path, to: nearestCrumbAncestor, type: \"crumb-parent\" });\n }\n }\n\n const pushedCrumb = seo?.crumb !== undefined;\n if (pushedCrumb) crumbStack.push(path);\n for (const child of route.children ?? []) {\n walkRoutes(child, path, false, nodes, edges, crumbStack);\n }\n if (pushedCrumb) crumbStack.pop();\n}\n\n/**\n * Add one collection's instance nodes, inheriting kind + sitemap policy from the\n * collection route's declaration, then append the edges it declares. A path already\n * owned by another node is a collision: the first owner keeps the path and the\n * conflict is reported (the `path-owner-collision` check turns it into a violation).\n */\nfunction addCollection(\n nodes: Map<string, SeoNode>,\n edges: Array<SeoEdge>,\n collisions: Array<{ path: string; sources: ReadonlyArray<SeoSource> }>,\n collection: SeoCollection,\n): void {\n const collectionNode = nodes.get(collection.route);\n const kind = collectionNode?.kind ?? FALLBACK_KIND;\n const sitemap = collectionNode?.policy.sitemap;\n\n /**\n * Paths this collection lost to an earlier owner. Their instances never enter\n * the graph, so any edge declared out of them would dangle — and the dead-edge\n * check only validates an edge's `to`, so nothing downstream would catch it.\n */\n const rejected = new Set<string>();\n\n for (const instance of collection.instances) {\n const existing = nodes.get(instance.path);\n if (existing) {\n collisions.push({ path: instance.path, sources: [existing.source, collection.source] });\n rejected.add(instance.path);\n continue;\n }\n nodes.set(instance.path, {\n path: instance.path,\n kind,\n source: collection.source,\n policy: { kind, sitemap },\n instance: {\n title: instance.title,\n description: instance.description,\n publishedAt: instance.publishedAt,\n modifiedAt: instance.modifiedAt,\n },\n });\n }\n\n for (const edge of collection.edges ?? []) {\n if (rejected.has(edge.from)) continue;\n edges.push(edge);\n }\n}\n\n/**\n * Build the SEO graph from route declarations and content collections.\n *\n * Synchronous: the caller materializes its collections before calling, so there is\n * no async work here. Callers own the origin.\n */\nexport function buildSeoGraph(input: BuildSeoGraphInput): SeoGraph {\n const nodes = new Map<string, SeoNode>();\n const edges: Array<SeoEdge> = [];\n const collisions: Array<{ path: string; sources: ReadonlyArray<SeoSource> }> = [];\n\n walkRoutes(input.routeTree as unknown as WalkableRoute, \"/\", true, nodes, edges, []);\n\n for (const collection of input.collections ?? []) {\n addCollection(nodes, edges, collisions, collection);\n }\n\n for (const node of nodes.values()) {\n if (node.source !== \"route\") continue;\n for (const to of node.policy.related ?? []) {\n edges.push({ from: node.path, to, type: \"related\" });\n }\n if (node.policy.redirectTo !== undefined) {\n edges.push({ from: node.path, to: node.policy.redirectTo, type: \"redirect\" });\n }\n }\n\n return { nodes, edges, collisions };\n}\n","/**\n * Projections of the SEO graph: sitemap.xml, robots.txt, and single-node\n * inspection. Pure functions — the origin, the host's indexability, and the\n * robots disallow list are injected by the caller, never read from the\n * environment or a generated file.\n */\n\nimport type { SeoEdge, SeoGraph, SeoNode } from \"./graph\";\n\nexport interface ProjectionConfig {\n origin: string;\n indexable: boolean;\n}\n\nexport interface RobotsConfig extends ProjectionConfig {\n /** Path prefixes to disallow on an indexable host (e.g. the app-only groups). */\n disallow: ReadonlyArray<string>;\n /**\n * Origin-wide Content-Signal preferences\n * (https://contentsignals.org/), emitted as `Content-Signal: <value>` under\n * `User-agent: *` on an indexable host. Omit for none. The plugin never\n * invents a default — pass the policy you want, e.g.\n * `\"search=yes, ai-input=yes, ai-train=yes\"`.\n */\n contentSignal?: string | undefined;\n /**\n * Extra full lines in the indexable `User-agent: *` group, after\n * Content-Signal (if any) and before Allow/Disallow. Use for directives the\n * plugin does not model, or to compose {@link contentSignal} yourself.\n */\n directives?: ReadonlyArray<string> | undefined;\n /**\n * Last-mile override: receives the rendered robots.txt and returns the file\n * to emit. Runs for indexable and preview hosts. Use when you need to wrap\n * or replace the default body rather than add group lines.\n */\n transform?: ((robots: string) => string) | undefined;\n}\n\nexport interface NodeReport {\n node: SeoNode;\n inSitemap: boolean;\n incoming: Array<SeoEdge>;\n outgoing: Array<SeoEdge>;\n}\n\nconst escapeXml = (value: string): string =>\n value\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n\n/**\n * A node belongs in the sitemap when it declares a positive sitemap policy, is not\n * a redirect, is not robots-noindexed, and is not a param template (a route whose\n * path still contains a `$` segment — those exist only so their instances inherit).\n */\nexport function isSitemapEligible(node: SeoNode): boolean {\n if (node.policy.redirectTo !== undefined) return false;\n if (node.policy.robots?.includes(\"noindex\")) return false;\n if (!node.policy.sitemap) return false; // false or absent\n if (node.path.includes(\"$\")) return false;\n return true;\n}\n\n/** Canonical absolute URL for a node under the given origin. */\nfunction urlForNode(origin: string, node: SeoNode): string {\n return node.path === \"/\" ? origin : `${origin}${node.path}`;\n}\n\n/**\n * Instance lastmod: the most recent date the page's frontmatter carries. A\n * collection whose instances carry no dates (docs, a manifest-driven gallery)\n * emits no `<lastmod>` at all.\n */\nfunction instanceLastmod(node: SeoNode): string | undefined {\n const instance = node.instance;\n if (!instance) return undefined;\n const date = instance.modifiedAt ?? instance.publishedAt;\n return date ? new Date(date).toISOString() : undefined;\n}\n\nfunction renderUrlEntry(url: string, lastmod: string | undefined, node: SeoNode): string {\n const sitemap = node.policy.sitemap;\n // isSitemapEligible guarantees a positive policy before this runs.\n const { changeFrequency, priority } = sitemap as { changeFrequency: string; priority: number };\n const lines = [` <url>`, ` <loc>${escapeXml(url)}</loc>`];\n if (lastmod) lines.push(` <lastmod>${lastmod}</lastmod>`);\n lines.push(\n ` <changefreq>${changeFrequency}</changefreq>`,\n ` <priority>${priority.toFixed(1)}</priority>`,\n ` </url>`,\n );\n return lines.join(\"\\n\");\n}\n\n/**\n * Render sitemap.xml from the graph. Structural route entries emit no `<lastmod>`\n * (a route has no publish date); content instances emit it from their frontmatter.\n * Route entries are sorted by path, then instances follow in collection order.\n * `indexable` is intentionally unused — the sitemap body is host-independent;\n * robots.txt is what gates crawling.\n */\nexport function renderSitemap(graph: SeoGraph, cfg: ProjectionConfig): string {\n const nodes = [...graph.nodes.values()];\n const staticNodes = nodes\n .filter((node) => node.source === \"route\" && isSitemapEligible(node))\n .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));\n const instanceNodes = nodes.filter((node) => node.source !== \"route\" && isSitemapEligible(node));\n\n const seen = new Set<string>();\n const entries: Array<string> = [];\n for (const node of [...staticNodes, ...instanceNodes]) {\n const url = urlForNode(cfg.origin, node);\n const key = url.toLowerCase().replace(/\\/$/, \"\");\n if (seen.has(key)) continue;\n seen.add(key);\n entries.push(renderUrlEntry(url, instanceLastmod(node), node));\n }\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\\n${entries.join(\n \"\\n\",\n )}\\n</urlset>\\n`;\n}\n\n/** Format a Content-Signal robots.txt directive from the preference list. */\nexport function contentSignal(value: string): string {\n return `Content-Signal: ${value}`;\n}\n\nconst groupLines = (cfg: RobotsConfig): Array<string> => {\n const lines: Array<string> = [];\n if (cfg.contentSignal !== undefined && cfg.contentSignal !== \"\") {\n lines.push(contentSignal(cfg.contentSignal));\n }\n for (const directive of cfg.directives ?? []) {\n if (directive !== \"\") lines.push(directive);\n }\n return lines;\n};\n\n/**\n * Render robots.txt. A non-indexable host (previews) gets a disallow-all\n * with no Sitemap line; an indexable host disallows exactly the prefixes the caller\n * passes. Pages that declare `robots: noindex` are intentionally NOT added as\n * Disallow entries — a Disallow would stop crawlers reaching the page to read its\n * `noindex, follow` meta, so the graph's per-node robots policy never feeds this\n * list. `graph` is unused — kept for signature parity with the other projections,\n * which callers load the graph once for and pass to each.\n *\n * Origin-wide group directives (`contentSignal`, `directives`) are indexable-host\n * only. {@link RobotsConfig.transform} always runs last so a consumer can override\n * the whole file.\n */\nexport function renderRobots(_graph: SeoGraph, cfg: RobotsConfig): string {\n const rendered = cfg.indexable\n ? [\n \"User-agent: *\",\n ...groupLines(cfg),\n \"Allow: /\",\n ...cfg.disallow.map((path) => `Disallow: ${path}`),\n \"\",\n `Sitemap: ${cfg.origin}/sitemap.xml`,\n `Host: ${cfg.origin}`,\n \"\",\n ].join(\"\\n\")\n : [\"User-agent: *\", \"Disallow: /\", \"\"].join(\"\\n\");\n return cfg.transform === undefined ? rendered : cfg.transform(rendered);\n}\n\n/** Inspect a single node: its declaration, sitemap eligibility, and edges. */\nexport function inspectNode(graph: SeoGraph, path: string): NodeReport | undefined {\n const node = graph.nodes.get(path);\n if (!node) return undefined;\n return {\n node,\n inSitemap: isSitemapEligible(node),\n incoming: graph.edges.filter((edge) => edge.to === path),\n outgoing: graph.edges.filter((edge) => edge.from === path),\n };\n}\n","/**\n * The SEO check engine: a set of rules run against the derived {@link SeoGraph}.\n * Pure — no I/O, no `clientEnv`, no React. The CLI's `pagegraph check` command and the\n * vitest suite both call {@link checkGraph}; nothing else derives correctness.\n *\n * Two severities, mapped to the CLI's exit contract:\n * - `structural` — a declaration is internally broken (a link points nowhere, a\n * card would render empty, a page contradicts its own robots/sitemap intent).\n * Any structural violation fails `pagegraph check` (exit 1); these must not ship.\n * - `editorial` — a quality smell (duplicate or mis-sized titles/descriptions).\n * Reported as warnings; `pagegraph check` still exits 0 when only these are present.\n *\n * The graph only knows what declarations and frontmatter carry, so the rules are\n * scoped to that: per-node title/description live only on collection *instances*,\n * never on structural route nodes (whose head tags are composed at render time and\n * are not in the graph). Rules are data-driven and listed once in\n * {@link CHECK_RULES}.\n */\n\nimport type { SitemapPolicy } from \"./declare\";\nimport type { SeoGraph, SeoNode } from \"./graph\";\nimport type { LinkEdge } from \"./links\";\nimport { isSitemapEligible } from \"./projections\";\n\nexport type Severity = \"structural\" | \"editorial\";\n\nexport interface Violation {\n severity: Severity;\n rule: string;\n path?: string | undefined;\n message: string;\n fix?: string | undefined;\n}\n\n/**\n * A contextual-link coverage rule: every sitemap-eligible page matching the\n * `path` glob must have at least `minInbound` incoming contextual links. Unlike\n * the static rules, these come from project policy (a CLI flag or\n * `seo.config.ts`), because \"which pages are money pages\" is app knowledge.\n *\n * The same rule shape is evaluated twice: `checkCoverage` counts declared\n * `related` edges, and `checkRenderedCoverage` counts the contextual anchors a\n * crawl actually served. The target universe is always the declared graph's\n * sitemap-eligible pages — the rendered pass only swaps the edge source.\n */\nexport interface CoverageRule {\n /** Path glob: `*` matches within a segment, `**` matches across segments. */\n readonly path: string;\n /** Minimum incoming contextual links the matched page needs. */\n readonly minInbound: number;\n}\n\n/** A single finding before its rule's `severity`/`rule` name are attached. */\ninterface RawViolation {\n path?: string | undefined;\n message: string;\n fix?: string | undefined;\n}\n\ninterface CheckRule {\n readonly name: string;\n readonly severity: Severity;\n readonly evaluate: (graph: SeoGraph) => ReadonlyArray<RawViolation>;\n}\n\n/** A positive sitemap policy — the author asked for this page to be indexed. */\nconst hasPositiveSitemap = (sitemap: SitemapPolicy | false | undefined): sitemap is SitemapPolicy =>\n sitemap !== undefined && sitemap !== false;\n\n/** Content and manifest instances carry page-level title/description. */\nconst isInstance = (node: SeoNode): boolean => node.source !== \"route\";\n\n/** Group instance nodes by a present, non-empty string field for duplicate detection. */\nconst groupInstancesBy = (\n graph: SeoGraph,\n field: (node: SeoNode) => string | undefined,\n): Map<string, Array<SeoNode>> => {\n const groups = new Map<string, Array<SeoNode>>();\n for (const node of graph.nodes.values()) {\n if (!isInstance(node)) continue;\n const value = field(node)?.trim();\n if (!value) continue;\n const bucket = groups.get(value);\n if (bucket) bucket.push(node);\n else groups.set(value, [node]);\n }\n return groups;\n};\n\nconst DESCRIPTION_MAX = 160;\nconst DESCRIPTION_MIN = 50;\n\n/**\n * Every check, in one place. `checkGraph` runs them in order and stamps each\n * finding with its rule name and severity, so the output is grouped by rule and\n * deterministic (nodes iterate in graph insertion order, edges in array order).\n */\nconst CHECK_RULES: ReadonlyArray<CheckRule> = [\n {\n name: \"path-owner-collision\",\n severity: \"structural\",\n evaluate: (graph) =>\n (graph.collisions ?? []).map((collision) => ({\n path: collision.path,\n message: `Canonical path \"${collision.path}\" is owned by multiple sources: ${collision.sources.join(\", \")}.`,\n fix: \"Give every concrete page one canonical path and one graph owner.\",\n })),\n },\n {\n name: \"canonical-path-collision\",\n severity: \"structural\",\n evaluate: (graph) => {\n const groups = new Map<string, Array<string>>();\n for (const path of graph.nodes.keys()) {\n const canonical = path.toLowerCase().replace(/\\/$/, \"\") || \"/\";\n const paths = groups.get(canonical);\n if (paths) paths.push(path);\n else groups.set(canonical, [path]);\n }\n return [...groups.values()]\n .filter((paths) => paths.length > 1)\n .flatMap((paths) =>\n paths.map((path) => ({\n path,\n message: `Canonical path collides with ${paths.filter((candidate) => candidate !== path).join(\", \")}.`,\n fix: \"Use one lowercase, trailing-slash-normalized canonical path.\",\n })),\n );\n },\n },\n {\n name: \"self-edge\",\n severity: \"structural\",\n evaluate: (graph) =>\n graph.edges\n .filter((edge) => edge.from === edge.to)\n .map((edge) => ({\n path: edge.from,\n message: `${edge.type} edge points back to its own source node.`,\n fix: \"Remove the self-reference from the canonical manifest or route declaration.\",\n })),\n },\n {\n name: \"duplicate-edge\",\n severity: \"structural\",\n evaluate: (graph) => {\n const seen = new Set<string>();\n const duplicates: Array<RawViolation> = [];\n for (const edge of graph.edges) {\n const key = `${edge.from}\\u0000${edge.to}\\u0000${edge.type}`;\n if (seen.has(key)) {\n duplicates.push({\n path: edge.from,\n message: `Duplicate ${edge.type} edge from \"${edge.from}\" to \"${edge.to}\".`,\n fix: \"Declare each graph relationship exactly once.\",\n });\n } else {\n seen.add(key);\n }\n }\n return duplicates;\n },\n },\n {\n // A `related` or `redirect` edge points at a path with no node — the target\n // route/page was renamed or deleted and the declaration wasn't updated.\n name: \"dead-edge\",\n severity: \"structural\",\n evaluate: (graph) =>\n graph.edges\n .filter((edge) => edge.type !== \"crumb-parent\" && !graph.nodes.has(edge.to))\n .map((edge) => ({\n path: edge.from,\n message: `${edge.type} edge from \"${edge.from}\" points at \"${edge.to}\", which is not a node in the graph.`,\n fix: `Update the ${edge.type === \"redirect\" ? \"redirectTo\" : \"related\"} target on the \"${edge.from}\" route, or restore \"${edge.to}\".`,\n })),\n },\n {\n // A content instance with no usable title can't render a legible <title> or card.\n name: \"instance-missing-title\",\n severity: \"structural\",\n evaluate: (graph) =>\n [...graph.nodes.values()]\n .filter((node) => isInstance(node) && !node.instance?.title.trim())\n .map((node) => ({\n path: node.path,\n message: `Content page \"${node.path}\" has no title.`,\n fix: \"Add a `title` to the page frontmatter.\",\n })),\n },\n {\n // The declaration asks for the page to be in the sitemap yet also marks it\n // noindex — contradictory intent. The projection resolves it (noindex wins,\n // excluded), but the declaration should say one thing.\n name: \"sitemap-noindex-contradiction\",\n severity: \"structural\",\n evaluate: (graph) =>\n [...graph.nodes.values()]\n .filter(\n (node) =>\n hasPositiveSitemap(node.policy.sitemap) &&\n node.policy.robots?.toLowerCase().includes(\"noindex\"),\n )\n .map((node) => ({\n path: node.path,\n message: `\"${node.path}\" declares a sitemap policy but its robots value is \"${node.policy.robots}\".`,\n fix: \"Drop the sitemap policy (or set `sitemap: false`) on a noindex page, or remove the noindex robots value.\",\n })),\n },\n {\n // Robots declarations are house-convention lowercase: the sitemap projection\n // matches `includes(\"noindex\")` literally, so a miscased value (\"Noindex\")\n // would silently stay sitemap-eligible. This gate makes that unrepresentable.\n name: \"robots-not-lowercase\",\n severity: \"structural\",\n evaluate: (graph) =>\n [...graph.nodes.values()]\n .filter(\n (node) =>\n node.policy.robots !== undefined &&\n node.policy.robots !== node.policy.robots.toLowerCase(),\n )\n .map((node) => ({\n path: node.path,\n message: `\"${node.path}\" declares robots \"${node.policy.robots}\" — robots values must be lowercase.`,\n fix: 'Lowercase the robots declaration (e.g. \"noindex, follow\").',\n })),\n },\n {\n // A `related` card for a route target renders from that route's `link`\n // metadata; without it the card has no title/description and renders empty.\n // (Content-instance targets render from their frontmatter, so they're exempt.)\n name: \"related-target-missing-link\",\n severity: \"structural\",\n evaluate: (graph) => {\n const linklessTargets = new Set<string>();\n for (const edge of graph.edges) {\n if (edge.type !== \"related\") continue;\n const target = graph.nodes.get(edge.to);\n if (target && target.source === \"route\" && target.policy.link === undefined) {\n linklessTargets.add(edge.to);\n }\n }\n return [...linklessTargets].map((path) => ({\n path,\n message: `Route \"${path}\" is a related-link target but declares no link metadata; its card would render empty.`,\n fix: `Add \\`link: { title, description }\\` to the \"${path}\" route's staticData.seo.`,\n }));\n },\n },\n {\n // A redirect/alias node should never advertise itself in the sitemap.\n name: \"redirect-in-sitemap\",\n severity: \"structural\",\n evaluate: (graph) =>\n [...graph.nodes.values()]\n .filter(\n (node) => node.policy.redirectTo !== undefined && hasPositiveSitemap(node.policy.sitemap),\n )\n .map((node) => ({\n path: node.path,\n message: `Redirect \"${node.path}\" (→ \"${node.policy.redirectTo}\") also declares a sitemap policy.`,\n fix: \"Remove the sitemap policy from the redirect route; only its target belongs in the sitemap.\",\n })),\n },\n {\n name: \"duplicate-title\",\n severity: \"editorial\",\n evaluate: (graph) =>\n [...groupInstancesBy(graph, (node) => node.instance?.title).entries()]\n .filter(([, nodes]) => nodes.length > 1)\n .flatMap(([title, nodes]) =>\n nodes.map((node) => ({\n path: node.path,\n message: `Title \"${title}\" is shared by ${nodes.length} pages.`,\n fix: \"Give each page a distinct title.\",\n })),\n ),\n },\n {\n name: \"duplicate-description\",\n severity: \"editorial\",\n evaluate: (graph) =>\n [...groupInstancesBy(graph, (node) => node.instance?.description).entries()]\n .filter(([, nodes]) => nodes.length > 1)\n .flatMap(([, nodes]) =>\n nodes.map((node) => ({\n path: node.path,\n message: `Description is shared by ${nodes.length} pages.`,\n fix: \"Write a distinct meta description for each page.\",\n })),\n ),\n },\n {\n // Meta descriptions outside ~50–160 chars either get truncated in SERPs or\n // read as too thin.\n name: \"description-length\",\n severity: \"editorial\",\n evaluate: (graph) =>\n [...graph.nodes.values()].flatMap((node) => {\n if (!isInstance(node)) return [];\n const description = node.instance?.description?.trim();\n if (!description) return [];\n if (description.length > DESCRIPTION_MAX) {\n return [\n {\n path: node.path,\n message: `Description is ${description.length} chars (max ${DESCRIPTION_MAX}); it will be truncated in results.`,\n fix: `Trim the description to ${DESCRIPTION_MAX} characters or fewer.`,\n },\n ];\n }\n if (description.length < DESCRIPTION_MIN) {\n return [\n {\n path: node.path,\n message: `Description is ${description.length} chars (min ${DESCRIPTION_MIN}); it reads as thin.`,\n fix: `Expand the description to at least ${DESCRIPTION_MIN} characters.`,\n },\n ];\n }\n return [];\n }),\n },\n];\n\n/**\n * Run every static rule against the graph, then any caller-supplied\n * {@link CoverageRule}s, and return the flat list of violations. With no\n * `coverage` option the result is exactly the static rule set.\n */\nexport function checkGraph(\n graph: SeoGraph,\n options: { readonly coverage?: ReadonlyArray<CoverageRule> | undefined } = {},\n): Array<Violation> {\n const violations: Array<Violation> = CHECK_RULES.flatMap((rule) =>\n rule.evaluate(graph).map((raw): Violation => ({\n severity: rule.severity,\n rule: rule.name,\n path: raw.path,\n message: raw.message,\n fix: raw.fix,\n })),\n );\n const coverage = options.coverage;\n if (coverage !== undefined && coverage.length > 0) {\n violations.push(...checkCoverage(graph, coverage));\n }\n return violations;\n}\n\n/** Escape a glob for `RegExp`, then expand `**`, `*`, and `?` to path-aware forms. */\nconst globToRegExp = (glob: string): RegExp => {\n const escaped = glob.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const source = escaped\n .replace(/\\*\\*/g, \"\\u0000\")\n .replace(/\\*/g, \"[^/]*\")\n .replace(/\\?/g, \"[^/]\")\n .replace(/\\u0000/g, \".*\");\n return new RegExp(`^${source}$`);\n};\n\n/**\n * The minimal view a coverage rule needs: which paths exist (for the glob), which\n * of them are sitemap-eligible (the enforced set), and how many contextual links\n * each receives. Declared and rendered coverage differ only in how this surface\n * is built, so the rule engine itself stays single-sourced.\n */\ninterface CoverageSurface {\n readonly paths: ReadonlyArray<string>;\n readonly eligiblePaths: ReadonlyArray<string>;\n readonly inbound: (path: string) => number;\n}\n\n/**\n * Enforce contextual-link coverage rules over an explicit surface.\n *\n * A rule that matches no sitemap-eligible page is itself a violation: a typo or\n * a rule aimed at a noindex page would otherwise pass silently forever.\n */\nconst checkCoverageRules = (\n surface: CoverageSurface,\n rules: ReadonlyArray<CoverageRule>,\n): Array<Violation> => {\n const violations: Array<Violation> = [];\n for (const rule of rules) {\n const matcher = globToRegExp(rule.path);\n const matched = surface.paths.filter((path) => matcher.test(path));\n const eligible = surface.eligiblePaths.filter((path) => matcher.test(path));\n\n if (eligible.length === 0) {\n violations.push({\n severity: \"structural\",\n rule: \"coverage-rule-unmatched\",\n message:\n matched.length === 0\n ? `Coverage rule \"${rule.path}\" matches no page in the graph.`\n : `Coverage rule \"${rule.path}\" matches only pages that are not sitemap-eligible.`,\n fix: \"Point the rule at a sitemap-eligible path, or drop it.\",\n });\n continue;\n }\n\n for (const path of eligible) {\n const inbound = surface.inbound(path);\n if (inbound >= rule.minInbound) continue;\n violations.push({\n severity: \"structural\",\n rule: \"inbound-link-coverage\",\n path,\n message: `\"${path}\" has ${inbound} incoming contextual link(s); the coverage rule requires ${rule.minInbound}.`,\n fix: `Add ${rule.minInbound - inbound} contextual link(s) pointing at \"${path}\".`,\n });\n }\n }\n return violations;\n};\n\n/** The sitemap-eligible path set of a declared graph, in graph insertion order. */\nconst eligiblePathsOf = (graph: SeoGraph): ReadonlyArray<string> =>\n [...graph.nodes.values()].filter(isSitemapEligible).map((node) => node.path);\n\n/**\n * Enforce contextual-link coverage against the *declared* graph: only `related`\n * edges count — breadcrumb ancestry is navigation, not context.\n */\nexport function checkCoverage(\n graph: SeoGraph,\n rules: ReadonlyArray<CoverageRule>,\n): Array<Violation> {\n return checkCoverageRules(\n {\n paths: [...graph.nodes.keys()],\n eligiblePaths: eligiblePathsOf(graph),\n inbound: (path) =>\n graph.edges.filter((edge) => edge.type === \"related\" && edge.to === path).length,\n },\n rules,\n );\n}\n\n/**\n * Enforce contextual-link coverage against the *rendered* graph: only same-origin\n * body-region anchors count. Nav, header, and footer anchors are site chrome, so\n * a page linked solely from them has no contextual inbound link — the same\n * distinction the declared pass draws by counting `related` edges alone.\n *\n * The target universe is the declared graph's sitemap-eligible pages: the globs\n * in `seo.config.ts` are written against declared paths, and the crawl is what\n * supplies the served edges. Pass the crawl's `internalEdges` (each edge carries\n * its `region`).\n */\nexport function checkRenderedCoverage(\n graph: SeoGraph,\n renderedEdges: ReadonlyArray<LinkEdge>,\n rules: ReadonlyArray<CoverageRule>,\n): Array<Violation> {\n const inbound = new Map<string, number>();\n for (const edge of renderedEdges) {\n if (edge.region !== \"body\") continue;\n inbound.set(edge.to, (inbound.get(edge.to) ?? 0) + 1);\n }\n return checkCoverageRules(\n {\n paths: [...graph.nodes.keys()],\n eligiblePaths: eligiblePathsOf(graph),\n inbound: (path) => inbound.get(path) ?? 0,\n },\n rules,\n );\n}\n\n/** Structural violations fail `pagegraph check`; editorial-only stays green. */\nexport const hasStructuralViolations = (violations: ReadonlyArray<Violation>): boolean =>\n violations.some((violation) => violation.severity === \"structural\");\n","/**\n * Pure candidate generation for contextual cross-links.\n *\n * Given the declared graph, enumerate `(source, destination)` pairs that are\n * *plausible* contextual links and *not already connected*. A pair is plausible\n * when the two pages sit in the same cluster: a shared top-level section (e.g.\n * `/blog/a` and `/blog/b` under `/blog`) or, for root-level pages that share no\n * section, the same `kind`. Only sitemap-eligible pages are candidates — a\n * noindex, redirect, or param template is not a link surface.\n *\n * The plan is a proposal, never an application: nothing here writes a\n * declaration. Effect- and framework-free so it runs in the CLI, a Worker, or a\n * test on the same graph.\n */\n\nimport type { SeoGraph, SeoNode } from \"./graph\";\nimport type { AnchorRegion, SimpleEdge } from \"./links\";\nimport { isSitemapEligible } from \"./projections\";\n\n/** One proposed contextual link, in the direction the link would be authored. */\nexport interface LinkCandidatePair {\n readonly source: string;\n readonly destination: string;\n /** The cluster the pair shares: a top-level section, or `kind:<kind>`. */\n readonly cluster: string;\n /** Why the pair is plausible, e.g. `same top-level section \"/blog\"`. */\n readonly reason: string;\n}\n\nexport interface LinkCandidateOptions {\n /** Maximum candidates to return after ordering (default 50). */\n readonly limit?: number;\n /** Restrict to these clusters; a filter may be a section or a bare kind. */\n readonly clusters?: ReadonlyArray<string>;\n /** Anchors already served in HTML. Only body-region edges exclude their own direction. */\n readonly renderedEdges?: ReadonlyArray<SimpleEdge & { readonly region?: AnchorRegion }>;\n}\n\n/** One cluster and how many candidate pairs it contributes (before `limit`). */\nexport interface LinkClusterSummary {\n readonly key: string;\n readonly candidates: number;\n}\n\nexport interface LinkCandidateResult {\n readonly candidates: ReadonlyArray<LinkCandidatePair>;\n /** Candidate pairs before `limit` was applied (after cluster filters). */\n readonly total: number;\n readonly truncated: boolean;\n readonly clusters: ReadonlyArray<LinkClusterSummary>;\n}\n\n/**\n * Canonical path key: query and hash stripped, trailing slashes removed, \"/\"\n * preserved. This is the same path key the graph and the rendered-link core\n * use, so a served anchor like `/blog/a?ref=nav` still matches the graph pair\n * `/blog/a`.\n */\nconst normalize = (path: string): string => {\n const pathname = path.split(/[?#]/, 1)[0] ?? \"\";\n const trimmed = pathname.replace(/\\/+$/, \"\");\n return trimmed === \"\" ? \"/\" : trimmed;\n};\n\n/** Directionless pair key, so an edge in either direction means \"connected\". */\nexport const undirectedEdgeKey = (from: string, to: string): string => {\n const [a, b] = [normalize(from), normalize(to)].sort();\n return `${a}\\u0000${b}`;\n};\n\nconst directedEdgeKey = (from: string, to: string): string => `${normalize(from)}\\u0000${normalize(to)}`;\n\n/** First path segment, or undefined for the root page. */\nconst topSegment = (path: string): string | undefined => {\n const segment = path.split(\"/\").filter(Boolean)[0];\n return segment === undefined || segment === \"\" ? undefined : segment;\n};\n\n/** A page with no nested segment (`/`, `/pricing`, `/blog`) is root-level. */\nconst isRootLevel = (path: string): boolean => path.split(\"/\").filter(Boolean).length <= 1;\n\n/**\n * The cluster a pair shares, or undefined when they share none. Section-first:\n * a shared top-level section wins. The kind fallback is local to root-level\n * pages, so it never pairs two nested pages from different sections.\n */\nexport const candidateClusterOf = (a: SeoNode, b: SeoNode): { key: string; reason: string } | undefined => {\n const aSegment = topSegment(a.path);\n const bSegment = topSegment(b.path);\n if (aSegment !== undefined && aSegment === bSegment) {\n return { key: aSegment, reason: `same top-level section \"/${aSegment}\"` };\n }\n if (isRootLevel(a.path) && isRootLevel(b.path) && a.kind === b.kind) {\n return { key: `kind:${a.kind}`, reason: `same kind \"${a.kind}\"` };\n }\n return undefined;\n};\n\n/** A `--cluster` filter matches a section by name, or a kind via its bare label. */\nexport const matchesClusterFilter = (cluster: string, filter: string): boolean => {\n const normalized = filter.replace(/^\\/+/, \"\").toLowerCase();\n const key = cluster.toLowerCase();\n return key === normalized || key === `kind:${normalized}`;\n};\n\n/** Human reason string for a candidate's source page, used when handing a plan to Jev. */\nexport const candidateSourceText = (node: SeoNode): string =>\n node.instance?.description?.trim() ||\n node.instance?.title?.trim() ||\n node.policy.link?.description?.trim() ||\n node.policy.link?.title?.trim() ||\n node.path;\n\n/**\n * Enumerate reviewable contextual-link candidates from the declared graph.\n *\n * Excluded: self-pairs, pages that are not sitemap-eligible, pairs already\n * declared as a `related` edge, and — when {@link LinkCandidateOptions.renderedEdges}\n * is supplied — pairs already rendered as an anchor. Queries, hashes, and\n * trailing slashes are normalized so both sides compare by the graph's path key.\n */\nexport const generateLinkCandidates = (\n graph: SeoGraph,\n options: LinkCandidateOptions = {},\n): LinkCandidateResult => {\n const limit = options.limit ?? 50;\n const filters = options.clusters ?? [];\n // Legacy bare edges have no region. Treat them as body links so a v1 edge dump\n // remains conservative; region-aware artifacts retain nav/header/footer edges.\n const rendered = new Set(\n (options.renderedEdges ?? []).filter((edge) => edge.region === undefined || edge.region === \"body\")\n .map((edge) => directedEdgeKey(edge.from, edge.to)),\n );\n const declared = new Set(\n graph.edges\n .filter((edge) => edge.type === \"related\")\n .map((edge) => directedEdgeKey(edge.from, edge.to)),\n );\n\n const eligible = [...graph.nodes.values()].filter(isSitemapEligible);\n\n const all: Array<LinkCandidatePair> = [];\n for (let i = 0; i < eligible.length; i++) {\n for (let j = i + 1; j < eligible.length; j++) {\n const a = eligible[i]!;\n const b = eligible[j]!;\n const cluster = candidateClusterOf(a, b);\n if (cluster === undefined) continue;\n if (filters.length > 0 && !filters.some((filter) => matchesClusterFilter(cluster.key, filter))) {\n continue;\n }\n for (const [source, destination] of [[a.path, b.path], [b.path, a.path]]) {\n const key = directedEdgeKey(source!, destination!);\n if (declared.has(key) || rendered.has(key)) continue;\n all.push({ source: source!, destination: destination!, cluster: cluster.key, reason: cluster.reason });\n }\n }\n }\n\n const compare = (x: LinkCandidatePair, y: LinkCandidatePair): number =>\n x.cluster < y.cluster\n ? -1\n : x.cluster > y.cluster\n ? 1\n : x.source < y.source\n ? -1\n : x.source > y.source\n ? 1\n : x.destination < y.destination\n ? -1\n : x.destination > y.destination\n ? 1\n : 0;\n all.sort(compare);\n\n const counts = new Map<string, number>();\n for (const pair of all) counts.set(pair.cluster, (counts.get(pair.cluster) ?? 0) + 1);\n\n const candidates = all.slice(0, limit);\n return {\n candidates,\n total: all.length,\n truncated: all.length > candidates.length,\n clusters: [...counts.entries()].map(([key, count]) => ({ key, candidates: count })),\n };\n};\n\n/**\n * Decode a rendered-edge dump supplied to the CLI: either a bare array of\n * `{ from, to }` edges, or an object carrying `edges` (or `internalEdges`).\n * Throws on any other shape — this is user input, not a provider payload.\n */\nexport const decodeRenderedEdges = (input: unknown): ReadonlyArray<SimpleEdge & { readonly region?: AnchorRegion }> => {\n const array =\n Array.isArray(input)\n ? input\n : input !== null && typeof input === \"object\" && Array.isArray((input as { edges?: unknown }).edges)\n ? (input as { edges: ReadonlyArray<unknown> }).edges\n : input !== null &&\n typeof input === \"object\" &&\n Array.isArray((input as { internalEdges?: unknown }).internalEdges)\n ? (input as { internalEdges: ReadonlyArray<unknown> }).internalEdges\n : undefined;\n\n if (array === undefined) {\n throw new Error(\n \"expected an array of { from, to } edges, or an object with an `edges` array\",\n );\n }\n\n return array.map((item) => {\n if (item === null || typeof item !== \"object\") {\n throw new Error(\"each rendered 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 rendered edge must have string `from` and `to`\");\n }\n if (region !== undefined && ![\"nav\", \"header\", \"footer\", \"body\"].includes(region as string)) {\n throw new Error(\"rendered edge region must be nav, header, footer, or body\");\n }\n return region === undefined ? { from, to } : { from, to, region: region as AnchorRegion };\n });\n};\n","/**\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","import type { AnyRoute, AnyRouter } from \"@tanstack/react-router\";\n\nimport type { RouteSeo } from \"./declare\";\n\ninterface RouteMaps {\n routesByPath: Record<string, AnyRoute | undefined>;\n routesById: Record<string, AnyRoute>;\n}\n\n/**\n * Resolve a route's declared `seo.link` card (title + description) by its full\n * path.\n *\n * `useRouter()` returns a Router typed to this app's exact route tree, so\n * `routesByPath` is keyed by the literal `FileRouteTypes[\"fullPaths\"]` union —\n * but callers here (declared `related` targets) hold arbitrary runtime path\n * strings, not that literal type. `routesByPath` and `routesById` are plain\n * Records on every Router instance regardless of which route tree it's\n * parameterized over, so this narrows to that structural shape once, at this\n * boundary, instead of threading an `AnyRoute` cast through every call site.\n */\nexport function resolveRouteLink(router: AnyRouter, path: string): RouteSeo[\"link\"] {\n const { routesByPath, routesById } = router as unknown as RouteMaps;\n\n const direct = routesByPath[path];\n if (direct) return direct.options.staticData?.seo?.link;\n\n const byFullPath = Object.values(routesById).find((route) => route.fullPath === path);\n return byFullPath?.options.staticData?.seo?.link;\n}\n"],"mappings":"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"}
|