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.
- package/README.md +277 -25
- package/dist/audit.d.ts +2 -2
- package/dist/audit.js +4 -4
- package/dist/build-only/audit.js +1 -1
- package/dist/build-only/claims.d.ts +1 -0
- package/dist/build-only/claims.js +6 -0
- package/dist/build-only/claims.js.map +1 -0
- package/dist/build-only/config.js +1 -1
- package/dist/build-only/oxlint.d.ts +1 -0
- package/dist/build-only/oxlint.js +6 -0
- package/dist/build-only/oxlint.js.map +1 -0
- package/dist/build-only/tanstack-start.js +1 -1
- package/dist/build-only/vite.js +1 -1
- package/dist/{checks-uZk5Em9A.js → checks-BS-aDXB_.js} +2 -2
- package/dist/checks-BS-aDXB_.js.map +1 -0
- package/dist/claims-Bch7Lxlo.d.ts +92 -0
- package/dist/claims-Bv7voeEV.js +431 -0
- package/dist/claims-Bv7voeEV.js.map +1 -0
- package/dist/claims.d.ts +94 -0
- package/dist/claims.js +2 -0
- package/dist/cli.js +10095 -8318
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +4 -24
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/{content-kcEnQdfi.js → content-CHd0X4s6.js} +2 -2
- package/dist/{content-kcEnQdfi.js.map → content-CHd0X4s6.js.map} +1 -1
- package/dist/{content-B1cPtL8b.d.ts → content-DgNr7apS.d.ts} +4 -5
- package/dist/declare-D5o7hwmv.js.map +1 -1
- package/dist/document-CztEu3Vd.d.ts +109 -0
- package/dist/document-Dvemcv7V.js +379 -0
- package/dist/document-Dvemcv7V.js.map +1 -0
- package/dist/facts-1OKclPOk.js +115 -0
- package/dist/facts-1OKclPOk.js.map +1 -0
- package/dist/{declare-DVgdEdOL.d.ts → graph-D1PIndsC.d.ts} +117 -6
- package/dist/{graph-Bcsourlv.js → graph-DMNOLXLu.js} +23 -13
- package/dist/graph-DMNOLXLu.js.map +1 -0
- package/dist/index-C5w9ripz.d.ts +204 -0
- package/dist/index.d.ts +6 -160
- package/dist/index.js +37 -5
- package/dist/index.js.map +1 -1
- package/dist/inline-BbS93GAT.js +682 -0
- package/dist/inline-BbS93GAT.js.map +1 -0
- package/dist/llms-Dhr_5u2O.js +19 -0
- package/dist/llms-Dhr_5u2O.js.map +1 -0
- package/dist/markdown-C6dRE9zD.js +168 -0
- package/dist/markdown-C6dRE9zD.js.map +1 -0
- package/dist/markdown-path-lJB9BAVZ.js +6 -0
- package/dist/markdown-path-lJB9BAVZ.js.map +1 -0
- package/dist/{message-DT_CjUdt.js → message-CuBjgZ74.js} +2 -2
- package/dist/{message-DT_CjUdt.js.map → message-CuBjgZ74.js.map} +1 -1
- package/dist/oxlint.d.ts +58 -0
- package/dist/oxlint.js +202 -0
- package/dist/oxlint.js.map +1 -0
- package/dist/page-heads-BfjWQvau.d.ts +108 -0
- package/dist/{page-heads-6y-CARRQ.js → page-heads-BgbaIjj9.js} +3 -3
- package/dist/page-heads-BgbaIjj9.js.map +1 -0
- package/dist/{projections-DeEFe7XT.d.ts → projections-Bk-qOOsR.d.ts} +2 -2
- package/dist/react.d.ts +3 -2
- package/dist/react.js +2 -1
- package/dist/react.js.map +1 -1
- package/dist/tanstack-start/index.d.ts +29 -13
- package/dist/tanstack-start/index.js +391 -42
- package/dist/tanstack-start/index.js.map +1 -1
- package/dist/tanstack-start/markdown.d.ts +12 -0
- package/dist/tanstack-start/markdown.js +149 -0
- package/dist/tanstack-start/markdown.js.map +1 -0
- package/dist/tanstack-start/prerender-worker.d.ts +2 -0
- package/dist/tanstack-start/prerender-worker.js +7 -0
- package/dist/tanstack-start/prerender-worker.js.map +1 -0
- package/dist/tanstack-start/react.d.ts +7 -0
- package/dist/tanstack-start/react.js +12 -0
- package/dist/tanstack-start/react.js.map +1 -0
- package/dist/tanstack-start/server.d.ts +9 -3
- package/dist/tanstack-start/server.js +26 -2
- package/dist/tanstack-start/server.js.map +1 -1
- package/dist/{vite-graph-loader-01p1WxEq.js → vite-graph-loader-CsGiYNWE.js} +6 -5
- package/dist/vite-graph-loader-CsGiYNWE.js.map +1 -0
- package/dist/vite-graph-loader-DZCwejuJ.d.ts +133 -0
- package/package.json +44 -3
- package/dist/checks-uZk5Em9A.js.map +0 -1
- package/dist/graph-Bcsourlv.js.map +0 -1
- package/dist/graph-BxCY90vq.d.ts +0 -104
- package/dist/graph-CBIY71w6.d.ts +0 -57
- package/dist/page-heads-6y-CARRQ.js.map +0 -1
- package/dist/page-heads-DNkjMUEN.d.ts +0 -21
- package/dist/vite-graph-loader-01p1WxEq.js.map +0 -1
- 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
|
|
32
|
-
|
|
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` |
|
|
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 (`
|
|
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 `
|
|
372
|
+
**5. Gate CI.** The CLI reads the same graph through `pagegraph.config.ts`:
|
|
199
373
|
|
|
200
374
|
```ts
|
|
201
|
-
//
|
|
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
|
-
`
|
|
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 `
|
|
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 `
|
|
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
|
|
414
|
-
|
|
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
|
-
//
|
|
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
|
|
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 `
|
|
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 `
|
|
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
|
-
`
|
|
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 `
|
|
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 `
|
|
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-
|
|
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-
|
|
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) => {
|
package/dist/build-only/audit.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -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"}
|
package/dist/build-only/vite.js
CHANGED
|
@@ -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 `
|
|
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-
|
|
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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"}
|