pagegraph 0.8.4 → 0.10.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 +158 -6
- package/dist/audit.d.ts +7 -0
- package/dist/audit.js +33 -7
- package/dist/audit.js.map +1 -1
- package/dist/cli.js +3827 -2198
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +14 -1
- package/dist/config.js.map +1 -1
- package/dist/{checks-6noG_xTS.d.ts → content-DVL2iyGU.d.ts} +274 -126
- package/dist/content-DbyJoRpj.js +1384 -0
- package/dist/content-DbyJoRpj.js.map +1 -0
- package/dist/index.d.ts +19 -8
- package/dist/index.js +31 -555
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/links-BFJc-pJ9.js +0 -318
- package/dist/links-BFJc-pJ9.js.map +0 -1
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { J as SeoGraph, V as FreshnessPolicy, h as CoverageRule, r as ContentPolicy } from "./content-DVL2iyGU.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. */
|
|
@@ -95,6 +95,19 @@ interface SeoCliConfig {
|
|
|
95
95
|
* `related` edges. A `--require-inbound` flag overrides this per invocation.
|
|
96
96
|
*/
|
|
97
97
|
readonly coverage?: ReadonlyArray<CoverageRule> | undefined;
|
|
98
|
+
/**
|
|
99
|
+
* Rendered content policy for `pagegraph check --site`: word-count floors per
|
|
100
|
+
* path glob. The heading, structured-data, robots, and canonical rules need no
|
|
101
|
+
* configuration.
|
|
102
|
+
*/
|
|
103
|
+
readonly content?: ContentPolicy | undefined;
|
|
104
|
+
/**
|
|
105
|
+
* Freshness policy: sitemap-eligible pages whose declared `modifiedAt` (or an
|
|
106
|
+
* instance's `publishedAt`) is older than `maxAgeDays` are editorial
|
|
107
|
+
* `stale-page` findings in `pagegraph check` and the refresh queue in
|
|
108
|
+
* `pagegraph stale`.
|
|
109
|
+
*/
|
|
110
|
+
readonly freshness?: FreshnessPolicy | undefined;
|
|
98
111
|
/** Agentic workflow host, context, and artifact settings. */
|
|
99
112
|
readonly workflows?: SeoWorkflowConfig | undefined;
|
|
100
113
|
/** How the CLI gets the graph. {@link viteGraphLoader} covers the Vite-app case. */
|
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 /** 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"}
|
|
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 { ContentPolicy } from \"../core/content\";\nimport type { FreshnessPolicy } from \"../core/freshness\";\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 /**\n * Rendered content policy for `pagegraph check --site`: word-count floors per\n * path glob. The heading, structured-data, robots, and canonical rules need no\n * configuration.\n */\n readonly content?: ContentPolicy | undefined;\n /**\n * Freshness policy: sitemap-eligible pages whose declared `modifiedAt` (or an\n * instance's `publishedAt`) is older than `maxAgeDays` are editorial\n * `stale-page` findings in `pagegraph check` and the refresh queue in\n * `pagegraph stale`.\n */\n readonly freshness?: FreshnessPolicy | 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;;;;ACpDF,MAAa,mBAAmB,WAAuC"}
|
|
@@ -1,4 +1,162 @@
|
|
|
1
1
|
import { AnyRoute, AnyRouteMatch } from "@tanstack/react-router";
|
|
2
|
+
//#region src/core/declare.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Consumer-augmented registry.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* declare module "pagegraph" {
|
|
8
|
+
* interface Register {
|
|
9
|
+
* paths: FileRouteTypes["fullPaths"];
|
|
10
|
+
* kinds: "page" | "article" | "hub";
|
|
11
|
+
* }
|
|
12
|
+
* }
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
interface Register {}
|
|
16
|
+
/** Every public full path in the consumer's route tree (type-only; erased at runtime). */
|
|
17
|
+
type PublicPath = Register extends {
|
|
18
|
+
paths: infer P extends string;
|
|
19
|
+
} ? P : string;
|
|
20
|
+
/** The consumer's page taxonomy. A label: nothing in the core branches on it. */
|
|
21
|
+
type SeoKind = Register extends {
|
|
22
|
+
kinds: infer K extends string;
|
|
23
|
+
} ? K : string;
|
|
24
|
+
interface SitemapPolicy {
|
|
25
|
+
priority: number;
|
|
26
|
+
changeFrequency: "weekly" | "monthly";
|
|
27
|
+
}
|
|
28
|
+
interface RouteSeo {
|
|
29
|
+
kind: SeoKind;
|
|
30
|
+
/** Breadcrumb label; fn form reads the match (e.g. loaderData frontmatter title). */
|
|
31
|
+
crumb?: string | ((match: AnyRouteMatch) => string) | undefined;
|
|
32
|
+
/** false = excluded from sitemap. Absent on non-indexable kinds is fine. */
|
|
33
|
+
sitemap?: SitemapPolicy | false | undefined;
|
|
34
|
+
/** meta robots value, e.g. "noindex, follow" on a hub page. */
|
|
35
|
+
robots?: string | undefined;
|
|
36
|
+
/** Typed cross-link edges. */
|
|
37
|
+
related?: ReadonlyArray<PublicPath> | undefined;
|
|
38
|
+
/** How OTHER pages render a card for this page. */
|
|
39
|
+
link?: {
|
|
40
|
+
title: string;
|
|
41
|
+
description: string;
|
|
42
|
+
} | undefined;
|
|
43
|
+
/** This route is a redirect/alias. */
|
|
44
|
+
redirectTo?: PublicPath | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* When this page's content last changed meaningfully, as an ISO 8601 date.
|
|
47
|
+
* Feeds the sitemap `<lastmod>` and the freshness report. Content instances
|
|
48
|
+
* carry their own dates instead; a param route's value is never inherited.
|
|
49
|
+
*/
|
|
50
|
+
modifiedAt?: string | undefined;
|
|
51
|
+
}
|
|
52
|
+
declare module "@tanstack/react-router" {
|
|
53
|
+
interface StaticDataRouteOption {
|
|
54
|
+
seo?: RouteSeo | undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/core/graph.d.ts
|
|
59
|
+
/**
|
|
60
|
+
* Where a node came from: `"route"` for a structural route declaration, or the
|
|
61
|
+
* `source` label of the collection that produced the instance (e.g. "blog").
|
|
62
|
+
*/
|
|
63
|
+
type SeoSource = string;
|
|
64
|
+
interface SeoNode {
|
|
65
|
+
/** Canonical path, no origin (e.g. "/pricing", "/blog/my-post"). */
|
|
66
|
+
path: string;
|
|
67
|
+
kind: SeoKind;
|
|
68
|
+
source: SeoSource;
|
|
69
|
+
/** Route-declared policy, or synthesized (kind + inherited sitemap) for instances. */
|
|
70
|
+
policy: RouteSeo;
|
|
71
|
+
instance?: {
|
|
72
|
+
title: string;
|
|
73
|
+
description?: string | undefined;
|
|
74
|
+
publishedAt?: string | undefined;
|
|
75
|
+
modifiedAt?: string | undefined;
|
|
76
|
+
} | undefined;
|
|
77
|
+
}
|
|
78
|
+
type SeoEdgeType = "crumb-parent" | "related" | "redirect" | "collection-member";
|
|
79
|
+
interface SeoEdge {
|
|
80
|
+
from: string;
|
|
81
|
+
to: string;
|
|
82
|
+
type: SeoEdgeType;
|
|
83
|
+
}
|
|
84
|
+
interface SeoGraph {
|
|
85
|
+
nodes: Map<string, SeoNode>;
|
|
86
|
+
edges: Array<SeoEdge>;
|
|
87
|
+
/** Exact-path ownership conflicts encountered while assembling graph sources. */
|
|
88
|
+
collisions?: ReadonlyArray<{
|
|
89
|
+
path: string;
|
|
90
|
+
sources: ReadonlyArray<SeoSource>;
|
|
91
|
+
}> | undefined;
|
|
92
|
+
}
|
|
93
|
+
/** One concrete page produced by a collection. */
|
|
94
|
+
interface SeoInstance {
|
|
95
|
+
/** Canonical path of the page (e.g. "/blog/my-post"). */
|
|
96
|
+
readonly path: string;
|
|
97
|
+
readonly title: string;
|
|
98
|
+
readonly description?: string | undefined;
|
|
99
|
+
readonly publishedAt?: string | undefined;
|
|
100
|
+
readonly modifiedAt?: string | undefined;
|
|
101
|
+
}
|
|
102
|
+
interface SeoCollection {
|
|
103
|
+
/**
|
|
104
|
+
* The param route these instances render through (e.g. "/blog/$slug"). Instances
|
|
105
|
+
* inherit this route's declared policy (kind + sitemap) — the graph reads it from
|
|
106
|
+
* the structural node, so declarations stay the single source of truth.
|
|
107
|
+
*/
|
|
108
|
+
readonly route: PublicPath;
|
|
109
|
+
/** The `source` stamped on every node this collection produces (e.g. "blog"). */
|
|
110
|
+
readonly source: SeoSource;
|
|
111
|
+
readonly instances: ReadonlyArray<SeoInstance>;
|
|
112
|
+
/** Edges the collection declares between its instances and the rest of the graph. */
|
|
113
|
+
readonly edges?: ReadonlyArray<SeoEdge> | undefined;
|
|
114
|
+
}
|
|
115
|
+
interface BuildSeoGraphInput {
|
|
116
|
+
readonly routeTree: AnyRoute;
|
|
117
|
+
readonly collections?: ReadonlyArray<SeoCollection> | undefined;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Build the SEO graph from route declarations and content collections.
|
|
121
|
+
*
|
|
122
|
+
* Synchronous: the caller materializes its collections before calling, so there is
|
|
123
|
+
* no async work here. Callers own the origin.
|
|
124
|
+
*/
|
|
125
|
+
declare function buildSeoGraph(input: BuildSeoGraphInput): SeoGraph;
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/core/freshness.d.ts
|
|
128
|
+
/** Project policy: pages whose last change is older than this are stale. */
|
|
129
|
+
interface FreshnessPolicy {
|
|
130
|
+
readonly maxAgeDays: number;
|
|
131
|
+
}
|
|
132
|
+
interface FreshnessEntry {
|
|
133
|
+
readonly path: string;
|
|
134
|
+
readonly kind: SeoKind;
|
|
135
|
+
readonly source: SeoSource;
|
|
136
|
+
/** The declared date, normalized to ISO 8601. */
|
|
137
|
+
readonly lastModified: string;
|
|
138
|
+
/** Whole days between `lastModified` and the report's `asOf`. */
|
|
139
|
+
readonly ageDays: number;
|
|
140
|
+
}
|
|
141
|
+
interface FreshnessReport {
|
|
142
|
+
readonly asOf: string;
|
|
143
|
+
readonly maxAgeDays: number;
|
|
144
|
+
/** Pages older than `maxAgeDays`, oldest first: the refresh queue. */
|
|
145
|
+
readonly stale: ReadonlyArray<FreshnessEntry>;
|
|
146
|
+
/** Dated pages within `maxAgeDays`, newest first. */
|
|
147
|
+
readonly fresh: ReadonlyArray<FreshnessEntry>;
|
|
148
|
+
/** Sitemap-eligible pages that declare no date, in graph order. */
|
|
149
|
+
readonly undated: ReadonlyArray<string>;
|
|
150
|
+
/** Sitemap-eligible pages whose date does not parse; `check` fails on each. */
|
|
151
|
+
readonly invalid: ReadonlyArray<string>;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Split the graph's sitemap-eligible pages into stale, fresh, undated, and
|
|
155
|
+
* invalid. An invalid date cannot be aged, but it is listed so a report never
|
|
156
|
+
* reads as complete while it skipped a page.
|
|
157
|
+
*/
|
|
158
|
+
declare function freshnessReport(graph: SeoGraph, policy: FreshnessPolicy, now: Date): FreshnessReport;
|
|
159
|
+
//#endregion
|
|
2
160
|
//#region src/core/links.d.ts
|
|
3
161
|
/**
|
|
4
162
|
* Pure link-graph utilities: read anchors out of rendered HTML, model the
|
|
@@ -108,6 +266,16 @@ interface RenderedEdgeArtifactCrawl {
|
|
|
108
266
|
readonly truncatedPages: ReadonlyArray<string>;
|
|
109
267
|
/** Pages that failed to fetch; their outgoing anchors are missing. */
|
|
110
268
|
readonly failures: ReadonlyArray<RenderedEdgeFailure>;
|
|
269
|
+
/** Non-HTML responses from link-only discovery; these do not hide HTML pages. */
|
|
270
|
+
readonly nonHtml?: ReadonlyArray<{
|
|
271
|
+
readonly url: string;
|
|
272
|
+
readonly finalUrl: string;
|
|
273
|
+
readonly contentType: string;
|
|
274
|
+
}>;
|
|
275
|
+
readonly discoveryFailures?: ReadonlyArray<RenderedEdgeFailure>;
|
|
276
|
+
readonly skipped?: ReadonlyArray<string>;
|
|
277
|
+
readonly sitemapTruncated?: boolean;
|
|
278
|
+
readonly sitemapUsed?: boolean;
|
|
111
279
|
}
|
|
112
280
|
/**
|
|
113
281
|
* The stable, `decodeRenderedEdges`-compatible artifact `pagegraph links verify
|
|
@@ -136,125 +304,6 @@ declare const RENDERED_EDGE_ARTIFACT_SCHEMA_VERSION: 1;
|
|
|
136
304
|
*/
|
|
137
305
|
declare const decodeRenderedEdgeArtifact: (input: unknown) => RenderedEdgeArtifact;
|
|
138
306
|
//#endregion
|
|
139
|
-
//#region src/core/declare.d.ts
|
|
140
|
-
/**
|
|
141
|
-
* Consumer-augmented registry.
|
|
142
|
-
*
|
|
143
|
-
* ```ts
|
|
144
|
-
* declare module "pagegraph" {
|
|
145
|
-
* interface Register {
|
|
146
|
-
* paths: FileRouteTypes["fullPaths"];
|
|
147
|
-
* kinds: "page" | "article" | "hub";
|
|
148
|
-
* }
|
|
149
|
-
* }
|
|
150
|
-
* ```
|
|
151
|
-
*/
|
|
152
|
-
interface Register {}
|
|
153
|
-
/** Every public full path in the consumer's route tree (type-only; erased at runtime). */
|
|
154
|
-
type PublicPath = Register extends {
|
|
155
|
-
paths: infer P extends string;
|
|
156
|
-
} ? P : string;
|
|
157
|
-
/** The consumer's page taxonomy. A label: nothing in the core branches on it. */
|
|
158
|
-
type SeoKind = Register extends {
|
|
159
|
-
kinds: infer K extends string;
|
|
160
|
-
} ? K : string;
|
|
161
|
-
interface SitemapPolicy {
|
|
162
|
-
priority: number;
|
|
163
|
-
changeFrequency: "weekly" | "monthly";
|
|
164
|
-
}
|
|
165
|
-
interface RouteSeo {
|
|
166
|
-
kind: SeoKind;
|
|
167
|
-
/** Breadcrumb label; fn form reads the match (e.g. loaderData frontmatter title). */
|
|
168
|
-
crumb?: string | ((match: AnyRouteMatch) => string) | undefined;
|
|
169
|
-
/** false = excluded from sitemap. Absent on non-indexable kinds is fine. */
|
|
170
|
-
sitemap?: SitemapPolicy | false | undefined;
|
|
171
|
-
/** meta robots value, e.g. "noindex, follow" on a hub page. */
|
|
172
|
-
robots?: string | undefined;
|
|
173
|
-
/** Typed cross-link edges. */
|
|
174
|
-
related?: ReadonlyArray<PublicPath> | undefined;
|
|
175
|
-
/** How OTHER pages render a card for this page. */
|
|
176
|
-
link?: {
|
|
177
|
-
title: string;
|
|
178
|
-
description: string;
|
|
179
|
-
} | undefined;
|
|
180
|
-
/** This route is a redirect/alias. */
|
|
181
|
-
redirectTo?: PublicPath | undefined;
|
|
182
|
-
}
|
|
183
|
-
declare module "@tanstack/react-router" {
|
|
184
|
-
interface StaticDataRouteOption {
|
|
185
|
-
seo?: RouteSeo | undefined;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
//#endregion
|
|
189
|
-
//#region src/core/graph.d.ts
|
|
190
|
-
/**
|
|
191
|
-
* Where a node came from: `"route"` for a structural route declaration, or the
|
|
192
|
-
* `source` label of the collection that produced the instance (e.g. "blog").
|
|
193
|
-
*/
|
|
194
|
-
type SeoSource = string;
|
|
195
|
-
interface SeoNode {
|
|
196
|
-
/** Canonical path, no origin (e.g. "/pricing", "/blog/my-post"). */
|
|
197
|
-
path: string;
|
|
198
|
-
kind: SeoKind;
|
|
199
|
-
source: SeoSource;
|
|
200
|
-
/** Route-declared policy, or synthesized (kind + inherited sitemap) for instances. */
|
|
201
|
-
policy: RouteSeo;
|
|
202
|
-
instance?: {
|
|
203
|
-
title: string;
|
|
204
|
-
description?: string | undefined;
|
|
205
|
-
publishedAt?: string | undefined;
|
|
206
|
-
modifiedAt?: string | undefined;
|
|
207
|
-
} | undefined;
|
|
208
|
-
}
|
|
209
|
-
type SeoEdgeType = "crumb-parent" | "related" | "redirect" | "collection-member";
|
|
210
|
-
interface SeoEdge {
|
|
211
|
-
from: string;
|
|
212
|
-
to: string;
|
|
213
|
-
type: SeoEdgeType;
|
|
214
|
-
}
|
|
215
|
-
interface SeoGraph {
|
|
216
|
-
nodes: Map<string, SeoNode>;
|
|
217
|
-
edges: Array<SeoEdge>;
|
|
218
|
-
/** Exact-path ownership conflicts encountered while assembling graph sources. */
|
|
219
|
-
collisions?: ReadonlyArray<{
|
|
220
|
-
path: string;
|
|
221
|
-
sources: ReadonlyArray<SeoSource>;
|
|
222
|
-
}> | undefined;
|
|
223
|
-
}
|
|
224
|
-
/** One concrete page produced by a collection. */
|
|
225
|
-
interface SeoInstance {
|
|
226
|
-
/** Canonical path of the page (e.g. "/blog/my-post"). */
|
|
227
|
-
readonly path: string;
|
|
228
|
-
readonly title: string;
|
|
229
|
-
readonly description?: string | undefined;
|
|
230
|
-
readonly publishedAt?: string | undefined;
|
|
231
|
-
readonly modifiedAt?: string | undefined;
|
|
232
|
-
}
|
|
233
|
-
interface SeoCollection {
|
|
234
|
-
/**
|
|
235
|
-
* The param route these instances render through (e.g. "/blog/$slug"). Instances
|
|
236
|
-
* inherit this route's declared policy (kind + sitemap) — the graph reads it from
|
|
237
|
-
* the structural node, so declarations stay the single source of truth.
|
|
238
|
-
*/
|
|
239
|
-
readonly route: PublicPath;
|
|
240
|
-
/** The `source` stamped on every node this collection produces (e.g. "blog"). */
|
|
241
|
-
readonly source: SeoSource;
|
|
242
|
-
readonly instances: ReadonlyArray<SeoInstance>;
|
|
243
|
-
/** Edges the collection declares between its instances and the rest of the graph. */
|
|
244
|
-
readonly edges?: ReadonlyArray<SeoEdge> | undefined;
|
|
245
|
-
}
|
|
246
|
-
interface BuildSeoGraphInput {
|
|
247
|
-
readonly routeTree: AnyRoute;
|
|
248
|
-
readonly collections?: ReadonlyArray<SeoCollection> | undefined;
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Build the SEO graph from route declarations and content collections.
|
|
252
|
-
*
|
|
253
|
-
* Synchronous: the caller materializes its collections before calling, so there is
|
|
254
|
-
* no async work here. Callers own the origin.
|
|
255
|
-
*/
|
|
256
|
-
declare function buildSeoGraph(input: BuildSeoGraphInput): SeoGraph;
|
|
257
|
-
//#endregion
|
|
258
307
|
//#region src/core/checks.d.ts
|
|
259
308
|
type Severity = "structural" | "editorial";
|
|
260
309
|
interface Violation {
|
|
@@ -282,13 +331,31 @@ interface CoverageRule {
|
|
|
282
331
|
readonly minInbound: number;
|
|
283
332
|
}
|
|
284
333
|
/**
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
334
|
+
* Options for {@link checkGraph}. Freshness needs a clock, and the engine stays
|
|
335
|
+
* pure, so a freshness policy is only accepted together with the `now` it is
|
|
336
|
+
* judged against.
|
|
288
337
|
*/
|
|
289
|
-
|
|
338
|
+
type CheckGraphOptions = {
|
|
290
339
|
readonly coverage?: ReadonlyArray<CoverageRule> | undefined;
|
|
291
|
-
}
|
|
340
|
+
} & ({
|
|
341
|
+
readonly freshness: FreshnessPolicy;
|
|
342
|
+
readonly now: Date;
|
|
343
|
+
} | {
|
|
344
|
+
readonly freshness?: undefined;
|
|
345
|
+
readonly now?: undefined;
|
|
346
|
+
});
|
|
347
|
+
/**
|
|
348
|
+
* Run every static rule against the graph, then any caller-supplied
|
|
349
|
+
* {@link CoverageRule}s and freshness policy, and return the flat list of
|
|
350
|
+
* violations. With no options the result is exactly the static rule set.
|
|
351
|
+
*/
|
|
352
|
+
declare function checkGraph(graph: SeoGraph, options?: CheckGraphOptions): Array<Violation>;
|
|
353
|
+
/**
|
|
354
|
+
* One editorial `stale-page` violation per sitemap-eligible page whose last
|
|
355
|
+
* declared change is older than the policy allows. Undated pages are not
|
|
356
|
+
* violations: see {@link freshnessReport} for the full picture.
|
|
357
|
+
*/
|
|
358
|
+
declare function checkFreshness(graph: SeoGraph, policy: FreshnessPolicy, now: Date): Array<Violation>;
|
|
292
359
|
/**
|
|
293
360
|
* Enforce contextual-link coverage against the *declared* graph: only `related`
|
|
294
361
|
* edges count — breadcrumb ancestry is navigation, not context.
|
|
@@ -309,5 +376,86 @@ declare function checkRenderedCoverage(graph: SeoGraph, renderedEdges: ReadonlyA
|
|
|
309
376
|
/** Structural violations fail `pagegraph check`; editorial-only stays green. */
|
|
310
377
|
declare const hasStructuralViolations: (violations: ReadonlyArray<Violation>) => boolean;
|
|
311
378
|
//#endregion
|
|
312
|
-
|
|
313
|
-
|
|
379
|
+
//#region src/core/content.d.ts
|
|
380
|
+
interface Heading {
|
|
381
|
+
readonly level: number;
|
|
382
|
+
readonly text: string;
|
|
383
|
+
}
|
|
384
|
+
/** A JSON-LD object that carries an `@type`, found at any depth of any block. */
|
|
385
|
+
type JsonLdNode = Readonly<Record<string, unknown>>;
|
|
386
|
+
/** What a rendered document says, reduced to the fields the content rules read. */
|
|
387
|
+
interface PageContent {
|
|
388
|
+
readonly title: string | undefined;
|
|
389
|
+
readonly description: string | undefined;
|
|
390
|
+
readonly robots: string | undefined;
|
|
391
|
+
readonly canonical: string | undefined;
|
|
392
|
+
/** Every H1 in the body, in document order, by accessible text. */
|
|
393
|
+
readonly h1: ReadonlyArray<string>;
|
|
394
|
+
/** Headings in the main content region (`<main>`, else the body without nav and footer). */
|
|
395
|
+
readonly outline: ReadonlyArray<Heading>;
|
|
396
|
+
/** Words of visible text in the main content region. */
|
|
397
|
+
readonly wordCount: number;
|
|
398
|
+
/** Visible body text with whitespace collapsed; scripts, styles, and templates removed. */
|
|
399
|
+
readonly text: string;
|
|
400
|
+
/** Every typed JSON-LD node on the page, flattened out of arrays, `@graph`, and nesting. */
|
|
401
|
+
readonly jsonLd: ReadonlyArray<JsonLdNode>;
|
|
402
|
+
}
|
|
403
|
+
type ContentRuleName = "missing-h1" | "multiple-h1" | "empty-h1" | "heading-level-skip" | "h1-title-mismatch" | "faq-not-visible" | "offer-price-not-visible" | "webpage-head-mismatch" | "thin-content";
|
|
404
|
+
interface ContentFinding {
|
|
405
|
+
readonly rule: ContentRuleName;
|
|
406
|
+
readonly severity: Severity;
|
|
407
|
+
readonly message: string;
|
|
408
|
+
readonly fix: string;
|
|
409
|
+
}
|
|
410
|
+
interface ContentCheckOptions {
|
|
411
|
+
/** Minimum words of main-region text; omit to skip the `thin-content` rule. */
|
|
412
|
+
readonly minWords?: number | undefined;
|
|
413
|
+
}
|
|
414
|
+
/** Reduce a rendered HTML document to the fields the content rules read. Pure. */
|
|
415
|
+
declare const extractPageContent: (html: string) => PageContent;
|
|
416
|
+
/** Run every content rule against one rendered page. Graph-independent; pure. */
|
|
417
|
+
declare const checkPageContent: (content: PageContent, options?: ContentCheckOptions) => Array<ContentFinding>;
|
|
418
|
+
/** A word-count floor for sitemap-eligible pages matching a path glob. */
|
|
419
|
+
interface WordCountRule {
|
|
420
|
+
/** Path glob: `*` matches within a segment, `**` matches across segments. */
|
|
421
|
+
readonly path: string;
|
|
422
|
+
readonly minWords: number;
|
|
423
|
+
}
|
|
424
|
+
/** Project policy for rendered content checks. */
|
|
425
|
+
interface ContentPolicy {
|
|
426
|
+
/** Word-count floors; a page matching several rules must meet the highest. */
|
|
427
|
+
readonly minWords?: ReadonlyArray<WordCountRule> | undefined;
|
|
428
|
+
}
|
|
429
|
+
/** One declared page as the server rendered it, or why it could not be read. */
|
|
430
|
+
type RenderedDocument = {
|
|
431
|
+
readonly path: string;
|
|
432
|
+
readonly ok: true;
|
|
433
|
+
/** Path of the final response URL, after redirects, encoded or not. */
|
|
434
|
+
readonly finalPath: string;
|
|
435
|
+
readonly html: string;
|
|
436
|
+
} | {
|
|
437
|
+
readonly path: string;
|
|
438
|
+
readonly ok: false;
|
|
439
|
+
readonly error: string;
|
|
440
|
+
};
|
|
441
|
+
/**
|
|
442
|
+
* The declared pages a rendered check fetches: every sitemap-eligible page,
|
|
443
|
+
* plus concrete pages that declare `noindex` so the rendered robots meta can be
|
|
444
|
+
* held to that declaration. Param templates and redirects render no page of
|
|
445
|
+
* their own.
|
|
446
|
+
*/
|
|
447
|
+
declare const renderedCheckTargets: (graph: SeoGraph) => Array<string>;
|
|
448
|
+
/**
|
|
449
|
+
* Judge rendered documents against the declared graph. Every indexable page must
|
|
450
|
+
* render, stay on its path, keep a self-referencing canonical and indexable
|
|
451
|
+
* robots meta, and pass {@link checkPageContent}; a page that declares `noindex`
|
|
452
|
+
* must render a `noindex` robots meta.
|
|
453
|
+
*
|
|
454
|
+
* Robots are read from the `<meta name="robots">` tag only. An `X-Robots-Tag`
|
|
455
|
+
* header is usually host policy (preview hosts noindex everything), so holding
|
|
456
|
+
* pages to it would fail every local run for a reason no declaration controls.
|
|
457
|
+
*/
|
|
458
|
+
declare function checkRenderedContent(graph: SeoGraph, documents: ReadonlyArray<RenderedDocument>, policy?: ContentPolicy): Array<Violation>;
|
|
459
|
+
//#endregion
|
|
460
|
+
export { PublicPath as $, RenderedEdgeFailure as A, FreshnessEntry as B, Anchor as C, RENDERED_EDGE_ARTIFACT_SCHEMA_VERSION as D, LinkGraphDiff as E, decodeRenderedEdgeArtifact as F, SeoCollection as G, FreshnessReport as H, diffLinkGraph as I, SeoGraph as J, SeoEdge as K, extractAnchors as L, RenderedPage as M, SimpleEdge as N, RenderedEdgeArtifact as O, buildRenderedGraph as P, buildSeoGraph as Q, normalizePath as R, hasStructuralViolations as S, LinkEdge as T, freshnessReport as U, FreshnessPolicy as V, BuildSeoGraphInput as W, SeoNode as X, SeoInstance as Y, SeoSource as Z, Violation as _, Heading as a, checkGraph as b, RenderedDocument as c, checkRenderedContent as d, Register as et, extractPageContent as f, Severity as g, CoverageRule as h, ContentRuleName as i, RenderedGraph as j, RenderedEdgeArtifactCrawl as k, WordCountRule as l, CheckGraphOptions as m, ContentFinding as n, SeoKind as nt, JsonLdNode as o, renderedCheckTargets as p, SeoEdgeType as q, ContentPolicy as r, SitemapPolicy as rt, PageContent as s, ContentCheckOptions as t, RouteSeo as tt, checkPageContent as u, checkCoverage as v, AnchorRegion as w, checkRenderedCoverage as x, checkFreshness as y, renderedGraphFromEdges as z };
|
|
461
|
+
//# sourceMappingURL=content-DVL2iyGU.d.ts.map
|