pagegraph 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +277 -25
  2. package/dist/audit.d.ts +2 -2
  3. package/dist/audit.js +4 -4
  4. package/dist/build-only/audit.js +1 -1
  5. package/dist/build-only/claims.d.ts +1 -0
  6. package/dist/build-only/claims.js +6 -0
  7. package/dist/build-only/claims.js.map +1 -0
  8. package/dist/build-only/config.js +1 -1
  9. package/dist/build-only/oxlint.d.ts +1 -0
  10. package/dist/build-only/oxlint.js +6 -0
  11. package/dist/build-only/oxlint.js.map +1 -0
  12. package/dist/build-only/tanstack-start.js +1 -1
  13. package/dist/build-only/vite.js +1 -1
  14. package/dist/{checks-uZk5Em9A.js → checks-BS-aDXB_.js} +2 -2
  15. package/dist/checks-BS-aDXB_.js.map +1 -0
  16. package/dist/claims-Bch7Lxlo.d.ts +92 -0
  17. package/dist/claims-Bv7voeEV.js +431 -0
  18. package/dist/claims-Bv7voeEV.js.map +1 -0
  19. package/dist/claims.d.ts +94 -0
  20. package/dist/claims.js +2 -0
  21. package/dist/cli.js +10095 -8318
  22. package/dist/cli.js.map +1 -1
  23. package/dist/config.d.ts +4 -24
  24. package/dist/config.js +2 -2
  25. package/dist/config.js.map +1 -1
  26. package/dist/{content-kcEnQdfi.js → content-CHd0X4s6.js} +2 -2
  27. package/dist/{content-kcEnQdfi.js.map → content-CHd0X4s6.js.map} +1 -1
  28. package/dist/{content-B1cPtL8b.d.ts → content-DgNr7apS.d.ts} +4 -5
  29. package/dist/declare-D5o7hwmv.js.map +1 -1
  30. package/dist/document-CztEu3Vd.d.ts +109 -0
  31. package/dist/document-Dvemcv7V.js +379 -0
  32. package/dist/document-Dvemcv7V.js.map +1 -0
  33. package/dist/facts-1OKclPOk.js +115 -0
  34. package/dist/facts-1OKclPOk.js.map +1 -0
  35. package/dist/{declare-DVgdEdOL.d.ts → graph-D1PIndsC.d.ts} +117 -6
  36. package/dist/{graph-Bcsourlv.js → graph-DMNOLXLu.js} +23 -13
  37. package/dist/graph-DMNOLXLu.js.map +1 -0
  38. package/dist/index-C5w9ripz.d.ts +204 -0
  39. package/dist/index.d.ts +6 -160
  40. package/dist/index.js +37 -5
  41. package/dist/index.js.map +1 -1
  42. package/dist/inline-BbS93GAT.js +682 -0
  43. package/dist/inline-BbS93GAT.js.map +1 -0
  44. package/dist/llms-Dhr_5u2O.js +19 -0
  45. package/dist/llms-Dhr_5u2O.js.map +1 -0
  46. package/dist/markdown-C6dRE9zD.js +168 -0
  47. package/dist/markdown-C6dRE9zD.js.map +1 -0
  48. package/dist/markdown-path-lJB9BAVZ.js +6 -0
  49. package/dist/markdown-path-lJB9BAVZ.js.map +1 -0
  50. package/dist/{message-DT_CjUdt.js → message-CuBjgZ74.js} +2 -2
  51. package/dist/{message-DT_CjUdt.js.map → message-CuBjgZ74.js.map} +1 -1
  52. package/dist/oxlint.d.ts +58 -0
  53. package/dist/oxlint.js +202 -0
  54. package/dist/oxlint.js.map +1 -0
  55. package/dist/page-heads-BfjWQvau.d.ts +108 -0
  56. package/dist/{page-heads-6y-CARRQ.js → page-heads-BgbaIjj9.js} +3 -3
  57. package/dist/page-heads-BgbaIjj9.js.map +1 -0
  58. package/dist/{projections-DeEFe7XT.d.ts → projections-Bk-qOOsR.d.ts} +2 -2
  59. package/dist/react.d.ts +3 -2
  60. package/dist/react.js +2 -1
  61. package/dist/react.js.map +1 -1
  62. package/dist/tanstack-start/index.d.ts +29 -13
  63. package/dist/tanstack-start/index.js +391 -42
  64. package/dist/tanstack-start/index.js.map +1 -1
  65. package/dist/tanstack-start/markdown.d.ts +12 -0
  66. package/dist/tanstack-start/markdown.js +149 -0
  67. package/dist/tanstack-start/markdown.js.map +1 -0
  68. package/dist/tanstack-start/prerender-worker.d.ts +2 -0
  69. package/dist/tanstack-start/prerender-worker.js +7 -0
  70. package/dist/tanstack-start/prerender-worker.js.map +1 -0
  71. package/dist/tanstack-start/react.d.ts +7 -0
  72. package/dist/tanstack-start/react.js +12 -0
  73. package/dist/tanstack-start/react.js.map +1 -0
  74. package/dist/tanstack-start/server.d.ts +9 -3
  75. package/dist/tanstack-start/server.js +26 -2
  76. package/dist/tanstack-start/server.js.map +1 -1
  77. package/dist/{vite-graph-loader-01p1WxEq.js → vite-graph-loader-CsGiYNWE.js} +6 -5
  78. package/dist/vite-graph-loader-CsGiYNWE.js.map +1 -0
  79. package/dist/vite-graph-loader-DZCwejuJ.d.ts +133 -0
  80. package/package.json +44 -3
  81. package/dist/checks-uZk5Em9A.js.map +0 -1
  82. package/dist/graph-Bcsourlv.js.map +0 -1
  83. package/dist/graph-BxCY90vq.d.ts +0 -104
  84. package/dist/graph-CBIY71w6.d.ts +0 -57
  85. package/dist/page-heads-6y-CARRQ.js.map +0 -1
  86. package/dist/page-heads-DNkjMUEN.d.ts +0 -21
  87. package/dist/vite-graph-loader-01p1WxEq.js.map +0 -1
  88. package/dist/vite-graph-loader-Btgjz5RH.d.ts +0 -56
package/README.md CHANGED
@@ -26,20 +26,194 @@ bun add pagegraph
26
26
  bun add -D lighthouse # only for `pagegraph audit` performance evidence
27
27
  ```
28
28
 
29
+ ## Authored pages
30
+
31
+ Keep authored text beside its React layout. `DocumentProvider` records only its page body when
32
+ supplied a collector; ordinary rendering preserves the layout. `Title` assigns a heading to the
33
+ nearest section or item, `Fact` resolves shared values, and `Visual` describes content for the
34
+ markdown document without rendering its children during capture.
35
+
36
+ ```tsx
37
+ import { renderToStaticMarkup } from "react-dom/server";
38
+ import { defineFacts, fact, documentMarkdown } from "pagegraph";
39
+ import {
40
+ DocumentProvider, T, Title, Section, Fact, createCollector, finishDocument,
41
+ } from "pagegraph/react";
42
+
43
+ const facts = defineFacts({ attempts: fact.number(8) });
44
+ declare module "pagegraph" {
45
+ interface Register { facts: typeof facts }
46
+ }
47
+
48
+ const collector = createCollector({ path: "/email", site: "https://example.com", facts });
49
+ const page = (
50
+ <DocumentProvider collector={collector}>
51
+ <Section kind="prose" id="intro">
52
+ <h1><Title>Email delivery</Title></h1>
53
+ <p><T>Retry up to <Fact id="attempts" /> times.</T></p>
54
+ </Section>
55
+ </DocumentProvider>
56
+ );
57
+ // Your SSR renderer supplies completed HTML, whose markers determine document order.
58
+ const html = renderToStaticMarkup(page);
59
+ const document = finishDocument(collector, {
60
+ title: "Email delivery", description: "Reliable delivery with retries.",
61
+ }, html);
62
+ const markdown = documentMarkdown(document, "https://example.com");
63
+ ```
64
+
65
+ Use `defineSectionKind` for custom formatting; its markdown writer provides `text`, `heading`,
66
+ and `table`:
67
+
68
+ ```ts
69
+ import { defineSectionKind } from "pagegraph";
70
+ const comparison = defineSectionKind({
71
+ kind: "comparison",
72
+ markdown: (section, md) => md.heading(2, section.title)
73
+ + md.table(section.items.map((item) => item.cells)),
74
+ }); // pass kind={comparison} to Section
75
+ ```
76
+
77
+ `Section.Item.Link` records a card's destination, while `CaptureAnchor` is the
78
+ public anchor boundary for router `createLink` integrations. `ForAgents` and `ForHumans` mark
79
+ audience-specific text; markdown excludes human-only content. `messageText` extracts text or
80
+ inline markdown for head metadata. The root entry also provides `hashDocument`,
81
+ `createMarkdownLock`, and pure `llmsMarkdown`/`llmsSection` string builders.
82
+
83
+ ## Rendered markdown with TanStack Start
84
+
85
+ Declare rendered pages on the route that owns `DocumentProvider`. Collection instances inherit
86
+ these fields from their parameter route; the parameter template itself is never captured.
87
+
88
+ ```tsx
89
+ import { createFileRoute } from "@tanstack/react-router";
90
+ import { DocumentProvider, Section, T, Title } from "pagegraph/react";
91
+ import { Link } from "pagegraph/tanstack-start/react";
92
+
93
+ export const Route = createFileRoute("/pricing")({
94
+ staticData: {
95
+ seo: { kind: "page", head: { title: "Pricing", description: "Plans for your team." } },
96
+ markdown: "rendered",
97
+ llms: "Product",
98
+ },
99
+ head: () => ({ meta: [{ title: "Pricing" }, { name: "description", content: "Plans for your team." }] }),
100
+ component: () => (
101
+ <DocumentProvider>
102
+ <Section kind="prose" id="plans">
103
+ <h1><Title>Plans for your team</Title></h1>
104
+ <Link to="/"><T>Back home</T></Link>
105
+ </Section>
106
+ </DocumentProvider>
107
+ ),
108
+ });
109
+ ```
110
+
111
+ `markdown: "source"` identifies pages whose Markdown is supplied by the application; pagegraph
112
+ captures only `"rendered"` pages. `llms` sets an optional llms.txt group. Use the capture-aware
113
+ `Link` for authored links: it uses TanStack's ordinary Link outside a capture and records resolved
114
+ anchor destinations during capture. The plugin adds `source=` locations to authored JSX imported
115
+ from `pagegraph/react`. In development, `DocumentProvider` rejects a current route lacking
116
+ `staticData.markdown: "rendered"`.
117
+
118
+ Configure the plugin and pass its fixed capture page to Start:
119
+
120
+ ```ts
121
+ import { pagegraph } from "pagegraph/tanstack-start";
122
+
123
+ const graph = pagegraph({
124
+ origin: "https://preview.example.com", // this deployment's SEO origin
125
+ collections: "src/collections.ts",
126
+ markdown: {
127
+ origin: "https://example.com", // canonical document and twin links
128
+ serverEntry: "dist/server/index.js", // default; relative to the Vite root
129
+ },
130
+ facts: "src/facts.ts", // exports `facts`; evaluated in the pagegraph environment
131
+ });
132
+
133
+ // In your Vite plugins:
134
+ // tanstackStart({
135
+ // prerender: { enabled: true, autoStaticPathsDiscovery: false, crawlLinks: false, failOnError: true },
136
+ // pages: [...graph.prerenderPages],
137
+ // }), graph
138
+ ```
139
+
140
+ For Cloudflare, wire the non-deployed prerender Worker to the compiled Start server. Add this to
141
+ `cloudflare()` alongside the production Worker's configuration:
142
+
143
+ ```ts
144
+ experimental: {
145
+ prerenderWorker: {
146
+ config: {
147
+ name: "app-markdown-prerender",
148
+ main: "pagegraph/tanstack-start/prerender-worker",
149
+ compatibility_date: "2026-09-20",
150
+ compatibility_flags: ["nodejs_compat"],
151
+ vars: { TSS_PRERENDERING: "true" },
152
+ assets: { binding: "ASSETS", run_worker_first: true },
153
+ },
154
+ },
155
+ },
156
+ ```
157
+
158
+ Call the capture adapter before your normal Start handler, forwarding the same request options:
159
+
160
+ ```ts
161
+ import handler from "@tanstack/react-start/server-entry";
162
+ import { markdownRequest } from "pagegraph/tanstack-start/markdown";
163
+
164
+ export default {
165
+ async fetch(request: Request) {
166
+ return await markdownRequest(request) ?? handler.fetch(request);
167
+ },
168
+ };
169
+ ```
170
+
171
+ Only dev and the prerender Worker capture documents. `GET` and `HEAD` on
172
+ `/__pagegraph/markdown.json` capture all rendered pages; `/pricing.md` and
173
+ `/pricing.document.json` capture one page in dev. Ordinary requests return `null`. In production,
174
+ private capture paths return 404 and Markdown twins fall through to your static asset server.
175
+ `markdownPagePaths()` exposes the concrete rendered page paths.
176
+
177
+ After prerender, the plugin writes `dist/client/pricing.md` (`/` becomes `index.md`),
178
+ `.pagegraph/documents/pricing.json`, and `.pagegraph/heads.json`, and removes the emitted private
179
+ capture response. Stale valid documents at their derived paths are pruned; unrelated files are
180
+ preserved. Ignore the private generated documents and heads in Git. Claims replay is reserved for
181
+ a separate integration.
182
+
183
+ Serve graph-derived llms.txt without rendering pages:
184
+
185
+ ```ts
186
+ import { createFileRoute } from "@tanstack/react-router";
187
+ import { llmsTxt } from "pagegraph/tanstack-start/server";
188
+
189
+ export const Route = createFileRoute("/llms.txt")({
190
+ server: { handlers: { GET: llmsTxt({ sections: ["## Docs\n\nRead our documentation."] }) } },
191
+ });
192
+ ```
193
+
194
+ `llmsSection()` returns the generated section for custom composition. It includes rendered twins
195
+ and explicitly grouped pages, using graph titles and descriptions and `markdown.origin`.
196
+
29
197
  ## What runs where
30
198
 
31
- Every entry belongs to one place. Runtime entries are safe in a Worker, an SSR server, or a
32
- browser; build entries load your app through Vite, parse routes natively, or drive Node I/O.
199
+ Every entry belongs to one place. Runtime entries use portable graph and document code with
200
+ the framework peers listed below; the capture adapter runs on a Start server. Build entries
201
+ load your app through Vite, parse routes natively, or drive Node I/O.
33
202
 
34
203
  | Entry | Runs in | Exports | Peers |
35
204
  | ------------------ | ----------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------- |
36
- | `pagegraph` | runtime (Worker, SSR, browser) | `buildSeoGraph`, `contentCollection`, `renderSitemap`, `renderRobots`, `pageHeads`, `graphToJson`/`graphFromJson`, checks, `inspectHtml` | — |
37
- | `pagegraph/react` | runtime | `createSeo` → `seo.head`, `seoHead`, `Breadcrumbs`, JSON-LD generators | `react`, `@tanstack/react-router` |
38
- | `pagegraph/tanstack-start/server` | runtime (Start server routes) | `robotsTxt`, `sitemapXml`, `seoGraph`, `seoSite` | the `pagegraph()` plugin |
39
- | `pagegraph/tanstack-start` | build (`vite.config.ts`, `seo.config.ts`) | `pagegraph()` Vite plugin, `tanstackStartGraph`, `evaluateAppGraph` | `vite`, `@tanstack/router-generator` |
205
+ | `pagegraph` | runtime (Worker, SSR, browser) | `buildSeoGraph`, `contentCollection`, `renderSitemap`, `renderRobots`, `pageHeads`, `graphToJson`/`graphFromJson`, checks, `inspectHtml`, document types, facts, markdown and locks | — |
206
+ | `pagegraph/react` | runtime | `createSeo` → `seo.head`, `seoHead`, `Breadcrumbs`, JSON-LD generators, authored page primitives | `react`, `@tanstack/react-router` |
207
+ | `pagegraph/tanstack-start/server` | runtime (Start server routes) | `robotsTxt`, `sitemapXml`, `llmsTxt`, `llmsSection`, `seoGraph`, `seoSite` | the `pagegraph()` plugin |
208
+ | `pagegraph/tanstack-start/markdown` | runtime (Start server) | `markdownRequest`, `markdownPagePaths` | `react`, `react-dom`, `@tanstack/react-start`, `@tanstack/react-router` |
209
+ | `pagegraph/tanstack-start/react` | runtime | capture-aware `Link` | `react`, `@tanstack/react-router` |
210
+ | `pagegraph/tanstack-start/prerender-worker` | non-deployed prerender Worker | compiled Start server | the `pagegraph()` plugin |
211
+ | `pagegraph/tanstack-start` | build (`vite.config.ts`, `pagegraph.config.ts`) | `pagegraph()` Vite plugin, `tanstackStartGraph`, `evaluateAppGraph` | `vite`, `@tanstack/router-generator`, `effect` |
40
212
  | `pagegraph/vite` | build (`vite.config.ts`) | `seoRouteConfig` coverage gate | `vite`, `@tanstack/router-generator` |
41
- | `pagegraph/config` | build (`seo.config.ts`, CLI) | `defineSeoConfig`, `viteGraphLoader`, `loadPageHeads` | `vite` |
213
+ | `pagegraph/config` | build (`pagegraph.config.ts`, CLI) | `defineSeoConfig`, `viteGraphLoader`, `loadPageHeads` | `vite` |
214
+ | `pagegraph/claims` | build / CLI (Node or Bun) | `claimsInput`, `claimsFamily`, document resolution, committed claims checks | `effect` |
42
215
  | `pagegraph/audit` | build / CLI (Node or Bun) | Audit services, scanner protocol, rules, and report schemas | `effect` |
216
+ | `pagegraph/oxlint` | build (oxlint) | `pagegraph` plugin: `no-bare-text`, `t-children` | — |
43
217
  | `pagegraph` bin | CLI (Bun) | CLI over the same graph | bundled |
44
218
 
45
219
  Build entries resolve to a stub under the `workerd`, `worker`, and `browser` export conditions.
@@ -195,15 +369,14 @@ import { robotsTxt } from "pagegraph/tanstack-start/server";
195
369
  export const Route = createFileRoute("/robots.txt")({ server: { handlers: { GET: robotsTxt } } });
196
370
  ```
197
371
 
198
- **5. Gate CI.** The CLI reads the same graph through `seo.config.ts`:
372
+ **5. Gate CI.** The CLI reads the same graph through `pagegraph.config.ts`:
199
373
 
200
374
  ```ts
201
- // seo.config.ts
375
+ // pagegraph.config.ts
202
376
  import { defineSeoConfig } from "pagegraph/config";
203
377
  import { tanstackStartGraph } from "pagegraph/tanstack-start";
204
378
 
205
379
  export default defineSeoConfig({
206
- origin: "https://example.com",
207
380
  loadGraph: tanstackStartGraph({ root: import.meta.dirname }),
208
381
  });
209
382
  ```
@@ -217,7 +390,7 @@ pagegraph check
217
390
  `buildSeoGraph({ routeTree, collections })` builds the same graph from any TanStack
218
391
  Router tree; render it with `renderSitemap(graph, { origin, indexable })` and
219
392
  `renderRobots(graph, { origin, indexable, disallow, contentSignal })`, and point
220
- `seo.config.ts` at it with `viteGraphLoader` (see [CLI](#cli)).
393
+ `pagegraph.config.ts` at it with `viteGraphLoader` (see [CLI](#cli)).
221
394
 
222
395
  `renderRobots` does not invent a Content-Signal. Pass `contentSignal` for
223
396
  the value (the plugin prefixes `Content-Signal: `), `directives` for extra
@@ -239,7 +412,7 @@ The generated `.pagegraph/opencode` directory contains one `seo` agent and custo
239
412
  the plugin own model and Executor authentication. PageGraph does not copy credentials or assume
240
413
  fixed provider-tool addresses.
241
414
 
242
- Enable workflows in `seo.config.ts`:
415
+ Enable workflows in `pagegraph.config.ts`:
243
416
 
244
417
  ```ts
245
418
  export default defineSeoConfig({
@@ -406,27 +579,106 @@ It also derives a small config module from the route tree: `robotsExclusions`
406
579
  (the plugin adds them to robots.txt; a Router-only app feeds them to `renderRobots`) and
407
580
  `reservedSegments` (top-level segments an app must not hand out as tenant/org slugs).
408
581
 
582
+ ## Claims gate
583
+
584
+ Declare probability rules against `claimsInput` from the build-only `pagegraph/claims` entry.
585
+ Each rule asks whether the section violates your policy:
586
+
587
+ ```ts
588
+ import { Decision } from "effect/ai";
589
+ import { claimsInput } from "pagegraph/claims";
590
+ import { pagegraph } from "pagegraph/tanstack-start";
591
+
592
+ const rules = Decision.make({
593
+ input: claimsInput,
594
+ decisions: {
595
+ unsupportedPromise: Decision.probability({
596
+ instructions: "Does this section promise a capability unsupported by its facts or evidence?",
597
+ }),
598
+ },
599
+ });
600
+
601
+ const graph = pagegraph({
602
+ origin: "https://example.com",
603
+ markdown: { origin: "https://example.com" },
604
+ facts: "src/facts.ts",
605
+ claims: { rules, model: "typesafe/jev", cutoff: 0.8 },
606
+ });
607
+ ```
608
+
609
+ Pass `graph.prerenderPages` to Start as shown in the rendered Markdown setup. The build persists
610
+ captured documents and graph heads, then replays committed answers without a model or credentials.
611
+ Missing answers and violations fail the build and name the page, section, and rule.
612
+
613
+ Run `pagegraph claims check` after changing prose, facts, context, or rules, then review and commit
614
+ `.pagegraph/decisions/claims/`. The cache key includes the rule definition fingerprint, model, and
615
+ input hash. Cached answers are reused; `--refresh` asks again and replaces them. The command reports
616
+ cached and asked counts and exits nonzero on violations. Set `TYPESAFE_API_KEY` when answers need
617
+ asking. `pagegraph check` also replays configured claims without model calls.
618
+
619
+ ```sh
620
+ pagegraph claims check
621
+ pagegraph claims check --refresh
622
+ pagegraph claims check --dev http://localhost:3000
623
+ ```
624
+
625
+ `claims.context` supplies policy text to every input. Inputs include the section's authored
626
+ Markdown, messages, all code-owned facts, and other sections marked as evidence. Page metadata is
627
+ judged as a `head` section, including graph pages outside rendered capture. `excludeHeads` accepts
628
+ path globs to omit metadata while keeping captured sections in the gate. The default cutoff is
629
+ `0.8` only when omitted; any probability at or above the configured cutoff is a violation, and
630
+ values below it pass. Changing the cutoff reuses answers and recalculates verdicts.
631
+
632
+ ## Markdown CLI
633
+
634
+ Inspect captured documents, search page metadata and sections, and maintain a reviewable lock:
635
+
636
+ ```sh
637
+ pagegraph markdown show /pricing
638
+ pagegraph markdown find email
639
+ pagegraph markdown lock
640
+ pagegraph markdown lock --check
641
+ ```
642
+
643
+ `show` prints the page's Markdown twin. `find` reports matching pages and sections.
644
+ `lock` writes `.pagegraph/markdown.lock.json` from document and message hashes, including
645
+ head-only pages; `--check` exits nonzero if the lock is missing or differs. These commands use
646
+ `.pagegraph/documents/` and `.pagegraph/heads.json` from the build. Use `--dev <origin>` to inspect
647
+ the development bundle instead. `--json` emits structured output using the CLI's usual conventions.
648
+ Markdown and claims settings come from `pagegraph()` in Vite, including the canonical Markdown
649
+ origin and facts module. Built-mode commands read Vite with `command: "build"` and
650
+ `mode: "production"`; `--dev` reads `command: "serve"` and `mode: "development"`.
651
+ Put CLI policies and the graph loader in `pagegraph.config.ts`.
652
+
409
653
  ## CLI
410
654
 
411
- The graph commands acquire your graph through `seo.config.ts` at the app root. A
655
+ The graph commands acquire your graph through `pagegraph.config.ts` at the app root.
656
+ Rename an existing `seo.config.ts`, `.js`, or `.mjs` to its `pagegraph.config.*` equivalent.
657
+ The file holds CLI policies and `loadGraph`; site identity and robots policy come from the
658
+ loader result, and Markdown and claims settings come from the Vite plugin. A
412
659
  TanStack Start app uses `tanstackStartGraph` (above), which also supplies the robots
413
- policy. Otherwise `viteGraphLoader` evaluates your graph module inside a headless Vite
414
- server, so path aliases, content plugins, and virtual modules all resolve:
660
+ policy and site identity from `pagegraph()`, using build settings in production mode by default.
661
+ Pass `command: "serve", mode: "development"` to inspect a development configuration instead.
662
+ Otherwise `viteGraphLoader` evaluates your graph
663
+ module inside a headless Vite server, so path aliases, content plugins, and virtual modules all resolve:
415
664
 
416
665
  ```ts
417
- // seo.config.ts
666
+ // pagegraph.config.ts
418
667
  import { defineSeoConfig, viteGraphLoader } from "pagegraph/config";
668
+ import { routeConfig } from "./src/lib/route-config";
419
669
 
420
670
  export default defineSeoConfig({
421
- origin: "https://example.com",
422
- disallow: routeConfig.robotsExclusions,
423
- contentSignal: "search=yes, ai-input=yes, ai-train=yes",
424
671
  // Fail `check` unless each named money page has enough contextual links.
425
672
  coverage: [{ path: "/pricing", minInbound: 2 }, { path: "/features/*", minInbound: 1 }],
426
673
  loadGraph: viteGraphLoader({
427
674
  root: import.meta.dirname,
428
675
  entry: "/lib/seo/graph.ts",
429
676
  exportName: "loadSeoGraph",
677
+ site: {
678
+ origin: "https://example.com",
679
+ indexable: true,
680
+ robots: { disallow: routeConfig.robotsExclusions, contentSignal: "search=yes, ai-input=yes, ai-train=yes" },
681
+ },
430
682
  }),
431
683
  });
432
684
  ```
@@ -461,7 +713,7 @@ pagegraph graph # the graph as a tree · --format mermaid | json
461
713
  pagegraph inspect /pricing # one node: policy, sitemap status, in/out edges
462
714
  pagegraph inspect <url> --live # fetch a deployed page, validate its rendered <head>
463
715
  pagegraph links verify <url> # crawl served HTML: depth, orphans, declared-vs-rendered
464
- pagegraph links verify <url> --assert-coverage # also assert seo.config.ts coverage on served anchors
716
+ pagegraph links verify <url> --assert-coverage # also assert pagegraph.config.ts coverage on served anchors
465
717
  pagegraph links verify <url> --emit-rendered <file> # save the rendered edge set
466
718
  pagegraph links candidates # propose contextual links from the declared graph
467
719
  pagegraph research keywords --query "email api" # combine graph, Executor, and Jev evidence
@@ -477,7 +729,7 @@ Stdout is data, stderr is status — `pagegraph check --json | jq` just works.
477
729
  `pagegraph links verify` crawls a site's served HTML — through the same DNS-pinned,
478
730
  private-IP-blocked HTTP path as `pagegraph audit` — and reports what a crawler
479
731
  actually receives: the real homepage depth, the pages with no incoming internal
480
- edge (rendered orphans), and, when the app has a `seo.config.ts`, the
732
+ edge (rendered orphans), and, when the app has a `pagegraph.config.ts`, the
481
733
  declared-vs-rendered link diff. It is bounded with `--limit` and needs no
482
734
  framework or config. Discovery reads same-origin `robots.txt` and bounded
483
735
  sitemaps before following page anchors, so a sitemap-only page can appear as an
@@ -532,7 +784,7 @@ It is also a drop-in input for `pagegraph links candidates --rendered`.
532
784
 
533
785
  ### Assert coverage on the rendered graph
534
786
 
535
- `pagegraph check` evaluates the `seo.config.ts` `coverage` rules against the
787
+ `pagegraph check` evaluates the `pagegraph.config.ts` `coverage` rules against the
536
788
  **declared** graph; `pagegraph links verify --assert-coverage` evaluates the same
537
789
  rules against the anchors a crawler **actually receives**. Only same-origin
538
790
  body-region anchors count — nav, header, and footer links are chrome — which is
@@ -549,7 +801,7 @@ non-zero) rather than reporting a false pass when the crawl outran `--limit`, a
549
801
  page failed to fetch (its anchors are missing), a body hit `--max-body-bytes`, the
550
802
  robots or sitemap discovery is incomplete, the
551
803
  config declares no `coverage`, or the artifact's origin differs from
552
- `seo.config.ts`. Under `--assert-coverage` the report gains a `coverage` block
804
+ `pagegraph.config.ts`. Under `--assert-coverage` the report gains a `coverage` block
553
805
  (`{ rules, ok, violations }`); the default `--json` summary is unchanged.
554
806
 
555
807
  ### Propose contextual links
@@ -615,7 +867,7 @@ both `links candidates --site` and `improve links --suggestions`.
615
867
 
616
868
  ### Contextual-link coverage
617
869
 
618
- Declare a contextual-link coverage policy in `seo.config.ts` (a `coverage` array
870
+ Declare a contextual-link coverage policy in `pagegraph.config.ts` (a `coverage` array
619
871
  of `{ path, minInbound }`), or pass repeatable `--require-inbound "<path-glob>=<n>"`
620
872
  flags to override it for one run. `pagegraph check` then fails (exit 1) unless
621
873
  every sitemap-eligible page matching the glob has at least `minInbound` incoming
@@ -709,7 +961,7 @@ undated rather than stale; give content its `modifiedAt` (or a route its
709
961
 
710
962
  ### Audit any website
711
963
 
712
- `pagegraph audit` is framework-independent and does not need `seo.config.ts`. It
964
+ `pagegraph audit` is framework-independent and does not need `pagegraph.config.ts`. It
713
965
  validates target URLs before making requests, follows redirects through the same
714
966
  validation boundary, inspects the rendered document and discovery files, and
715
967
  can collect Lighthouse evidence through a validating proxy.
package/dist/audit.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import "./content-B1cPtL8b.js";
2
- import * as Context from "effect/Context";
1
+ import "./content-DgNr7apS.js";
3
2
  import * as Effect from "effect/Effect";
3
+ import * as Context from "effect/Context";
4
4
  import * as Layer from "effect/Layer";
5
5
  import { Data, Schema } from "effect";
6
6
  import "node:net";
package/dist/audit.js CHANGED
@@ -1,12 +1,13 @@
1
- import { l as extractAnchors, r as extractPageContent, t as checkPageContent } from "./content-kcEnQdfi.js";
1
+ import { l as extractAnchors, r as extractPageContent, t as checkPageContent } from "./content-CHd0X4s6.js";
2
2
  import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
- import * as Context from "effect/Context";
5
4
  import * as Data$1 from "effect/Data";
6
5
  import * as Effect from "effect/Effect";
6
+ import * as Schema$1 from "effect/Schema";
7
+ import { createHash } from "node:crypto";
8
+ import * as Context from "effect/Context";
7
9
  import * as Layer from "effect/Layer";
8
10
  import { Data, Schema } from "effect";
9
- import { createHash } from "node:crypto";
10
11
  import { createServer, request } from "node:http";
11
12
  import { request as request$1 } from "node:https";
12
13
  import { BlockList, connect, isIP } from "node:net";
@@ -16,7 +17,6 @@ import { spawn } from "node:child_process";
16
17
  import { createRequire } from "node:module";
17
18
  import { tmpdir } from "node:os";
18
19
  import * as Option from "effect/Option";
19
- import * as Schema$1 from "effect/Schema";
20
20
  //#region src/audit/rules.ts
21
21
  const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
22
22
  const stringAt = (value, ...keys) => {
@@ -1,4 +1,4 @@
1
- import { t as buildOnlyError } from "../message-DT_CjUdt.js";
1
+ import { t as buildOnlyError } from "../message-CuBjgZ74.js";
2
2
  //#region src/build-only/audit.ts
3
3
  throw buildOnlyError("pagegraph/audit");
4
4
  //#endregion
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,6 @@
1
+ import { t as buildOnlyError } from "../message-CuBjgZ74.js";
2
+ //#region src/build-only/claims.ts
3
+ throw buildOnlyError("pagegraph/claims");
4
+ //#endregion
5
+
6
+ //# sourceMappingURL=claims.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claims.js","names":[],"sources":["../../src/build-only/claims.ts"],"sourcesContent":["import { buildOnlyError } from \"./message\";\n\nthrow buildOnlyError(\"pagegraph/claims\");\n"],"mappings":";;AAEA,MAAM,eAAe,kBAAkB"}
@@ -1,4 +1,4 @@
1
- import { t as buildOnlyError } from "../message-DT_CjUdt.js";
1
+ import { t as buildOnlyError } from "../message-CuBjgZ74.js";
2
2
  //#region src/build-only/config.ts
3
3
  throw buildOnlyError("pagegraph/config");
4
4
  //#endregion
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,6 @@
1
+ import { t as buildOnlyError } from "../message-CuBjgZ74.js";
2
+ //#region src/build-only/oxlint.ts
3
+ throw buildOnlyError("pagegraph/oxlint");
4
+ //#endregion
5
+
6
+ //# sourceMappingURL=oxlint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oxlint.js","names":[],"sources":["../../src/build-only/oxlint.ts"],"sourcesContent":["import { buildOnlyError } from \"./message\";\n\nthrow buildOnlyError(\"pagegraph/oxlint\");\n"],"mappings":";;AAEA,MAAM,eAAe,kBAAkB"}
@@ -1,4 +1,4 @@
1
- import { t as buildOnlyError } from "../message-DT_CjUdt.js";
1
+ import { t as buildOnlyError } from "../message-CuBjgZ74.js";
2
2
  //#region src/build-only/tanstack-start.ts
3
3
  throw buildOnlyError("pagegraph/tanstack-start");
4
4
  //#endregion
@@ -1,4 +1,4 @@
1
- import { t as buildOnlyError } from "../message-DT_CjUdt.js";
1
+ import { t as buildOnlyError } from "../message-CuBjgZ74.js";
2
2
  //#region src/build-only/vite.ts
3
3
  throw buildOnlyError("pagegraph/vite");
4
4
  //#endregion
@@ -328,7 +328,7 @@ function checkCoverage(graph, rules) {
328
328
  * distinction the declared pass draws by counting `related` edges alone.
329
329
  *
330
330
  * The target universe is the declared graph's sitemap-eligible pages: the globs
331
- * in `seo.config.ts` are written against declared paths, and the crawl is what
331
+ * in `pagegraph.config.ts` are written against declared paths, and the crawl is what
332
332
  * supplies the served edges. Pass the crawl's `internalEdges` (each edge carries
333
333
  * its `region`).
334
334
  */
@@ -349,4 +349,4 @@ const hasStructuralViolations = (violations) => violations.some((violation) => v
349
349
  //#endregion
350
350
  export { globToRegExp as a, checkRenderedCoverage as i, checkFreshness as n, hasStructuralViolations as o, checkGraph as r, freshnessReport as s, checkCoverage as t };
351
351
 
352
- //# sourceMappingURL=checks-uZk5Em9A.js.map
352
+ //# sourceMappingURL=checks-BS-aDXB_.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checks-BS-aDXB_.js","names":[],"sources":["../src/core/freshness.ts","../src/core/checks.ts"],"sourcesContent":["/**\n * Freshness: how long ago each indexable page last changed, by the dates its\n * declaration carries ({@link lastModified}). Pure — the caller injects `now`,\n * so a test and the CLI read the same verdict from the same graph.\n *\n * Only sitemap-eligible pages are judged: a noindex or redirect page is not a\n * refresh candidate. A page with no date is reported as `undated` rather than\n * stale, because age cannot be inferred; the sitemap omits its `<lastmod>` too.\n */\n\nimport type { SeoKind } from \"./declare\";\nimport type { SeoGraph, SeoSource } from \"./graph\";\nimport { isSitemapEligible, lastModified, parseDeclaredDate } from \"./projections\";\n\n/** Project policy: pages whose last change is older than this are stale. */\nexport interface FreshnessPolicy {\n readonly maxAgeDays: number;\n}\n\nexport interface FreshnessEntry {\n readonly path: string;\n readonly kind: SeoKind;\n readonly source: SeoSource;\n /** The declared date, normalized to ISO 8601. */\n readonly lastModified: string;\n /** Whole days between `lastModified` and the report's `asOf`. */\n readonly ageDays: number;\n}\n\nexport interface FreshnessReport {\n readonly asOf: string;\n readonly maxAgeDays: number;\n /** Pages older than `maxAgeDays`, oldest first: the refresh queue. */\n readonly stale: ReadonlyArray<FreshnessEntry>;\n /** Dated pages within `maxAgeDays`, newest first. */\n readonly fresh: ReadonlyArray<FreshnessEntry>;\n /** Sitemap-eligible pages that declare no date, in graph order. */\n readonly undated: ReadonlyArray<string>;\n /** Sitemap-eligible pages whose date does not parse; `check` fails on each. */\n readonly invalid: ReadonlyArray<string>;\n}\n\nconst DAY_MS = 86_400_000;\n\n/**\n * Split the graph's sitemap-eligible pages into stale, fresh, undated, and\n * invalid. An invalid date cannot be aged, but it is listed so a report never\n * reads as complete while it skipped a page.\n */\nexport function freshnessReport(\n graph: SeoGraph,\n policy: FreshnessPolicy,\n now: Date,\n): FreshnessReport {\n const stale: Array<FreshnessEntry> = [];\n const fresh: Array<FreshnessEntry> = [];\n const undated: Array<string> = [];\n const invalid: Array<string> = [];\n\n for (const node of graph.nodes.values()) {\n if (!isSitemapEligible(node)) continue;\n const declared = lastModified(node);\n if (declared === undefined) {\n undated.push(node.path);\n continue;\n }\n const time = parseDeclaredDate(declared);\n if (time === undefined) {\n invalid.push(node.path);\n continue;\n }\n const entry: FreshnessEntry = {\n path: node.path,\n kind: node.kind,\n source: node.source,\n lastModified: new Date(time).toISOString(),\n ageDays: Math.max(0, Math.floor((now.getTime() - time) / DAY_MS)),\n };\n (entry.ageDays > policy.maxAgeDays ? stale : fresh).push(entry);\n }\n\n stale.sort((a, b) => b.ageDays - a.ageDays || (a.path < b.path ? -1 : 1));\n fresh.sort((a, b) => a.ageDays - b.ageDays || (a.path < b.path ? -1 : 1));\n return {\n asOf: now.toISOString(),\n maxAgeDays: policy.maxAgeDays,\n stale,\n fresh,\n undated,\n invalid,\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 { freshnessReport, type FreshnessPolicy } from \"./freshness\";\nimport type { SeoGraph, SeoNode } from \"./graph\";\nimport type { LinkEdge } from \"./links\";\nimport { isSitemapEligible, parseDeclaredDate } 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 * `pagegraph.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 // Every declared date feeds the sitemap's `<lastmod>`, and the sitemap\n // projection refuses a date it cannot parse. Catch it here, at the\n // declaration, instead of as a failed sitemap request.\n name: \"invalid-date\",\n severity: \"structural\",\n evaluate: (graph) =>\n [...graph.nodes.values()].flatMap((node) => {\n const dates: Array<[string, string | undefined]> = node.instance\n ? [\n [\"publishedAt\", node.instance.publishedAt],\n [\"modifiedAt\", node.instance.modifiedAt],\n ]\n : [[\"modifiedAt\", node.policy.modifiedAt]];\n return dates.flatMap(([field, value]) =>\n value !== undefined && parseDeclaredDate(value) === undefined\n ? [\n {\n path: node.path,\n message: `${field} \"${value}\" is not an ISO 8601 date on a real calendar day.`,\n fix: `Write ${field} as an ISO 8601 date, e.g. \"2026-09-29\".`,\n },\n ]\n : [],\n );\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 * Options for {@link checkGraph}. Freshness needs a clock, and the engine stays\n * pure, so a freshness policy is only accepted together with the `now` it is\n * judged against.\n */\nexport type CheckGraphOptions = {\n readonly coverage?: ReadonlyArray<CoverageRule> | undefined;\n} & (\n | { readonly freshness: FreshnessPolicy; readonly now: Date }\n | { readonly freshness?: undefined; readonly now?: undefined }\n);\n\n/**\n * Run every static rule against the graph, then any caller-supplied\n * {@link CoverageRule}s and freshness policy, and return the flat list of\n * violations. With no options the result is exactly the static rule set.\n */\nexport function checkGraph(graph: SeoGraph, options: CheckGraphOptions = {}): 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 if (options.freshness !== undefined) {\n violations.push(...checkFreshness(graph, options.freshness, options.now));\n }\n return violations;\n}\n\n/**\n * One editorial `stale-page` violation per sitemap-eligible page whose last\n * declared change is older than the policy allows. Undated pages are not\n * violations: see {@link freshnessReport} for the full picture.\n */\nexport function checkFreshness(\n graph: SeoGraph,\n policy: FreshnessPolicy,\n now: Date,\n): Array<Violation> {\n return freshnessReport(graph, policy, now).stale.map((entry) => ({\n severity: \"editorial\",\n rule: \"stale-page\",\n path: entry.path,\n message: `Last changed ${entry.lastModified.slice(0, 10)}, ${entry.ageDays} days ago (limit ${policy.maxAgeDays}).`,\n fix: \"Review the page; when its content changes, update its modifiedAt.\",\n }));\n}\n\n/** Escape a glob for `RegExp`, then expand `**`, `*`, and `?` to path-aware forms. */\nexport const 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 `pagegraph.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"],"mappings":"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"}