pagegraph 0.11.0 → 0.12.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 (65) hide show
  1. package/README.md +140 -137
  2. package/dist/audit.d.ts +1 -1
  3. package/dist/audit.js +1 -1
  4. package/dist/build-only/audit.d.ts +1 -0
  5. package/dist/build-only/audit.js +6 -0
  6. package/dist/build-only/audit.js.map +1 -0
  7. package/dist/build-only/config.d.ts +1 -0
  8. package/dist/build-only/config.js +6 -0
  9. package/dist/build-only/config.js.map +1 -0
  10. package/dist/build-only/tanstack-start.d.ts +1 -0
  11. package/dist/build-only/tanstack-start.js +6 -0
  12. package/dist/build-only/tanstack-start.js.map +1 -0
  13. package/dist/build-only/vite.d.ts +1 -0
  14. package/dist/build-only/vite.js +6 -0
  15. package/dist/build-only/vite.js.map +1 -0
  16. package/dist/{checks-nk8ZRRIA.js → checks-uZk5Em9A.js} +3 -141
  17. package/dist/checks-uZk5Em9A.js.map +1 -0
  18. package/dist/cli.js +393 -287
  19. package/dist/cli.js.map +1 -1
  20. package/dist/config.d.ts +7 -66
  21. package/dist/config.js +3 -122
  22. package/dist/config.js.map +1 -1
  23. package/dist/{content-DVL2iyGU.d.ts → content-B1cPtL8b.d.ts} +4 -128
  24. package/dist/{content-CB_kHmJ3.js → content-kcEnQdfi.js} +3 -2
  25. package/dist/{content-CB_kHmJ3.js.map → content-kcEnQdfi.js.map} +1 -1
  26. package/dist/declare-D5o7hwmv.js +9 -0
  27. package/dist/declare-D5o7hwmv.js.map +1 -0
  28. package/dist/declare-DVgdEdOL.d.ts +12127 -0
  29. package/dist/graph-Bcsourlv.js +162 -0
  30. package/dist/graph-Bcsourlv.js.map +1 -0
  31. package/dist/graph-BxCY90vq.d.ts +104 -0
  32. package/dist/graph-CBIY71w6.d.ts +57 -0
  33. package/dist/index.d.ts +37 -73
  34. package/dist/index.js +23 -134
  35. package/dist/index.js.map +1 -1
  36. package/dist/message-DT_CjUdt.js +12 -0
  37. package/dist/message-DT_CjUdt.js.map +1 -0
  38. package/dist/page-heads-6y-CARRQ.js +25 -0
  39. package/dist/page-heads-6y-CARRQ.js.map +1 -0
  40. package/dist/page-heads-DNkjMUEN.d.ts +21 -0
  41. package/dist/projections-D0oOFKiY.js +142 -0
  42. package/dist/projections-D0oOFKiY.js.map +1 -0
  43. package/dist/projections-DeEFe7XT.d.ts +78 -0
  44. package/dist/react.d.ts +26 -12051
  45. package/dist/react.js +12 -3
  46. package/dist/react.js.map +1 -1
  47. package/dist/route-config-35ZEzkNC.js +308 -0
  48. package/dist/route-config-35ZEzkNC.js.map +1 -0
  49. package/dist/route-config-wneg6ktn.d.ts +29 -0
  50. package/dist/tanstack-start/index.d.ts +40 -0
  51. package/dist/tanstack-start/index.js +368 -0
  52. package/dist/tanstack-start/index.js.map +1 -0
  53. package/dist/tanstack-start/server.d.ts +14 -0
  54. package/dist/tanstack-start/server.js +53 -0
  55. package/dist/tanstack-start/server.js.map +1 -0
  56. package/dist/vite-graph-loader-01p1WxEq.js +104 -0
  57. package/dist/vite-graph-loader-01p1WxEq.js.map +1 -0
  58. package/dist/vite-graph-loader-Btgjz5RH.d.ts +56 -0
  59. package/dist/vite.d.ts +2 -28
  60. package/dist/vite.js +1 -307
  61. package/dist/wire-B4RQMc4e.js +26 -0
  62. package/dist/wire-B4RQMc4e.js.map +1 -0
  63. package/package.json +27 -5
  64. package/dist/checks-nk8ZRRIA.js.map +0 -1
  65. package/dist/vite.js.map +0 -1
@@ -0,0 +1,56 @@
1
+ import { a as SeoGraph } from "./graph-BxCY90vq.js";
2
+ import { r as RobotsConfig } from "./projections-DeEFe7XT.js";
3
+ import { PluginOption } from "vite";
4
+ //#region src/config/vite-graph-loader.d.ts
5
+ /** A graph, plus the release of whatever producing it acquired. */
6
+ interface LoadedSeoGraph {
7
+ readonly graph: SeoGraph;
8
+ /**
9
+ * The robots policy the app serves, when the loader knows it (the TanStack
10
+ * Start loader reads it from the `pagegraph()` plugin). `pagegraph robots`
11
+ * uses it instead of `seo.config.ts` fields.
12
+ */
13
+ readonly robots?: Pick<RobotsConfig, "disallow" | "contentSignal" | "directives"> | undefined;
14
+ /** Called once the command is done with the graph, on success or failure. */
15
+ readonly dispose: () => Promise<void>;
16
+ }
17
+ /**
18
+ * Produces the SEO graph for the `pagegraph` CLI. A plain promise on purpose: a
19
+ * config file must be writable without Effect, which the CLI keeps behind its
20
+ * `bin` (an optional peer dependency).
21
+ */
22
+ /** JSON input that can be supplied by config files to an app graph export. */
23
+ type GraphLoaderInput = null | boolean | number | string | ReadonlyArray<GraphLoaderInput> | {
24
+ readonly [key: string]: GraphLoaderInput;
25
+ };
26
+ type SeoGraphLoader<Input extends GraphLoaderInput | void = void> = (input: Input) => Promise<LoadedSeoGraph>;
27
+ interface ViteGraphLoaderOptions {
28
+ /** The app's Vite root — the directory its aliases and plugins resolve against. */
29
+ readonly root: string;
30
+ /** Module exporting the graph loader, root-relative (e.g. `/lib/seo/graph.ts`). */
31
+ readonly entry: string;
32
+ /** Named export on `entry` returning `Promise<SeoGraph>`. Defaults to `loadSeoGraph`. */
33
+ readonly exportName?: string | undefined;
34
+ /** Vite plugins the app's module graph needs — a content/MDX plugin, say. */
35
+ readonly plugins?: ReadonlyArray<PluginOption> | undefined;
36
+ /**
37
+ * Bare or virtual module ids to replace with inert source, so a module that
38
+ * only resolves inside the app's server runtime does not break the load.
39
+ */
40
+ readonly stubs?: Readonly<Record<string, string>> | undefined;
41
+ /**
42
+ * Variables to seed on `process.env` before the app's modules parse it. Vite
43
+ * exposes prefixed values through `import.meta.env`, so this is how a module
44
+ * that demands an env var at import time is satisfied. Existing values win —
45
+ * a real `.env` is never clobbered.
46
+ */
47
+ readonly env?: Readonly<Record<string, string>> | undefined;
48
+ }
49
+ /**
50
+ * Build a {@link SeoGraphLoader} that evaluates `entry` in a headless Vite
51
+ * server rooted at `root`, and returns what its `exportName` export resolves to.
52
+ */
53
+ declare const viteGraphLoader: <Input extends GraphLoaderInput | void = void>(options: ViteGraphLoaderOptions) => SeoGraphLoader<Input>;
54
+ //#endregion
55
+ export { viteGraphLoader as a, ViteGraphLoaderOptions as i, LoadedSeoGraph as n, SeoGraphLoader as r, GraphLoaderInput as t };
56
+ //# sourceMappingURL=vite-graph-loader-Btgjz5RH.d.ts.map
package/dist/vite.d.ts CHANGED
@@ -1,28 +1,2 @@
1
- import { Plugin } from "vite";
2
- //#region src/vite/index.d.ts
3
- interface SeoRouteConfigOptions {
4
- /** Absolute path the generated route-config module is written to. */
5
- outputPath: string;
6
- /** Route groups whose page leaves form the public surface. Include the parens. */
7
- publicGroups: ReadonlyArray<string>;
8
- /** Route groups where every page leaf must declare `staticData` or `head`. */
9
- enforceCoverageIn: ReadonlyArray<string>;
10
- /** Route groups whose page paths are disallowed in robots.txt (e.g. auth). */
11
- disallowGroups?: ReadonlyArray<string> | undefined;
12
- /** Public routes that live outside any group folder, so nothing discovers them. */
13
- extraPublicRoutes?: ReadonlyArray<string> | undefined;
14
- /** Reserved top-level segments that are not route-tree paths (e.g. an api mount). */
15
- reservedExtras?: ReadonlyArray<string> | undefined;
16
- /** Prefixes always disallowed in robots.txt, route tree or not. */
17
- alwaysDisallow?: ReadonlyArray<string> | undefined;
18
- /** Routes directory, relative to the Vite root. Defaults to `src/routes`. */
19
- routesDirectory?: string | undefined;
20
- }
21
- /**
22
- * Vite plugin that runs its own `@tanstack/router-generator` instance over the
23
- * routes directory and derives the route-config module from the result.
24
- */
25
- declare function seoRouteConfig(options: SeoRouteConfigOptions): Plugin;
26
- //#endregion
27
- export { SeoRouteConfigOptions, seoRouteConfig };
28
- //# sourceMappingURL=vite.d.ts.map
1
+ import { n as seoRouteConfig, t as SeoRouteConfigOptions } from "./route-config-wneg6ktn.js";
2
+ export { type SeoRouteConfigOptions, seoRouteConfig };
package/dist/vite.js CHANGED
@@ -1,308 +1,2 @@
1
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import { open, stat } from "node:fs/promises";
3
- import { basename, normalize, resolve } from "node:path";
4
- import { Generator, getConfig } from "@tanstack/router-generator";
5
- //#region src/vite/index.ts
6
- /**
7
- * Derives a route-config module from the parsed TanStack route tree.
8
- *
9
- * The route tree is the single source of truth for which paths exist; this
10
- * plugin projects it into the small config the host consumes at runtime:
11
- * - `robotsExclusions` — path prefixes disallowed in robots.txt (feed them to
12
- * `renderRobots`).
13
- * - `reservedSegments` — top-level path segments the app must not hand out (an
14
- * org slug, a tenant name). Derived from the full public route surface — the
15
- * `publicGroups` route groups plus the group-less `extraPublicRoutes` — which
16
- * is parsed here but not emitted: "which pages are public" already has a
17
- * canonical runtime answer in the SEO graph.
18
- *
19
- * It also enforces that every page in `enforceCoverageIn` declares its SEO: a
20
- * page leaf missing both `staticData` and `head` fails dev startup and build.
21
- *
22
- * **Why we run our own Generator.** The parse must come from
23
- * `@tanstack/router-generator` itself — a second, hand-rolled scanner would
24
- * drift from TanStack's group/param/layout conventions. But routing our plugin
25
- * through TanStack Start does not work: `start-plugin-core` builds its own
26
- * generator-plugin array and drops `router.plugins` on the floor
27
- * (tanstack/router#7768 — `plugins` is not public API there), and even when
28
- * forced through, the router unplugin wraps `generator.run()` in
29
- * `catch (e) { console.error(e) }`, so a plugin can never fail a build.
30
- *
31
- * So we instantiate `Generator` ourselves — `config.plugins` is natively
32
- * honored on our own instance, and because we own the `.run()` call site a
33
- * coverage violation throws straight out of `buildStart`. Cost: route files are
34
- * parsed twice at startup (~40 files, milliseconds) — the host's router plugin
35
- * still owns the real `routeTree.gen.ts`; ours is a shadow parse whose route
36
- * tree is thrown away (see {@link inMemoryWriteFs}).
37
- *
38
- * `onRouteTreeChanged` fires on every generator run, but the config is written
39
- * only when its content changes, and the output path lives outside the routes
40
- * directory, so regeneration never feeds back into the watcher.
41
- */
42
- const PLUGIN_NAME = "seo-route-config";
43
- /** Match a route's group folder (`filePath` is relative to the routes directory). */
44
- function groupOf(filePath, groups) {
45
- return groups.find((group) => filePath.startsWith(`${group}/`));
46
- }
47
- /**
48
- * A page leaf is a `.tsx` route file that is not a `route.tsx` layout. This
49
- * excludes layout wrappers, `.ts` server routes (robots.txt / sitemap.xml /
50
- * markdown mirrors), and any other non-page file — matching the leaves that
51
- * contribute a crawlable URL.
52
- */
53
- function isPageLeaf(node) {
54
- return node.filePath.endsWith(".tsx") && basename(node.filePath) !== "route.tsx";
55
- }
56
- /**
57
- * Convert a group-prefixed routePath (e.g. "/(marketing)/blog/$slug") into the
58
- * public URL form used by route-config ("/blog/*"): drop the group segment,
59
- * collapse the first dynamic/splat segment and everything after it to "*", and
60
- * trim the index trailing slash.
61
- */
62
- function routePathToPublicUrl(routePath) {
63
- const stripped = routePath.replace(/^\/\([^)]+\)/, "");
64
- if (stripped === "" || stripped === "/") return "/";
65
- const segments = stripped.split("/");
66
- const dynamicIndex = segments.findIndex((segment) => segment.startsWith("$"));
67
- if (dynamicIndex !== -1) return `${segments.slice(0, dynamicIndex).join("/")}/*`;
68
- return stripped.endsWith("/") ? stripped.slice(0, -1) : stripped;
69
- }
70
- /**
71
- * First path segment of each route, reserved so it can't be claimed as an org
72
- * slug. Strip the leading slash and a trailing "/*", then take the first
73
- * segment ("/features/email" -> "features").
74
- */
75
- function extractReservedSegments(routes) {
76
- const segments = /* @__PURE__ */ new Set();
77
- for (const route of routes) {
78
- const first = route.replace(/^\//, "").replace(/\/\*$/, "").split("/")[0];
79
- if (first) segments.add(first);
80
- }
81
- return Array.from(segments).sort();
82
- }
83
- /**
84
- * robots.txt disallow prefixes are directory prefixes, so a splat route
85
- * ("/reset-password/*") disallows the whole subtree ("/reset-password").
86
- */
87
- function normalizeRoutePrefix(route) {
88
- const cleaned = route.replace(/\/\*$/, "");
89
- return cleaned === "" ? "/" : cleaned;
90
- }
91
- function deriveRouteConfig(routeNodes, options) {
92
- const publicUrls = new Set(options.extraPublicRoutes);
93
- const disallowedUrls = /* @__PURE__ */ new Set();
94
- for (const node of routeNodes) {
95
- const group = groupOf(node.filePath, options.publicGroups);
96
- if (group === void 0 || !isPageLeaf(node) || node.routePath === void 0) continue;
97
- const url = routePathToPublicUrl(node.routePath);
98
- publicUrls.add(url);
99
- if (options.disallowGroups.includes(group)) disallowedUrls.add(url);
100
- }
101
- const reservedSegments = [...extractReservedSegments([...publicUrls].sort()), ...options.reservedExtras].sort();
102
- return {
103
- robotsExclusions: [.../* @__PURE__ */ new Set([...options.alwaysDisallow, ...[...disallowedUrls].map(normalizeRoutePrefix)])].sort(),
104
- reservedSegments
105
- };
106
- }
107
- /**
108
- * Every page in an enforced group must declare its search presence. A leaf that
109
- * passes neither `staticData` (from which the SEO graph reads `staticData.seo`)
110
- * nor `head` has no path to correct metadata.
111
- */
112
- function findCoverageViolations(routeNodes, options) {
113
- const violations = [];
114
- for (const node of routeNodes) {
115
- const group = groupOf(node.filePath, options.enforceCoverageIn);
116
- if (group === void 0 || !isPageLeaf(node)) continue;
117
- const props = node.createFileRouteProps;
118
- if (props?.has("staticData") || props?.has("head")) continue;
119
- violations.push(`${node.filePath} (${group} page)`);
120
- }
121
- return violations;
122
- }
123
- function coverageMessage(violations) {
124
- return `[${PLUGIN_NAME}] ${violations.length} page(s) declare neither staticData nor head. Every page in an SEO-enforced route group must declare its SEO — add staticData: { seo: ... } or a head() to each route:\n - ${violations.join("\n - ")}`;
125
- }
126
- const HEADER = `// Auto-generated by the ${PLUGIN_NAME} Vite plugin.\n// DO NOT EDIT MANUALLY — regenerated from the route tree on every dev/build.
127
- `;
128
- function renderRouteConfig(config) {
129
- return `${HEADER}
130
- export const routeConfig = ${JSON.stringify(config, null, 2)} as const;
131
-
132
- export type RouteConfig = typeof routeConfig;
133
- `;
134
- }
135
- function routeConfigGeneratorPlugin(options) {
136
- const { outputPath } = options;
137
- return {
138
- name: PLUGIN_NAME,
139
- init() {
140
- if (!existsSync(resolve(outputPath, ".."))) throw new Error(`[${PLUGIN_NAME}] output directory does not exist: ${resolve(outputPath, "..")}`);
141
- },
142
- onRouteTreeChanged({ routeNodes }) {
143
- const violations = findCoverageViolations(routeNodes, options);
144
- if (violations.length > 0) throw new Error(coverageMessage(violations));
145
- const content = renderRouteConfig(deriveRouteConfig(routeNodes, options));
146
- if ((existsSync(outputPath) ? readFileSync(outputPath, "utf8") : void 0) !== content) writeFileSync(outputPath, content);
147
- }
148
- };
149
- }
150
- /**
151
- * A `Generator` filesystem that reads real files but keeps every write in
152
- * memory, so our shadow parse cannot touch the repo.
153
- *
154
- * The Generator writes through `safeFileWrite`: `writeFile(<tmp>)`, then
155
- * `rename(<tmp>, <dest>)`. Two destinations exist, and neither may land:
156
- * - the route tree — we point it at a throwaway path and hold it in memory, so
157
- * the only route tree on disk stays the one the host's router plugin writes;
158
- * - route source files — the Generator normalizes them (`createFileRoute` ids,
159
- * code-split transforms). The host's generator already did that with its own
160
- * config; ours is not the host's, so such a write is dropped rather than
161
- * allowed to fight it.
162
- *
163
- * Reads and stats of real files go to disk, which keeps the Generator's mtime
164
- * cache honest: a route edited mid-watch is always seen.
165
- */
166
- function inMemoryWriteFs(routeTreePath) {
167
- const written = /* @__PURE__ */ new Map();
168
- let clock = 0n;
169
- const tick = () => clock += 1n;
170
- return {
171
- /**
172
- * The in-memory route tree is invisible to the watcher, so nothing else can
173
- * mark it stale. Bumping its mtime before each run is what makes the
174
- * Generator rebuild the tree — and therefore re-fire `onRouteTreeChanged`
175
- * — on every run, not only when the set of routes changes. Coverage and the
176
- * derived config depend on route *contents* (`staticData` / `head`), which
177
- * leave the route tree itself untouched.
178
- */
179
- touchRouteTree() {
180
- const file = written.get(routeTreePath);
181
- if (file) written.set(routeTreePath, {
182
- content: file.content,
183
- mtimeMs: tick()
184
- });
185
- },
186
- fs: {
187
- async stat(filePath) {
188
- const file = written.get(filePath);
189
- if (file) return {
190
- mtimeMs: file.mtimeMs,
191
- mode: 33188,
192
- uid: 0,
193
- gid: 0
194
- };
195
- const real = await stat(filePath, { bigint: true });
196
- return {
197
- mtimeMs: real.mtimeMs,
198
- mode: Number(real.mode),
199
- uid: Number(real.uid),
200
- gid: Number(real.gid)
201
- };
202
- },
203
- async readFile(filePath) {
204
- const file = written.get(filePath);
205
- if (file) return {
206
- stat: { mtimeMs: file.mtimeMs },
207
- fileContent: file.content
208
- };
209
- if (!existsSync(filePath)) return "file-not-existing";
210
- const handle = await open(filePath, "r");
211
- const fileStat = await handle.stat({ bigint: true });
212
- const fileContent = (await handle.readFile()).toString();
213
- await handle.close();
214
- return {
215
- stat: { mtimeMs: fileStat.mtimeMs },
216
- fileContent
217
- };
218
- },
219
- async writeFile(filePath, content) {
220
- written.set(filePath, {
221
- content,
222
- mtimeMs: tick()
223
- });
224
- },
225
- async rename(oldPath, newPath) {
226
- const file = written.get(oldPath);
227
- written.delete(oldPath);
228
- if (file && newPath === routeTreePath) written.set(newPath, {
229
- content: file.content,
230
- mtimeMs: tick()
231
- });
232
- },
233
- async chmod() {},
234
- async chown() {}
235
- }
236
- };
237
- }
238
- function resolveOptions(options) {
239
- return {
240
- outputPath: options.outputPath,
241
- publicGroups: options.publicGroups,
242
- enforceCoverageIn: options.enforceCoverageIn,
243
- disallowGroups: options.disallowGroups ?? [],
244
- extraPublicRoutes: options.extraPublicRoutes ?? [],
245
- reservedExtras: options.reservedExtras ?? [],
246
- alwaysDisallow: options.alwaysDisallow ?? [],
247
- routesDirectory: options.routesDirectory ?? "src/routes"
248
- };
249
- }
250
- /**
251
- * Vite plugin that runs its own `@tanstack/router-generator` instance over the
252
- * routes directory and derives the route-config module from the result.
253
- */
254
- function seoRouteConfig(options) {
255
- const resolved = resolveOptions(options);
256
- let generator;
257
- let touchRouteTree;
258
- let routesDirectory;
259
- /**
260
- * A throw here propagates: it fails `vite build` and dev startup, which is
261
- * the reason we own this call site instead of registering through Start.
262
- */
263
- const run = async (event) => {
264
- if (generator === void 0 || touchRouteTree === void 0) throw new Error(`[${PLUGIN_NAME}] generator was not initialized by configResolved`);
265
- touchRouteTree();
266
- await generator.run(event);
267
- };
268
- return {
269
- name: PLUGIN_NAME,
270
- enforce: "pre",
271
- configResolved(config) {
272
- const root = config.root;
273
- const cacheDirectory = resolve(root, `node_modules/.cache/${PLUGIN_NAME}`);
274
- routesDirectory = resolve(root, resolved.routesDirectory);
275
- const generatorConfig = getConfig({
276
- routesDirectory,
277
- generatedRouteTree: resolve(cacheDirectory, "route-tree.gen.ts"),
278
- tmpDir: resolve(cacheDirectory, "tmp"),
279
- plugins: [routeConfigGeneratorPlugin(resolved)],
280
- disableTypes: true,
281
- enableRouteTreeFormatting: false,
282
- disableLogging: true
283
- }, root);
284
- const memory = inMemoryWriteFs(generatorConfig.generatedRouteTree);
285
- touchRouteTree = memory.touchRouteTree;
286
- generator = new Generator({
287
- config: generatorConfig,
288
- root,
289
- fs: memory.fs
290
- });
291
- },
292
- async buildStart() {
293
- await run();
294
- },
295
- async watchChange(id, { event }) {
296
- const file = normalize(id);
297
- if (routesDirectory === void 0 || !file.startsWith(routesDirectory)) return;
298
- await run({
299
- path: file,
300
- type: event
301
- });
302
- }
303
- };
304
- }
305
- //#endregion
1
+ import { i as seoRouteConfig } from "./route-config-35ZEzkNC.js";
306
2
  export { seoRouteConfig };
307
-
308
- //# sourceMappingURL=vite.js.map
@@ -0,0 +1,26 @@
1
+ //#region src/core/wire.ts
2
+ function graphToJson(graph) {
3
+ return {
4
+ nodes: [...graph.nodes.values()].map((node) => ({
5
+ ...node,
6
+ policy: {
7
+ ...node.policy,
8
+ crumb: typeof node.policy.crumb === "function" ? void 0 : node.policy.crumb,
9
+ head: void 0
10
+ }
11
+ })),
12
+ edges: graph.edges,
13
+ collisions: graph.collisions
14
+ };
15
+ }
16
+ function graphFromJson(json) {
17
+ return {
18
+ nodes: new Map(json.nodes.map((node) => [node.path, node])),
19
+ edges: [...json.edges],
20
+ collisions: json.collisions
21
+ };
22
+ }
23
+ //#endregion
24
+ export { graphToJson as n, graphFromJson as t };
25
+
26
+ //# sourceMappingURL=wire-B4RQMc4e.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wire-B4RQMc4e.js","names":[],"sources":["../src/core/wire.ts"],"sourcesContent":["/**\n * The graph as JSON, for shipping a build-time graph to a runtime that cannot\n * rebuild it (a Worker serving sitemap.xml). Node order is preserved — the\n * sitemap lists collection instances in collection order — and a function\n * `crumb` (render-time only) is dropped.\n */\n\nimport type { SeoEdge, SeoGraph, SeoNode, SeoSource } from \"./graph\";\n\nexport interface SeoGraphJson {\n readonly nodes: ReadonlyArray<SeoNode>;\n readonly edges: ReadonlyArray<SeoEdge>;\n readonly collisions?: ReadonlyArray<{ readonly path: string; readonly sources: ReadonlyArray<SeoSource> }> | undefined;\n}\n\nexport function graphToJson(graph: SeoGraph): SeoGraphJson {\n return {\n nodes: [...graph.nodes.values()].map((node) => ({\n ...node,\n // `node.head` carries the head as data; the declaration's render-only\n // fields (JSON-LD, Open Graph overrides) stay in the route module.\n policy: {\n ...node.policy,\n crumb: typeof node.policy.crumb === \"function\" ? undefined : node.policy.crumb,\n head: undefined,\n },\n })),\n edges: graph.edges,\n collisions: graph.collisions,\n };\n}\n\nexport function graphFromJson(json: SeoGraphJson): SeoGraph {\n return {\n nodes: new Map(json.nodes.map((node) => [node.path, node])),\n edges: [...json.edges],\n collisions: json.collisions,\n };\n}\n"],"mappings":";AAeA,SAAgB,YAAY,OAA+B;CACzD,OAAO;EACL,OAAO,CAAC,GAAG,MAAM,MAAM,OAAO,CAAC,CAAC,CAAC,KAAK,UAAU;GAC9C,GAAG;GAGH,QAAQ;IACN,GAAG,KAAK;IACR,OAAO,OAAO,KAAK,OAAO,UAAU,aAAa,KAAA,IAAY,KAAK,OAAO;IACzE,MAAM,KAAA;GACR;EACF,EAAE;EACF,OAAO,MAAM;EACb,YAAY,MAAM;CACpB;AACF;AAEA,SAAgB,cAAc,MAA8B;CAC1D,OAAO;EACL,OAAO,IAAI,IAAI,KAAK,MAAM,KAAK,SAAS,CAAC,KAAK,MAAM,IAAI,CAAC,CAAC;EAC1D,OAAO,CAAC,GAAG,KAAK,KAAK;EACrB,YAAY,KAAK;CACnB;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pagegraph",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Route-declared SEO graph, audit, and agentic workflow toolkit for TanStack Start.",
5
5
  "keywords": [
6
6
  "seo",
@@ -46,17 +46,37 @@
46
46
  "types": "./dist/react.d.ts",
47
47
  "import": "./dist/react.js"
48
48
  },
49
+ "./tanstack-start/server": {
50
+ "types": "./dist/tanstack-start/server.d.ts",
51
+ "import": "./dist/tanstack-start/server.js"
52
+ },
49
53
  "./vite": {
50
54
  "types": "./dist/vite.d.ts",
55
+ "workerd": "./dist/build-only/vite.js",
56
+ "worker": "./dist/build-only/vite.js",
57
+ "browser": "./dist/build-only/vite.js",
51
58
  "import": "./dist/vite.js"
52
59
  },
53
60
  "./config": {
54
61
  "types": "./dist/config.d.ts",
62
+ "workerd": "./dist/build-only/config.js",
63
+ "worker": "./dist/build-only/config.js",
64
+ "browser": "./dist/build-only/config.js",
55
65
  "import": "./dist/config.js"
56
66
  },
57
67
  "./audit": {
58
68
  "types": "./dist/audit.d.ts",
69
+ "workerd": "./dist/build-only/audit.js",
70
+ "worker": "./dist/build-only/audit.js",
71
+ "browser": "./dist/build-only/audit.js",
59
72
  "import": "./dist/audit.js"
73
+ },
74
+ "./tanstack-start": {
75
+ "types": "./dist/tanstack-start/index.d.ts",
76
+ "workerd": "./dist/build-only/tanstack-start.js",
77
+ "worker": "./dist/build-only/tanstack-start.js",
78
+ "browser": "./dist/build-only/tanstack-start.js",
79
+ "import": "./dist/tanstack-start/index.js"
60
80
  }
61
81
  },
62
82
  "publishConfig": {
@@ -75,16 +95,14 @@
75
95
  "release:validate": "bun run scripts/validate-release.ts",
76
96
  "release": "bun run release:check && bun run tegami publish"
77
97
  },
78
- "dependencies": {
79
- "@tanstack/router-generator": "^1.167.21"
80
- },
81
98
  "devDependencies": {
82
- "@opencode/sdk": "2.0.22",
83
99
  "@effect/ai-typesafe": "4.0.0",
84
100
  "@effect/platform-bun": "4.0.0",
85
101
  "@effect/platform-node-shared": "4.0.0",
86
102
  "@effect/vitest": "4.0.0",
103
+ "@opencode/sdk": "2.0.22",
87
104
  "@tanstack/react-router": "^1.170.18",
105
+ "@tanstack/router-generator": "^1.167.21",
88
106
  "@types/node": "^24.0.0",
89
107
  "@types/react": "19.2.18",
90
108
  "effect": "4.0.0",
@@ -101,6 +119,7 @@
101
119
  "@effect/ai-typesafe": "^4.0.0",
102
120
  "@effect/platform-bun": "^4.0.0",
103
121
  "@tanstack/react-router": "^1.170.0",
122
+ "@tanstack/router-generator": "^1.167.21",
104
123
  "effect": "^4.0.0",
105
124
  "lighthouse": ">=13.0.0",
106
125
  "react": "^19.0.0",
@@ -113,6 +132,9 @@
113
132
  "@effect/platform-bun": {
114
133
  "optional": true
115
134
  },
135
+ "@tanstack/router-generator": {
136
+ "optional": true
137
+ },
116
138
  "effect": {
117
139
  "optional": true
118
140
  },
@@ -1 +0,0 @@
1
- {"version":3,"file":"checks-nk8ZRRIA.js","names":[],"sources":["../src/core/projections.ts","../src/core/freshness.ts","../src/core/checks.ts"],"sourcesContent":["/**\n * Projections of the SEO graph: sitemap.xml, robots.txt, and single-node\n * inspection. Pure functions — the origin, the host's indexability, and the\n * robots disallow list are injected by the caller, never read from the\n * environment or a generated file.\n */\n\nimport type { SeoEdge, SeoGraph, SeoNode } from \"./graph\";\n\nexport interface ProjectionConfig {\n origin: string;\n indexable: boolean;\n}\n\nexport interface RobotsConfig extends ProjectionConfig {\n /** Path prefixes to disallow on an indexable host (e.g. the app-only groups). */\n disallow: ReadonlyArray<string>;\n /**\n * Origin-wide Content-Signal preferences\n * (https://contentsignals.org/), emitted as `Content-Signal: <value>` under\n * `User-agent: *` on an indexable host. Omit for none. The plugin never\n * invents a default — pass the policy you want, e.g.\n * `\"search=yes, ai-input=yes, ai-train=yes\"`.\n */\n contentSignal?: string | undefined;\n /**\n * Extra full lines in the indexable `User-agent: *` group, after\n * Content-Signal (if any) and before Allow/Disallow. Use for directives the\n * plugin does not model, or to compose {@link contentSignal} yourself.\n */\n directives?: ReadonlyArray<string> | undefined;\n /**\n * Last-mile override: receives the rendered robots.txt and returns the file\n * to emit. Runs for indexable and preview hosts. Use when you need to wrap\n * or replace the default body rather than add group lines.\n */\n transform?: ((robots: string) => string) | undefined;\n}\n\nexport interface NodeReport {\n node: SeoNode;\n inSitemap: boolean;\n incoming: Array<SeoEdge>;\n outgoing: Array<SeoEdge>;\n}\n\nconst escapeXml = (value: string): string =>\n value\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\");\n\n/**\n * A node belongs in the sitemap when it declares a positive sitemap policy, is not\n * a redirect, is not robots-noindexed, and is not a param template (a route whose\n * path still contains a `$` segment — those exist only so their instances inherit).\n */\nexport function isSitemapEligible(node: SeoNode): boolean {\n if (node.policy.redirectTo !== undefined) return false;\n if (node.policy.robots?.includes(\"noindex\")) return false;\n if (!node.policy.sitemap) return false; // false or absent\n if (node.path.includes(\"$\")) return false;\n return true;\n}\n\n/** Canonical absolute URL for a node under the given origin. */\nfunction urlForNode(origin: string, node: SeoNode): string {\n return node.path === \"/\" ? origin : `${origin}${node.path}`;\n}\n\n/**\n * The date a node's content last changed, as declared: an instance's\n * `modifiedAt`, else its `publishedAt`; a route's own `modifiedAt`. A node with\n * no date (a docs page without frontmatter dates, an undated route) returns\n * undefined, and the sitemap omits its `<lastmod>`.\n */\nexport function lastModified(node: SeoNode): string | undefined {\n if (node.instance) return node.instance.modifiedAt ?? node.instance.publishedAt;\n return node.policy.modifiedAt;\n}\n\n/** An ISO 8601 date, or a date-time that states its offset. */\nconst ISO_DATE = /^(\\d{4})-(\\d{2})-(\\d{2})(?:T\\d{2}:\\d{2}(?::\\d{2}(?:\\.\\d+)?)?(?:Z|[+-]\\d{2}:\\d{2}))?$/;\n\n/**\n * Parse a declared date to epoch milliseconds, or undefined when it is not an\n * ISO 8601 date (`2026-09-29`) or offset date-time (`2026-09-29T10:00:00Z`) on a\n * real calendar day. `Date.parse` alone would accept \"Sep 29\" in the host's\n * zone and roll \"2026-02-30\" into March; either would publish a wrong\n * `<lastmod>`. The `invalid-date` check reports the undefined case, so\n * projections that meet one fail loudly.\n */\nexport function parseDeclaredDate(value: string): number | undefined {\n const match = ISO_DATE.exec(value);\n if (match === null) return undefined;\n const [year, month, day] = [Number(match[1]), Number(match[2]), Number(match[3])];\n const calendar = new Date(Date.UTC(year, month - 1, day));\n if (calendar.getUTCMonth() !== month - 1 || calendar.getUTCDate() !== day) return undefined;\n const time = Date.parse(value);\n return Number.isNaN(time) ? undefined : time;\n}\n\nfunction sitemapLastmod(node: SeoNode): string | undefined {\n const date = lastModified(node);\n if (date === undefined) return undefined;\n const time = parseDeclaredDate(date);\n if (time === undefined) {\n throw new Error(`\"${node.path}\" declares an invalid date \"${date}\"; run \\`pagegraph check\\`.`);\n }\n return new Date(time).toISOString();\n}\n\nfunction renderUrlEntry(url: string, lastmod: string | undefined, node: SeoNode): string {\n const sitemap = node.policy.sitemap;\n // isSitemapEligible guarantees a positive policy before this runs.\n const { changeFrequency, priority } = sitemap as { changeFrequency: string; priority: number };\n const lines = [` <url>`, ` <loc>${escapeXml(url)}</loc>`];\n if (lastmod) lines.push(` <lastmod>${lastmod}</lastmod>`);\n lines.push(\n ` <changefreq>${changeFrequency}</changefreq>`,\n ` <priority>${priority.toFixed(1)}</priority>`,\n ` </url>`,\n );\n return lines.join(\"\\n\");\n}\n\n/**\n * Render sitemap.xml from the graph. Each entry's `<lastmod>` is its\n * {@link lastModified} date: frontmatter for content instances, the declared\n * `modifiedAt` for routes. Route entries are sorted by path, then instances follow in collection order.\n * `indexable` is intentionally unused — the sitemap body is host-independent;\n * robots.txt is what gates crawling.\n */\nexport function renderSitemap(graph: SeoGraph, cfg: ProjectionConfig): string {\n const nodes = [...graph.nodes.values()];\n const staticNodes = nodes\n .filter((node) => node.source === \"route\" && isSitemapEligible(node))\n .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));\n const instanceNodes = nodes.filter((node) => node.source !== \"route\" && isSitemapEligible(node));\n\n const seen = new Set<string>();\n const entries: Array<string> = [];\n for (const node of [...staticNodes, ...instanceNodes]) {\n const url = urlForNode(cfg.origin, node);\n const key = url.toLowerCase().replace(/\\/$/, \"\");\n if (seen.has(key)) continue;\n seen.add(key);\n entries.push(renderUrlEntry(url, sitemapLastmod(node), node));\n }\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\\n${entries.join(\n \"\\n\",\n )}\\n</urlset>\\n`;\n}\n\n/** Format a Content-Signal robots.txt directive from the preference list. */\nexport function contentSignal(value: string): string {\n return `Content-Signal: ${value}`;\n}\n\nconst groupLines = (cfg: RobotsConfig): Array<string> => {\n const lines: Array<string> = [];\n if (cfg.contentSignal !== undefined && cfg.contentSignal !== \"\") {\n lines.push(contentSignal(cfg.contentSignal));\n }\n for (const directive of cfg.directives ?? []) {\n if (directive !== \"\") lines.push(directive);\n }\n return lines;\n};\n\n/**\n * Render robots.txt. A non-indexable host (previews) gets a disallow-all\n * with no Sitemap line; an indexable host disallows exactly the prefixes the caller\n * passes. Pages that declare `robots: noindex` are intentionally NOT added as\n * Disallow entries — a Disallow would stop crawlers reaching the page to read its\n * `noindex, follow` meta, so the graph's per-node robots policy never feeds this\n * list. `graph` is unused — kept for signature parity with the other projections,\n * which callers load the graph once for and pass to each.\n *\n * Origin-wide group directives (`contentSignal`, `directives`) are indexable-host\n * only. {@link RobotsConfig.transform} always runs last so a consumer can override\n * the whole file.\n */\nexport function renderRobots(_graph: SeoGraph, cfg: RobotsConfig): string {\n const rendered = cfg.indexable\n ? [\n \"User-agent: *\",\n ...groupLines(cfg),\n \"Allow: /\",\n ...cfg.disallow.map((path) => `Disallow: ${path}`),\n \"\",\n `Sitemap: ${cfg.origin}/sitemap.xml`,\n `Host: ${cfg.origin}`,\n \"\",\n ].join(\"\\n\")\n : [\"User-agent: *\", \"Disallow: /\", \"\"].join(\"\\n\");\n return cfg.transform === undefined ? rendered : cfg.transform(rendered);\n}\n\n/** Inspect a single node: its declaration, sitemap eligibility, and edges. */\nexport function inspectNode(graph: SeoGraph, path: string): NodeReport | undefined {\n const node = graph.nodes.get(path);\n if (!node) return undefined;\n return {\n node,\n inSitemap: isSitemapEligible(node),\n incoming: graph.edges.filter((edge) => edge.to === path),\n outgoing: graph.edges.filter((edge) => edge.from === path),\n };\n}\n","/**\n * 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 * `seo.config.ts`), because \"which pages are money pages\" is app knowledge.\n *\n * The same rule shape is evaluated twice: `checkCoverage` counts declared\n * `related` edges, and `checkRenderedCoverage` counts the contextual anchors a\n * crawl actually served. The target universe is always the declared graph's\n * sitemap-eligible pages — the rendered pass only swaps the edge source.\n */\nexport interface CoverageRule {\n /** Path glob: `*` matches within a segment, `**` matches across segments. */\n readonly path: string;\n /** Minimum incoming contextual links the matched page needs. */\n readonly minInbound: number;\n}\n\n/** A single finding before its rule's `severity`/`rule` name are attached. */\ninterface RawViolation {\n path?: string | undefined;\n message: string;\n fix?: string | undefined;\n}\n\ninterface CheckRule {\n readonly name: string;\n readonly severity: Severity;\n readonly evaluate: (graph: SeoGraph) => ReadonlyArray<RawViolation>;\n}\n\n/** A positive sitemap policy — the author asked for this page to be indexed. */\nconst hasPositiveSitemap = (sitemap: SitemapPolicy | false | undefined): sitemap is SitemapPolicy =>\n sitemap !== undefined && sitemap !== false;\n\n/** Content and manifest instances carry page-level title/description. */\nconst isInstance = (node: SeoNode): boolean => node.source !== \"route\";\n\n/** Group instance nodes by a present, non-empty string field for duplicate detection. */\nconst groupInstancesBy = (\n graph: SeoGraph,\n field: (node: SeoNode) => string | undefined,\n): Map<string, Array<SeoNode>> => {\n const groups = new Map<string, Array<SeoNode>>();\n for (const node of graph.nodes.values()) {\n if (!isInstance(node)) continue;\n const value = field(node)?.trim();\n if (!value) continue;\n const bucket = groups.get(value);\n if (bucket) bucket.push(node);\n else groups.set(value, [node]);\n }\n return groups;\n};\n\nconst DESCRIPTION_MAX = 160;\nconst DESCRIPTION_MIN = 50;\n\n/**\n * Every check, in one place. `checkGraph` runs them in order and stamps each\n * finding with its rule name and severity, so the output is grouped by rule and\n * deterministic (nodes iterate in graph insertion order, edges in array order).\n */\nconst CHECK_RULES: ReadonlyArray<CheckRule> = [\n {\n name: \"path-owner-collision\",\n severity: \"structural\",\n evaluate: (graph) =>\n (graph.collisions ?? []).map((collision) => ({\n path: collision.path,\n message: `Canonical path \"${collision.path}\" is owned by multiple sources: ${collision.sources.join(\", \")}.`,\n fix: \"Give every concrete page one canonical path and one graph owner.\",\n })),\n },\n {\n name: \"canonical-path-collision\",\n severity: \"structural\",\n evaluate: (graph) => {\n const groups = new Map<string, Array<string>>();\n for (const path of graph.nodes.keys()) {\n const canonical = path.toLowerCase().replace(/\\/$/, \"\") || \"/\";\n const paths = groups.get(canonical);\n if (paths) paths.push(path);\n else groups.set(canonical, [path]);\n }\n return [...groups.values()]\n .filter((paths) => paths.length > 1)\n .flatMap((paths) =>\n paths.map((path) => ({\n path,\n message: `Canonical path collides with ${paths.filter((candidate) => candidate !== path).join(\", \")}.`,\n fix: \"Use one lowercase, trailing-slash-normalized canonical path.\",\n })),\n );\n },\n },\n {\n name: \"self-edge\",\n severity: \"structural\",\n evaluate: (graph) =>\n graph.edges\n .filter((edge) => edge.from === edge.to)\n .map((edge) => ({\n path: edge.from,\n message: `${edge.type} edge points back to its own source node.`,\n fix: \"Remove the self-reference from the canonical manifest or route declaration.\",\n })),\n },\n {\n name: \"duplicate-edge\",\n severity: \"structural\",\n evaluate: (graph) => {\n const seen = new Set<string>();\n const duplicates: Array<RawViolation> = [];\n for (const edge of graph.edges) {\n const key = `${edge.from}\\u0000${edge.to}\\u0000${edge.type}`;\n if (seen.has(key)) {\n duplicates.push({\n path: edge.from,\n message: `Duplicate ${edge.type} edge from \"${edge.from}\" to \"${edge.to}\".`,\n fix: \"Declare each graph relationship exactly once.\",\n });\n } else {\n seen.add(key);\n }\n }\n return duplicates;\n },\n },\n {\n // A `related` or `redirect` edge points at a path with no node — the target\n // route/page was renamed or deleted and the declaration wasn't updated.\n name: \"dead-edge\",\n severity: \"structural\",\n evaluate: (graph) =>\n graph.edges\n .filter((edge) => edge.type !== \"crumb-parent\" && !graph.nodes.has(edge.to))\n .map((edge) => ({\n path: edge.from,\n message: `${edge.type} edge from \"${edge.from}\" points at \"${edge.to}\", which is not a node in the graph.`,\n fix: `Update the ${edge.type === \"redirect\" ? \"redirectTo\" : \"related\"} target on the \"${edge.from}\" route, or restore \"${edge.to}\".`,\n })),\n },\n {\n // A content instance with no usable title can't render a legible <title> or card.\n name: \"instance-missing-title\",\n severity: \"structural\",\n evaluate: (graph) =>\n [...graph.nodes.values()]\n .filter((node) => isInstance(node) && !node.instance?.title.trim())\n .map((node) => ({\n path: node.path,\n message: `Content page \"${node.path}\" has no title.`,\n fix: \"Add a `title` to the page frontmatter.\",\n })),\n },\n {\n // The declaration asks for the page to be in the sitemap yet also marks it\n // noindex — contradictory intent. The projection resolves it (noindex wins,\n // excluded), but the declaration should say one thing.\n name: \"sitemap-noindex-contradiction\",\n severity: \"structural\",\n evaluate: (graph) =>\n [...graph.nodes.values()]\n .filter(\n (node) =>\n hasPositiveSitemap(node.policy.sitemap) &&\n node.policy.robots?.toLowerCase().includes(\"noindex\"),\n )\n .map((node) => ({\n path: node.path,\n message: `\"${node.path}\" declares a sitemap policy but its robots value is \"${node.policy.robots}\".`,\n fix: \"Drop the sitemap policy (or set `sitemap: false`) on a noindex page, or remove the noindex robots value.\",\n })),\n },\n {\n // Robots declarations are house-convention lowercase: the sitemap projection\n // matches `includes(\"noindex\")` literally, so a miscased value (\"Noindex\")\n // would silently stay sitemap-eligible. This gate makes that unrepresentable.\n name: \"robots-not-lowercase\",\n severity: \"structural\",\n evaluate: (graph) =>\n [...graph.nodes.values()]\n .filter(\n (node) =>\n node.policy.robots !== undefined &&\n node.policy.robots !== node.policy.robots.toLowerCase(),\n )\n .map((node) => ({\n path: node.path,\n message: `\"${node.path}\" declares robots \"${node.policy.robots}\" — robots values must be lowercase.`,\n fix: 'Lowercase the robots declaration (e.g. \"noindex, follow\").',\n })),\n },\n {\n // A `related` card for a route target renders from that route's `link`\n // metadata; without it the card has no title/description and renders empty.\n // (Content-instance targets render from their frontmatter, so they're exempt.)\n name: \"related-target-missing-link\",\n severity: \"structural\",\n evaluate: (graph) => {\n const linklessTargets = new Set<string>();\n for (const edge of graph.edges) {\n if (edge.type !== \"related\") continue;\n const target = graph.nodes.get(edge.to);\n if (target && target.source === \"route\" && target.policy.link === undefined) {\n linklessTargets.add(edge.to);\n }\n }\n return [...linklessTargets].map((path) => ({\n path,\n message: `Route \"${path}\" is a related-link target but declares no link metadata; its card would render empty.`,\n fix: `Add \\`link: { title, description }\\` to the \"${path}\" route's staticData.seo.`,\n }));\n },\n },\n {\n // A redirect/alias node should never advertise itself in the sitemap.\n name: \"redirect-in-sitemap\",\n severity: \"structural\",\n evaluate: (graph) =>\n [...graph.nodes.values()]\n .filter(\n (node) => node.policy.redirectTo !== undefined && hasPositiveSitemap(node.policy.sitemap),\n )\n .map((node) => ({\n path: node.path,\n message: `Redirect \"${node.path}\" (→ \"${node.policy.redirectTo}\") also declares a sitemap policy.`,\n fix: \"Remove the sitemap policy from the redirect route; only its target belongs in the sitemap.\",\n })),\n },\n {\n // 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 `seo.config.ts` are written against declared paths, and the crawl is what\n * supplies the served edges. Pass the crawl's `internalEdges` (each edge carries\n * its `region`).\n */\nexport function checkRenderedCoverage(\n graph: SeoGraph,\n renderedEdges: ReadonlyArray<LinkEdge>,\n rules: ReadonlyArray<CoverageRule>,\n): Array<Violation> {\n const inbound = new Map<string, number>();\n for (const edge of renderedEdges) {\n if (edge.region !== \"body\") continue;\n inbound.set(edge.to, (inbound.get(edge.to) ?? 0) + 1);\n }\n return checkCoverageRules(\n {\n paths: [...graph.nodes.keys()],\n eligiblePaths: eligiblePathsOf(graph),\n inbound: (path) => inbound.get(path) ?? 0,\n },\n rules,\n );\n}\n\n/** Structural violations fail `pagegraph check`; editorial-only stays green. */\nexport const hasStructuralViolations = (violations: ReadonlyArray<Violation>): boolean =>\n violations.some((violation) => violation.severity === \"structural\");\n"],"mappings":"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"}