pagegraph 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +277 -25
  2. package/dist/audit.d.ts +2 -2
  3. package/dist/audit.js +4 -4
  4. package/dist/build-only/audit.js +1 -1
  5. package/dist/build-only/claims.d.ts +1 -0
  6. package/dist/build-only/claims.js +6 -0
  7. package/dist/build-only/claims.js.map +1 -0
  8. package/dist/build-only/config.js +1 -1
  9. package/dist/build-only/oxlint.d.ts +1 -0
  10. package/dist/build-only/oxlint.js +6 -0
  11. package/dist/build-only/oxlint.js.map +1 -0
  12. package/dist/build-only/tanstack-start.js +1 -1
  13. package/dist/build-only/vite.js +1 -1
  14. package/dist/{checks-uZk5Em9A.js → checks-BS-aDXB_.js} +2 -2
  15. package/dist/checks-BS-aDXB_.js.map +1 -0
  16. package/dist/claims-Bch7Lxlo.d.ts +92 -0
  17. package/dist/claims-Bv7voeEV.js +431 -0
  18. package/dist/claims-Bv7voeEV.js.map +1 -0
  19. package/dist/claims.d.ts +94 -0
  20. package/dist/claims.js +2 -0
  21. package/dist/cli.js +10095 -8318
  22. package/dist/cli.js.map +1 -1
  23. package/dist/config.d.ts +4 -24
  24. package/dist/config.js +2 -2
  25. package/dist/config.js.map +1 -1
  26. package/dist/{content-kcEnQdfi.js → content-CHd0X4s6.js} +2 -2
  27. package/dist/{content-kcEnQdfi.js.map → content-CHd0X4s6.js.map} +1 -1
  28. package/dist/{content-B1cPtL8b.d.ts → content-DgNr7apS.d.ts} +4 -5
  29. package/dist/declare-D5o7hwmv.js.map +1 -1
  30. package/dist/document-CztEu3Vd.d.ts +109 -0
  31. package/dist/document-Dvemcv7V.js +379 -0
  32. package/dist/document-Dvemcv7V.js.map +1 -0
  33. package/dist/facts-1OKclPOk.js +115 -0
  34. package/dist/facts-1OKclPOk.js.map +1 -0
  35. package/dist/{declare-DVgdEdOL.d.ts → graph-D1PIndsC.d.ts} +117 -6
  36. package/dist/{graph-Bcsourlv.js → graph-DMNOLXLu.js} +23 -13
  37. package/dist/graph-DMNOLXLu.js.map +1 -0
  38. package/dist/index-C5w9ripz.d.ts +204 -0
  39. package/dist/index.d.ts +6 -160
  40. package/dist/index.js +37 -5
  41. package/dist/index.js.map +1 -1
  42. package/dist/inline-BbS93GAT.js +682 -0
  43. package/dist/inline-BbS93GAT.js.map +1 -0
  44. package/dist/llms-Dhr_5u2O.js +19 -0
  45. package/dist/llms-Dhr_5u2O.js.map +1 -0
  46. package/dist/markdown-C6dRE9zD.js +168 -0
  47. package/dist/markdown-C6dRE9zD.js.map +1 -0
  48. package/dist/markdown-path-lJB9BAVZ.js +6 -0
  49. package/dist/markdown-path-lJB9BAVZ.js.map +1 -0
  50. package/dist/{message-DT_CjUdt.js → message-CuBjgZ74.js} +2 -2
  51. package/dist/{message-DT_CjUdt.js.map → message-CuBjgZ74.js.map} +1 -1
  52. package/dist/oxlint.d.ts +58 -0
  53. package/dist/oxlint.js +202 -0
  54. package/dist/oxlint.js.map +1 -0
  55. package/dist/page-heads-BfjWQvau.d.ts +108 -0
  56. package/dist/{page-heads-6y-CARRQ.js → page-heads-BgbaIjj9.js} +3 -3
  57. package/dist/page-heads-BgbaIjj9.js.map +1 -0
  58. package/dist/{projections-DeEFe7XT.d.ts → projections-Bk-qOOsR.d.ts} +2 -2
  59. package/dist/react.d.ts +3 -2
  60. package/dist/react.js +2 -1
  61. package/dist/react.js.map +1 -1
  62. package/dist/tanstack-start/index.d.ts +29 -13
  63. package/dist/tanstack-start/index.js +391 -42
  64. package/dist/tanstack-start/index.js.map +1 -1
  65. package/dist/tanstack-start/markdown.d.ts +12 -0
  66. package/dist/tanstack-start/markdown.js +149 -0
  67. package/dist/tanstack-start/markdown.js.map +1 -0
  68. package/dist/tanstack-start/prerender-worker.d.ts +2 -0
  69. package/dist/tanstack-start/prerender-worker.js +7 -0
  70. package/dist/tanstack-start/prerender-worker.js.map +1 -0
  71. package/dist/tanstack-start/react.d.ts +7 -0
  72. package/dist/tanstack-start/react.js +12 -0
  73. package/dist/tanstack-start/react.js.map +1 -0
  74. package/dist/tanstack-start/server.d.ts +9 -3
  75. package/dist/tanstack-start/server.js +26 -2
  76. package/dist/tanstack-start/server.js.map +1 -1
  77. package/dist/{vite-graph-loader-01p1WxEq.js → vite-graph-loader-CsGiYNWE.js} +6 -5
  78. package/dist/vite-graph-loader-CsGiYNWE.js.map +1 -0
  79. package/dist/vite-graph-loader-DZCwejuJ.d.ts +133 -0
  80. package/package.json +44 -3
  81. package/dist/checks-uZk5Em9A.js.map +0 -1
  82. package/dist/graph-Bcsourlv.js.map +0 -1
  83. package/dist/graph-BxCY90vq.d.ts +0 -104
  84. package/dist/graph-CBIY71w6.d.ts +0 -57
  85. package/dist/page-heads-6y-CARRQ.js.map +0 -1
  86. package/dist/page-heads-DNkjMUEN.d.ts +0 -21
  87. package/dist/vite-graph-loader-01p1WxEq.js.map +0 -1
  88. package/dist/vite-graph-loader-Btgjz5RH.d.ts +0 -56
@@ -0,0 +1,133 @@
1
+ import { a as SeoGraph } from "./graph-D1PIndsC.js";
2
+ import { n as ClaimsOptions } from "./claims-Bch7Lxlo.js";
3
+ import { o as Facts } from "./page-heads-BfjWQvau.js";
4
+ import { r as RobotsConfig } from "./projections-Bk-qOOsR.js";
5
+ import { t as SeoRouteConfigOptions } from "./route-config-wneg6ktn.js";
6
+ import "@tanstack/router-generator";
7
+ import { PluginOption } from "vite";
8
+ //#region src/tanstack-start/graph.d.ts
9
+ /** The robots.txt policy a build serves; `origin` and indexability come from {@link SiteRuntime}. */
10
+ type RobotsPolicy = Pick<RobotsConfig, "disallow" | "contentSignal" | "directives">;
11
+ /** The graph and the site runtime one evaluation produces. */
12
+ interface AppGraph {
13
+ readonly graph: SeoGraph;
14
+ readonly site: SiteRuntime;
15
+ readonly markdown: {
16
+ readonly origin: string;
17
+ } | null;
18
+ readonly facts: Facts | undefined;
19
+ }
20
+ interface PagegraphOptions {
21
+ /** This build's canonical origin, no trailing slash, e.g. `https://example.com`. */
22
+ readonly origin: string;
23
+ /**
24
+ * Whether this build's host may be indexed. A non-indexable build serves a
25
+ * disallow-all robots.txt with no Sitemap line. Defaults to `true`.
26
+ */
27
+ readonly indexable?: boolean | undefined;
28
+ readonly robots?: {
29
+ /** Path prefixes disallowed on an indexable host, beyond the route-config exclusions. */
30
+ readonly disallow?: ReadonlyArray<string> | undefined;
31
+ /** `Content-Signal` value for an indexable host, e.g. `"search=yes, ai-input=yes, ai-train=yes"`. */
32
+ readonly contentSignal?: string | undefined;
33
+ /** Extra lines in the `User-agent: *` group on an indexable host. */
34
+ readonly directives?: ReadonlyArray<string> | undefined;
35
+ } | undefined;
36
+ /**
37
+ * Root-relative module whose `collections` export is a `SeoCollection[]`, or a
38
+ * function returning one (sync or async). It runs in the app's Vite pipeline,
39
+ * so content virtual modules (Fumadocs, MDX) and path aliases resolve.
40
+ */
41
+ readonly collections?: string | undefined;
42
+ /** Canonical origin for rendered documents; serverEntry is relative to the Vite root. */
43
+ readonly markdown?: {
44
+ readonly origin: string;
45
+ readonly serverEntry?: string | undefined;
46
+ } | undefined;
47
+ /** Committed claims answers gate the rendered documents and graph heads at build time. */
48
+ readonly claims?: ClaimsOptions | undefined;
49
+ /** Root-relative module exporting the code-owned document facts. */
50
+ readonly facts?: string | undefined;
51
+ /**
52
+ * Route files (globs relative to the routes directory) never evaluated for the
53
+ * graph: app surfaces that declare `staticData` but import server-runtime-only
54
+ * modules. Their paths stay out of the graph.
55
+ */
56
+ readonly exclude?: ReadonlyArray<string> | undefined;
57
+ /** Relative to the Vite root. Defaults to `src/routes`. */
58
+ readonly routesDirectory?: string | undefined;
59
+ /**
60
+ * Generate the route-config module and enforce per-group SEO coverage (the
61
+ * `seoRouteConfig` gate). Its robots exclusions join `robots.disallow`.
62
+ */
63
+ readonly routeConfig?: Omit<SeoRouteConfigOptions, "routesDirectory"> | undefined;
64
+ }
65
+ //#endregion
66
+ //#region src/config/vite-graph-loader.d.ts
67
+ /** Identity and robots policy produced by the graph's host. */
68
+ interface SiteRuntime {
69
+ readonly origin: string;
70
+ readonly indexable: boolean;
71
+ readonly robots: Pick<RobotsConfig, "disallow" | "contentSignal" | "directives">;
72
+ }
73
+ /** A graph, plus the release of whatever producing it acquired. */
74
+ interface LoadedSeoGraph {
75
+ readonly graph: SeoGraph;
76
+ /** The site's identity and policy belong to the graph-producing host. */
77
+ readonly site: SiteRuntime;
78
+ /** Settings and artifacts from the same resolved app that produced the graph. */
79
+ readonly pagegraph?: {
80
+ readonly root: string;
81
+ readonly options: PagegraphOptions;
82
+ readonly facts: Facts;
83
+ } | undefined;
84
+ /** Called once the command is done with the graph, on success or failure. */
85
+ readonly dispose: () => Promise<void>;
86
+ }
87
+ /**
88
+ * Produces the SEO graph for the `pagegraph` CLI. A plain promise on purpose: a
89
+ * config file must be writable without Effect, which the CLI keeps behind its
90
+ * `bin` (an optional peer dependency).
91
+ */
92
+ /** JSON input that can be supplied by config files to an app graph export. */
93
+ type GraphLoaderInput = null | boolean | number | string | ReadonlyArray<GraphLoaderInput> | {
94
+ readonly [key: string]: GraphLoaderInput;
95
+ };
96
+ /** Per-invocation evaluation settings, separate from the graph module's JSON input. */
97
+ interface GraphLoaderEnvironment {
98
+ readonly command?: "serve" | "build" | undefined;
99
+ readonly mode?: string | undefined;
100
+ }
101
+ type SeoGraphLoader<Input extends GraphLoaderInput | void = void> = (input: Input, environment?: GraphLoaderEnvironment) => Promise<LoadedSeoGraph>;
102
+ interface ViteGraphLoaderOptions {
103
+ /** The app's Vite root — the directory its aliases and plugins resolve against. */
104
+ readonly root: string;
105
+ /** Module exporting the graph loader, root-relative (e.g. `/lib/seo/graph.ts`). */
106
+ readonly entry: string;
107
+ /** Site identity and robots policy for this graph. */
108
+ readonly site: SiteRuntime;
109
+ /** Named export on `entry` returning `Promise<SeoGraph>`. Defaults to `loadSeoGraph`. */
110
+ readonly exportName?: string | undefined;
111
+ /** Vite plugins the app's module graph needs — a content/MDX plugin, say. */
112
+ readonly plugins?: ReadonlyArray<PluginOption> | undefined;
113
+ /**
114
+ * Bare or virtual module ids to replace with inert source, so a module that
115
+ * only resolves inside the app's server runtime does not break the load.
116
+ */
117
+ readonly stubs?: Readonly<Record<string, string>> | undefined;
118
+ /**
119
+ * Variables to seed on `process.env` before the app's modules parse it. Vite
120
+ * exposes prefixed values through `import.meta.env`, so this is how a module
121
+ * that demands an env var at import time is satisfied. Existing values win —
122
+ * a real `.env` is never clobbered.
123
+ */
124
+ readonly env?: Readonly<Record<string, string>> | undefined;
125
+ }
126
+ /**
127
+ * Build a {@link SeoGraphLoader} that evaluates `entry` in a headless Vite
128
+ * server rooted at `root`, and returns what its `exportName` export resolves to.
129
+ */
130
+ declare const viteGraphLoader: <Input extends GraphLoaderInput | void = void>(options: ViteGraphLoaderOptions) => SeoGraphLoader<Input>;
131
+ //#endregion
132
+ export { SiteRuntime as a, AppGraph as c, SeoGraphLoader as i, PagegraphOptions as l, GraphLoaderInput as n, ViteGraphLoaderOptions as o, LoadedSeoGraph as r, viteGraphLoader as s, GraphLoaderEnvironment as t, RobotsPolicy as u };
133
+ //# sourceMappingURL=vite-graph-loader-DZCwejuJ.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pagegraph",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Route-declared SEO graph, audit, and agentic workflow toolkit for TanStack Start.",
5
5
  "keywords": [
6
6
  "seo",
@@ -50,6 +50,18 @@
50
50
  "types": "./dist/tanstack-start/server.d.ts",
51
51
  "import": "./dist/tanstack-start/server.js"
52
52
  },
53
+ "./tanstack-start/markdown": {
54
+ "types": "./dist/tanstack-start/markdown.d.ts",
55
+ "import": "./dist/tanstack-start/markdown.js"
56
+ },
57
+ "./tanstack-start/react": {
58
+ "types": "./dist/tanstack-start/react.d.ts",
59
+ "import": "./dist/tanstack-start/react.js"
60
+ },
61
+ "./tanstack-start/prerender-worker": {
62
+ "types": "./dist/tanstack-start/prerender-worker.d.ts",
63
+ "import": "./dist/tanstack-start/prerender-worker.js"
64
+ },
53
65
  "./vite": {
54
66
  "types": "./dist/vite.d.ts",
55
67
  "workerd": "./dist/build-only/vite.js",
@@ -77,6 +89,20 @@
77
89
  "worker": "./dist/build-only/tanstack-start.js",
78
90
  "browser": "./dist/build-only/tanstack-start.js",
79
91
  "import": "./dist/tanstack-start/index.js"
92
+ },
93
+ "./oxlint": {
94
+ "types": "./dist/oxlint.d.ts",
95
+ "workerd": "./dist/build-only/oxlint.js",
96
+ "worker": "./dist/build-only/oxlint.js",
97
+ "browser": "./dist/build-only/oxlint.js",
98
+ "import": "./dist/oxlint.js"
99
+ },
100
+ "./claims": {
101
+ "types": "./dist/claims.d.ts",
102
+ "workerd": "./dist/build-only/claims.js",
103
+ "worker": "./dist/build-only/claims.js",
104
+ "browser": "./dist/build-only/claims.js",
105
+ "import": "./dist/claims.js"
80
106
  }
81
107
  },
82
108
  "publishConfig": {
@@ -96,18 +122,25 @@
96
122
  "release": "bun run release:check && bun run tegami publish"
97
123
  },
98
124
  "devDependencies": {
125
+ "@cloudflare/vite-plugin": "^1.62.5",
99
126
  "@effect/ai-typesafe": "4.0.0",
100
127
  "@effect/platform-bun": "4.0.0",
101
128
  "@effect/platform-node-shared": "4.0.0",
102
129
  "@effect/vitest": "4.0.0",
130
+ "@noble/hashes": "^2.4.0",
103
131
  "@opencode/sdk": "2.0.22",
104
- "@tanstack/react-router": "^1.170.18",
132
+ "@tanstack/react-router": "^1.170.26",
133
+ "@tanstack/react-start": "^1.168.56",
105
134
  "@tanstack/router-generator": "^1.167.21",
106
135
  "@types/node": "^24.0.0",
107
136
  "@types/react": "19.2.18",
137
+ "@types/react-dom": "19.2.3",
138
+ "@vitejs/plugin-react": "^6.0.6",
108
139
  "effect": "4.0.0",
109
140
  "lighthouse": "13.4.1",
141
+ "oxlint": "^1.83.0",
110
142
  "react": "19.2.8",
143
+ "react-dom": "19.2.8",
111
144
  "schema-dts": "^2.0.0",
112
145
  "tegami": "1.4.0",
113
146
  "tsdown": "^0.22.13",
@@ -123,7 +156,9 @@
123
156
  "effect": "^4.0.0",
124
157
  "lighthouse": ">=13.0.0",
125
158
  "react": "^19.0.0",
126
- "vite": "^8.0.0"
159
+ "vite": "^8.0.0",
160
+ "@tanstack/react-start": "^1.168.56",
161
+ "react-dom": "^19.0.0"
127
162
  },
128
163
  "peerDependenciesMeta": {
129
164
  "@effect/ai-typesafe": {
@@ -146,6 +181,12 @@
146
181
  },
147
182
  "vite": {
148
183
  "optional": true
184
+ },
185
+ "@tanstack/react-start": {
186
+ "optional": true
187
+ },
188
+ "react-dom": {
189
+ "optional": true
149
190
  }
150
191
  },
151
192
  "overrides": {
@@ -1 +0,0 @@
1
- {"version":3,"file":"checks-uZk5Em9A.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 * `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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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"graph-Bcsourlv.js","names":[],"sources":["../src/core/graph.ts"],"sourcesContent":["/**\n * SEO graph — the derived model that projections (sitemap, robots), the CLI, and\n * the check engine all read from. Built from route declarations (`staticData.seo`)\n * plus consumer-supplied content collections. This module is pure: no React, no\n * env, no knowledge of where instances come from. Origins and env-derived values\n * are injected by the callers of the projections, never read here.\n *\n * A node is one of:\n * - a structural route (`source: \"route\"`), keyed by its normalized full path,\n * merging a layout route's declaration (crumb) with its index child's (kind,\n * sitemap policy) when both resolve to the same URL;\n * - a content instance (`source` = the collection's label), keyed by the page URL\n * and carrying page-level metadata.\n *\n * Edges: `crumb-parent` (breadcrumb ancestry), `related` (deliberate cross-links),\n * `collection-member` (membership in a curated set), and `redirect` (route aliases).\n * The route walk emits crumb/related/redirect edges from declarations; a collection\n * may declare any additional edges its instances need.\n */\n\nimport type { AnyRoute } from \"@tanstack/react-router\";\n\nimport { applyTitleTemplate, type PublicPath, type RouteSeo, type SeoKind } from \"./declare\";\n\n/**\n * Where a node came from: `\"route\"` for a structural route declaration, or the\n * `source` label of the collection that produced the instance (e.g. \"blog\").\n */\nexport type SeoSource = string;\n\nexport interface SeoNode {\n /** Canonical path, no origin (e.g. \"/pricing\", \"/blog/my-post\"). */\n path: string;\n kind: SeoKind;\n source: SeoSource;\n /** Route-declared policy, or synthesized (kind + inherited sitemap) for instances. */\n policy: RouteSeo;\n /**\n * The head the page ships: a static route's declared `head`, or an instance's\n * title (through its route's `titleTemplate`) and description. Absent when the\n * route computes its head at request time and no collection supplies it.\n */\n head?: SeoNodeHead | undefined;\n instance?:\n | {\n title: string;\n description?: string | undefined;\n publishedAt?: string | undefined;\n modifiedAt?: string | undefined;\n }\n | undefined;\n}\n\n/**\n * A node's head as graph data: the full title, description, and FAQ text. An\n * instance without a description still carries its title.\n */\nexport interface SeoNodeHead {\n readonly title: string;\n readonly description?: string | undefined;\n readonly faqs?: ReadonlyArray<{ readonly question: string; readonly answer: string }> | undefined;\n}\n\nexport type SeoEdgeType = \"crumb-parent\" | \"related\" | \"redirect\" | \"collection-member\";\n\nexport interface SeoEdge {\n from: string;\n to: string;\n type: SeoEdgeType;\n}\n\nexport interface SeoGraph {\n nodes: Map<string, SeoNode>;\n edges: Array<SeoEdge>;\n /** Exact-path ownership conflicts encountered while assembling graph sources. */\n collisions?: ReadonlyArray<{ path: string; sources: ReadonlyArray<SeoSource> }> | undefined;\n}\n\n/** One concrete page produced by a collection. */\nexport interface SeoInstance {\n /** Canonical path of the page (e.g. \"/blog/my-post\"). */\n readonly path: string;\n readonly title: string;\n readonly description?: string | undefined;\n readonly publishedAt?: string | undefined;\n readonly modifiedAt?: string | undefined;\n}\n\nexport interface SeoCollection {\n /**\n * The param route these instances render through (e.g. \"/blog/$slug\"). Instances\n * inherit this route's declared policy (kind + sitemap) — the graph reads it from\n * the structural node, so declarations stay the single source of truth.\n */\n readonly route: PublicPath;\n /** The `source` stamped on every node this collection produces (e.g. \"blog\"). */\n readonly source: SeoSource;\n readonly instances: ReadonlyArray<SeoInstance>;\n /** Edges the collection declares between its instances and the rest of the graph. */\n readonly edges?: ReadonlyArray<SeoEdge> | undefined;\n}\n\n/**\n * The structural route shape the graph walks: a TanStack route tree has it\n * before `init()`, and an adapter can build it from parsed route files.\n */\nexport interface SeoRouteNode {\n readonly options: {\n readonly path?: string | undefined;\n readonly staticData?: { readonly seo?: RouteSeo | undefined } | undefined;\n };\n readonly children?: ReadonlyArray<SeoRouteNode> | undefined;\n}\n\nexport interface BuildSeoGraphInput {\n readonly routeTree: AnyRoute | SeoRouteNode;\n readonly collections?: ReadonlyArray<SeoCollection> | undefined;\n}\n\n/** Kind for instances whose collection route carries no declaration to inherit. */\nconst FALLBACK_KIND: SeoKind = \"page\";\n\n\n/**\n * Join a child's local path onto its parent's computed full path with the same\n * semantics as TanStack's route init (relative segments, index \"/\" inherits the\n * parent, pathless/group routes are transparent), then normalize trailing slashes.\n */\nfunction joinPath(parent: string, seg: string | undefined): string {\n if (seg === undefined) return parent; // pathless layout / route group\n if (seg === \"/\") return parent; // index route resolves to its parent's URL\n const trimmed = seg.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\");\n const joined = `${parent === \"/\" ? \"\" : parent}/${trimmed}`;\n return joined.replace(/\\/{2,}/g, \"/\");\n}\n\n/** A route's declared head as graph data: templated title, description, and plain FAQs. */\nfunction declaredHead(seo: RouteSeo): SeoNodeHead | undefined {\n if (seo.head === undefined) return undefined;\n return {\n title: applyTitleTemplate(seo.titleTemplate, seo.head.title),\n description: seo.head.description,\n faqs: seo.head.faqs?.map(({ question, answer }) => ({ question, answer })),\n };\n}\n\n/** Merge a route's declaration into an existing same-path node (deeper route wins). */\nfunction mergeSeo(base: RouteSeo, override: RouteSeo): RouteSeo {\n return {\n kind: override.kind,\n crumb: override.crumb ?? base.crumb,\n sitemap: override.sitemap ?? base.sitemap,\n robots: override.robots ?? base.robots,\n related: override.related ?? base.related,\n link: override.link ?? base.link,\n redirectTo: override.redirectTo ?? base.redirectTo,\n modifiedAt: override.modifiedAt ?? base.modifiedAt,\n head: override.head ?? base.head,\n titleTemplate: override.titleTemplate ?? base.titleTemplate,\n };\n}\n\n/**\n * Walk the route tree building structural nodes and crumb-parent edges. `crumbStack`\n * holds the paths of crumb-declaring ancestors so each crumb node links to its nearest\n * crumb ancestor down the real route-parent chain (matching render-time breadcrumbs).\n */\nfunction walkRoutes(\n route: SeoRouteNode,\n parentPath: string,\n isRoot: boolean,\n nodes: Map<string, SeoNode>,\n edges: Array<SeoEdge>,\n crumbStack: Array<string>,\n): void {\n const path = isRoot ? \"/\" : joinPath(parentPath, route.options.path);\n const seo = route.options.staticData?.seo;\n\n if (seo) {\n const existing = nodes.get(path);\n if (existing) {\n existing.policy = mergeSeo(existing.policy, seo);\n existing.kind = existing.policy.kind;\n // A head renders with its own route's template, never one merged from a layout.\n existing.head = declaredHead(seo) ?? existing.head;\n } else {\n nodes.set(path, { path, kind: seo.kind, source: \"route\", policy: { ...seo }, head: declaredHead(seo) });\n }\n\n const nearestCrumbAncestor = crumbStack[crumbStack.length - 1];\n if (seo.crumb !== undefined && nearestCrumbAncestor !== undefined) {\n edges.push({ from: path, to: nearestCrumbAncestor, type: \"crumb-parent\" });\n }\n }\n\n const pushedCrumb = seo?.crumb !== undefined;\n if (pushedCrumb) crumbStack.push(path);\n for (const child of route.children ?? []) {\n walkRoutes(child, path, false, nodes, edges, crumbStack);\n }\n if (pushedCrumb) crumbStack.pop();\n}\n\n/**\n * Add one collection's instance nodes, inheriting kind + sitemap policy from the\n * collection route's declaration, then append the edges it declares. A path already\n * owned by another node is a collision: the first owner keeps the path and the\n * conflict is reported (the `path-owner-collision` check turns it into a violation).\n */\nfunction addCollection(\n nodes: Map<string, SeoNode>,\n edges: Array<SeoEdge>,\n collisions: Array<{ path: string; sources: ReadonlyArray<SeoSource> }>,\n collection: SeoCollection,\n): void {\n const collectionNode = nodes.get(collection.route);\n const kind = collectionNode?.kind ?? FALLBACK_KIND;\n const sitemap = collectionNode?.policy.sitemap;\n const titleTemplate = collectionNode?.policy.titleTemplate;\n\n /**\n * Paths this collection lost to an earlier owner. Their instances never enter\n * the graph, so any edge declared out of them would dangle — and the dead-edge\n * check only validates an edge's `to`, so nothing downstream would catch it.\n */\n const rejected = new Set<string>();\n\n for (const instance of collection.instances) {\n const existing = nodes.get(instance.path);\n if (existing) {\n collisions.push({ path: instance.path, sources: [existing.source, collection.source] });\n rejected.add(instance.path);\n continue;\n }\n nodes.set(instance.path, {\n path: instance.path,\n kind,\n source: collection.source,\n policy: { kind, sitemap },\n head: { title: applyTitleTemplate(titleTemplate, instance.title), description: instance.description },\n instance: {\n title: instance.title,\n description: instance.description,\n publishedAt: instance.publishedAt,\n modifiedAt: instance.modifiedAt,\n },\n });\n }\n\n for (const edge of collection.edges ?? []) {\n if (rejected.has(edge.from)) continue;\n edges.push(edge);\n }\n}\n\n/**\n * Build the SEO graph from route declarations and content collections.\n *\n * Synchronous: the caller materializes its collections before calling, so there is\n * no async work here. Callers own the origin.\n */\nexport function buildSeoGraph(input: BuildSeoGraphInput): SeoGraph {\n const nodes = new Map<string, SeoNode>();\n const edges: Array<SeoEdge> = [];\n const collisions: Array<{ path: string; sources: ReadonlyArray<SeoSource> }> = [];\n\n walkRoutes(input.routeTree as unknown as SeoRouteNode, \"/\", true, nodes, edges, []);\n\n for (const collection of input.collections ?? []) {\n addCollection(nodes, edges, collisions, collection);\n }\n\n for (const node of nodes.values()) {\n if (node.source !== \"route\") continue;\n for (const to of node.policy.related ?? []) {\n edges.push({ from: node.path, to, type: \"related\" });\n }\n if (node.policy.redirectTo !== undefined) {\n edges.push({ from: node.path, to: node.policy.redirectTo, type: \"redirect\" });\n }\n }\n\n return { nodes, edges, collisions };\n}\n"],"mappings":";;;AAwHA,MAAM,gBAAyB;;;;;;AAQ/B,SAAS,SAAS,QAAgB,KAAiC;CACjE,IAAI,QAAQ,KAAA,GAAW,OAAO;CAC9B,IAAI,QAAQ,KAAK,OAAO;CACxB,MAAM,UAAU,IAAI,QAAQ,QAAQ,EAAE,CAAC,CAAC,QAAQ,QAAQ,EAAE;CAE1D,OAAO,GADW,WAAW,MAAM,KAAK,OAAO,GAAG,UACpC,QAAQ,WAAW,GAAG;AACtC;;AAGA,SAAS,aAAa,KAAwC;CAC5D,IAAI,IAAI,SAAS,KAAA,GAAW,OAAO,KAAA;CACnC,OAAO;EACL,OAAO,mBAAmB,IAAI,eAAe,IAAI,KAAK,KAAK;EAC3D,aAAa,IAAI,KAAK;EACtB,MAAM,IAAI,KAAK,MAAM,KAAK,EAAE,UAAU,cAAc;GAAE;GAAU;EAAO,EAAE;CAC3E;AACF;;AAGA,SAAS,SAAS,MAAgB,UAA8B;CAC9D,OAAO;EACL,MAAM,SAAS;EACf,OAAO,SAAS,SAAS,KAAK;EAC9B,SAAS,SAAS,WAAW,KAAK;EAClC,QAAQ,SAAS,UAAU,KAAK;EAChC,SAAS,SAAS,WAAW,KAAK;EAClC,MAAM,SAAS,QAAQ,KAAK;EAC5B,YAAY,SAAS,cAAc,KAAK;EACxC,YAAY,SAAS,cAAc,KAAK;EACxC,MAAM,SAAS,QAAQ,KAAK;EAC5B,eAAe,SAAS,iBAAiB,KAAK;CAChD;AACF;;;;;;AAOA,SAAS,WACP,OACA,YACA,QACA,OACA,OACA,YACM;CACN,MAAM,OAAO,SAAS,MAAM,SAAS,YAAY,MAAM,QAAQ,IAAI;CACnE,MAAM,MAAM,MAAM,QAAQ,YAAY;CAEtC,IAAI,KAAK;EACP,MAAM,WAAW,MAAM,IAAI,IAAI;EAC/B,IAAI,UAAU;GACZ,SAAS,SAAS,SAAS,SAAS,QAAQ,GAAG;GAC/C,SAAS,OAAO,SAAS,OAAO;GAEhC,SAAS,OAAO,aAAa,GAAG,KAAK,SAAS;EAChD,OACE,MAAM,IAAI,MAAM;GAAE;GAAM,MAAM,IAAI;GAAM,QAAQ;GAAS,QAAQ,EAAE,GAAG,IAAI;GAAG,MAAM,aAAa,GAAG;EAAE,CAAC;EAGxG,MAAM,uBAAuB,WAAW,WAAW,SAAS;EAC5D,IAAI,IAAI,UAAU,KAAA,KAAa,yBAAyB,KAAA,GACtD,MAAM,KAAK;GAAE,MAAM;GAAM,IAAI;GAAsB,MAAM;EAAe,CAAC;CAE7E;CAEA,MAAM,cAAc,KAAK,UAAU,KAAA;CACnC,IAAI,aAAa,WAAW,KAAK,IAAI;CACrC,KAAK,MAAM,SAAS,MAAM,YAAY,CAAC,GACrC,WAAW,OAAO,MAAM,OAAO,OAAO,OAAO,UAAU;CAEzD,IAAI,aAAa,WAAW,IAAI;AAClC;;;;;;;AAQA,SAAS,cACP,OACA,OACA,YACA,YACM;CACN,MAAM,iBAAiB,MAAM,IAAI,WAAW,KAAK;CACjD,MAAM,OAAO,gBAAgB,QAAQ;CACrC,MAAM,UAAU,gBAAgB,OAAO;CACvC,MAAM,gBAAgB,gBAAgB,OAAO;;;;;;CAO7C,MAAM,2BAAW,IAAI,IAAY;CAEjC,KAAK,MAAM,YAAY,WAAW,WAAW;EAC3C,MAAM,WAAW,MAAM,IAAI,SAAS,IAAI;EACxC,IAAI,UAAU;GACZ,WAAW,KAAK;IAAE,MAAM,SAAS;IAAM,SAAS,CAAC,SAAS,QAAQ,WAAW,MAAM;GAAE,CAAC;GACtF,SAAS,IAAI,SAAS,IAAI;GAC1B;EACF;EACA,MAAM,IAAI,SAAS,MAAM;GACvB,MAAM,SAAS;GACf;GACA,QAAQ,WAAW;GACnB,QAAQ;IAAE;IAAM;GAAQ;GACxB,MAAM;IAAE,OAAO,mBAAmB,eAAe,SAAS,KAAK;IAAG,aAAa,SAAS;GAAY;GACpG,UAAU;IACR,OAAO,SAAS;IAChB,aAAa,SAAS;IACtB,aAAa,SAAS;IACtB,YAAY,SAAS;GACvB;EACF,CAAC;CACH;CAEA,KAAK,MAAM,QAAQ,WAAW,SAAS,CAAC,GAAG;EACzC,IAAI,SAAS,IAAI,KAAK,IAAI,GAAG;EAC7B,MAAM,KAAK,IAAI;CACjB;AACF;;;;;;;AAQA,SAAgB,cAAc,OAAqC;CACjE,MAAM,wBAAQ,IAAI,IAAqB;CACvC,MAAM,QAAwB,CAAC;CAC/B,MAAM,aAAyE,CAAC;CAEhF,WAAW,MAAM,WAAsC,KAAK,MAAM,OAAO,OAAO,CAAC,CAAC;CAElF,KAAK,MAAM,cAAc,MAAM,eAAe,CAAC,GAC7C,cAAc,OAAO,OAAO,YAAY,UAAU;CAGpD,KAAK,MAAM,QAAQ,MAAM,OAAO,GAAG;EACjC,IAAI,KAAK,WAAW,SAAS;EAC7B,KAAK,MAAM,MAAM,KAAK,OAAO,WAAW,CAAC,GACvC,MAAM,KAAK;GAAE,MAAM,KAAK;GAAM;GAAI,MAAM;EAAU,CAAC;EAErD,IAAI,KAAK,OAAO,eAAe,KAAA,GAC7B,MAAM,KAAK;GAAE,MAAM,KAAK;GAAM,IAAI,KAAK,OAAO;GAAY,MAAM;EAAW,CAAC;CAEhF;CAEA,OAAO;EAAE;EAAO;EAAO;CAAW;AACpC"}
@@ -1,104 +0,0 @@
1
- import { i as RouteSeo, n as PublicPath, o as SeoKind } from "./declare-DVgdEdOL.js";
2
- import { AnyRoute } from "@tanstack/react-router";
3
- //#region src/core/graph.d.ts
4
- /**
5
- * Where a node came from: `"route"` for a structural route declaration, or the
6
- * `source` label of the collection that produced the instance (e.g. "blog").
7
- */
8
- type SeoSource = string;
9
- interface SeoNode {
10
- /** Canonical path, no origin (e.g. "/pricing", "/blog/my-post"). */
11
- path: string;
12
- kind: SeoKind;
13
- source: SeoSource;
14
- /** Route-declared policy, or synthesized (kind + inherited sitemap) for instances. */
15
- policy: RouteSeo;
16
- /**
17
- * The head the page ships: a static route's declared `head`, or an instance's
18
- * title (through its route's `titleTemplate`) and description. Absent when the
19
- * route computes its head at request time and no collection supplies it.
20
- */
21
- head?: SeoNodeHead | undefined;
22
- instance?: {
23
- title: string;
24
- description?: string | undefined;
25
- publishedAt?: string | undefined;
26
- modifiedAt?: string | undefined;
27
- } | undefined;
28
- }
29
- /**
30
- * A node's head as graph data: the full title, description, and FAQ text. An
31
- * instance without a description still carries its title.
32
- */
33
- interface SeoNodeHead {
34
- readonly title: string;
35
- readonly description?: string | undefined;
36
- readonly faqs?: ReadonlyArray<{
37
- readonly question: string;
38
- readonly answer: string;
39
- }> | undefined;
40
- }
41
- type SeoEdgeType = "crumb-parent" | "related" | "redirect" | "collection-member";
42
- interface SeoEdge {
43
- from: string;
44
- to: string;
45
- type: SeoEdgeType;
46
- }
47
- interface SeoGraph {
48
- nodes: Map<string, SeoNode>;
49
- edges: Array<SeoEdge>;
50
- /** Exact-path ownership conflicts encountered while assembling graph sources. */
51
- collisions?: ReadonlyArray<{
52
- path: string;
53
- sources: ReadonlyArray<SeoSource>;
54
- }> | undefined;
55
- }
56
- /** One concrete page produced by a collection. */
57
- interface SeoInstance {
58
- /** Canonical path of the page (e.g. "/blog/my-post"). */
59
- readonly path: string;
60
- readonly title: string;
61
- readonly description?: string | undefined;
62
- readonly publishedAt?: string | undefined;
63
- readonly modifiedAt?: string | undefined;
64
- }
65
- interface SeoCollection {
66
- /**
67
- * The param route these instances render through (e.g. "/blog/$slug"). Instances
68
- * inherit this route's declared policy (kind + sitemap) — the graph reads it from
69
- * the structural node, so declarations stay the single source of truth.
70
- */
71
- readonly route: PublicPath;
72
- /** The `source` stamped on every node this collection produces (e.g. "blog"). */
73
- readonly source: SeoSource;
74
- readonly instances: ReadonlyArray<SeoInstance>;
75
- /** Edges the collection declares between its instances and the rest of the graph. */
76
- readonly edges?: ReadonlyArray<SeoEdge> | undefined;
77
- }
78
- /**
79
- * The structural route shape the graph walks: a TanStack route tree has it
80
- * before `init()`, and an adapter can build it from parsed route files.
81
- */
82
- interface SeoRouteNode {
83
- readonly options: {
84
- readonly path?: string | undefined;
85
- readonly staticData?: {
86
- readonly seo?: RouteSeo | undefined;
87
- } | undefined;
88
- };
89
- readonly children?: ReadonlyArray<SeoRouteNode> | undefined;
90
- }
91
- interface BuildSeoGraphInput {
92
- readonly routeTree: AnyRoute | SeoRouteNode;
93
- readonly collections?: ReadonlyArray<SeoCollection> | undefined;
94
- }
95
- /**
96
- * Build the SEO graph from route declarations and content collections.
97
- *
98
- * Synchronous: the caller materializes its collections before calling, so there is
99
- * no async work here. Callers own the origin.
100
- */
101
- declare function buildSeoGraph(input: BuildSeoGraphInput): SeoGraph;
102
- //#endregion
103
- export { SeoGraph as a, SeoNodeHead as c, buildSeoGraph as d, SeoEdgeType as i, SeoRouteNode as l, SeoCollection as n, SeoInstance as o, SeoEdge as r, SeoNode as s, BuildSeoGraphInput as t, SeoSource as u };
104
- //# sourceMappingURL=graph-BxCY90vq.d.ts.map
@@ -1,57 +0,0 @@
1
- import { a as SeoGraph } from "./graph-BxCY90vq.js";
2
- import { r as RobotsConfig } from "./projections-DeEFe7XT.js";
3
- import { t as SeoRouteConfigOptions } from "./route-config-wneg6ktn.js";
4
- import "@tanstack/router-generator";
5
- //#region src/tanstack-start/graph.d.ts
6
- /** The robots.txt policy a build serves; `origin` and indexability come from {@link SiteRuntime}. */
7
- type RobotsPolicy = Pick<RobotsConfig, "disallow" | "contentSignal" | "directives">;
8
- /** The graph and the site runtime one evaluation produces. */
9
- interface AppGraph {
10
- readonly graph: SeoGraph;
11
- readonly site: SiteRuntime;
12
- }
13
- /** Site identity and robots policy, baked into the runtime module per build. */
14
- interface SiteRuntime {
15
- readonly origin: string;
16
- readonly indexable: boolean;
17
- readonly robots: RobotsPolicy;
18
- }
19
- interface PagegraphOptions {
20
- /** This build's canonical origin, no trailing slash, e.g. `https://example.com`. */
21
- readonly origin: string;
22
- /**
23
- * Whether this build's host may be indexed. A non-indexable build serves a
24
- * disallow-all robots.txt with no Sitemap line. Defaults to `true`.
25
- */
26
- readonly indexable?: boolean | undefined;
27
- readonly robots?: {
28
- /** Path prefixes disallowed on an indexable host, beyond the route-config exclusions. */
29
- readonly disallow?: ReadonlyArray<string> | undefined;
30
- /** `Content-Signal` value for an indexable host, e.g. `"search=yes, ai-input=yes, ai-train=yes"`. */
31
- readonly contentSignal?: string | undefined;
32
- /** Extra lines in the `User-agent: *` group on an indexable host. */
33
- readonly directives?: ReadonlyArray<string> | undefined;
34
- } | undefined;
35
- /**
36
- * Root-relative module whose `collections` export is a `SeoCollection[]`, or a
37
- * function returning one (sync or async). It runs in the app's Vite pipeline,
38
- * so content virtual modules (Fumadocs, MDX) and path aliases resolve.
39
- */
40
- readonly collections?: string | undefined;
41
- /**
42
- * Route files (globs relative to the routes directory) never evaluated for the
43
- * graph: app surfaces that declare `staticData` but import server-runtime-only
44
- * modules. Their paths stay out of the graph.
45
- */
46
- readonly exclude?: ReadonlyArray<string> | undefined;
47
- /** Relative to the Vite root. Defaults to `src/routes`. */
48
- readonly routesDirectory?: string | undefined;
49
- /**
50
- * Generate the route-config module and enforce per-group SEO coverage (the
51
- * `seoRouteConfig` gate). Its robots exclusions join `robots.disallow`.
52
- */
53
- readonly routeConfig?: Omit<SeoRouteConfigOptions, "routesDirectory"> | undefined;
54
- }
55
- //#endregion
56
- export { SiteRuntime as i, PagegraphOptions as n, RobotsPolicy as r, AppGraph as t };
57
- //# sourceMappingURL=graph-CBIY71w6.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"page-heads-6y-CARRQ.js","names":[],"sources":["../src/core/page-heads.ts"],"sourcesContent":["import { globToRegExp } from \"./checks\";\nimport type { SeoGraph, SeoNode } from \"./graph\";\n\n/** Plain document metadata, without framework or schema dependencies. */\nexport interface PageHead {\n readonly path: string;\n readonly title: string;\n readonly description: string;\n}\n\nexport interface PageHeadsOptions {\n /** Path globs: `*` matches a segment, `**` matches across segments. */\n readonly exclude?: ReadonlyArray<string>;\n /** Exclude noindex pages, redirects and parameter templates. Defaults to true. */\n readonly indexable?: boolean;\n}\n\n/** Select page nodes before the host resolves metadata; sitemap-disabled hubs remain pages. */\nexport function selectPageHeadNodes(graph: SeoGraph, options: PageHeadsOptions = {}): Array<SeoNode> {\n const excludes = (options.exclude ?? []).map(globToRegExp);\n return [...graph.nodes.values()].filter((node) =>\n node.kind !== \"layout\" &&\n (node.policy.sitemap !== undefined || node.instance !== undefined) &&\n !excludes.some((pattern) => pattern.test(node.path)) &&\n (options.indexable === false || (\n !node.path.includes(\"$\") &&\n node.policy.redirectTo === undefined &&\n !node.policy.robots?.toLowerCase().split(/[\\s,]+/).includes(\"noindex\")\n )),\n );\n}\n\n/** Extract resolved heads, naming the selected page when metadata is missing or blank. */\nexport function pageHeads(graph: SeoGraph, options: PageHeadsOptions = {}): Array<PageHead> {\n return selectPageHeadNodes(graph, options).map((node) => {\n const title = node.head?.title;\n const description = node.head?.description;\n if (!title?.trim()) throw new Error(`Invalid page head for ${node.path}: missing title`);\n if (!description?.trim()) throw new Error(`Invalid page head for ${node.path}: missing description`);\n return { path: node.path, title, description };\n });\n}\n"],"mappings":";;;AAkBA,SAAgB,oBAAoB,OAAiB,UAA4B,CAAC,GAAmB;CACnG,MAAM,YAAY,QAAQ,WAAW,CAAC,EAAA,CAAG,IAAI,YAAY;CACzD,OAAO,CAAC,GAAG,MAAM,MAAM,OAAO,CAAC,CAAC,CAAC,QAAQ,SACvC,KAAK,SAAS,aACb,KAAK,OAAO,YAAY,KAAA,KAAa,KAAK,aAAa,KAAA,MACxD,CAAC,SAAS,MAAM,YAAY,QAAQ,KAAK,KAAK,IAAI,CAAC,MAClD,QAAQ,cAAc,SACrB,CAAC,KAAK,KAAK,SAAS,GAAG,KACvB,KAAK,OAAO,eAAe,KAAA,KAC3B,CAAC,KAAK,OAAO,QAAQ,YAAY,CAAC,CAAC,MAAM,QAAQ,CAAC,CAAC,SAAS,SAAS,EAEzE;AACF;;AAGA,SAAgB,UAAU,OAAiB,UAA4B,CAAC,GAAoB;CAC1F,OAAO,oBAAoB,OAAO,OAAO,CAAC,CAAC,KAAK,SAAS;EACvD,MAAM,QAAQ,KAAK,MAAM;EACzB,MAAM,cAAc,KAAK,MAAM;EAC/B,IAAI,CAAC,OAAO,KAAK,GAAG,MAAM,IAAI,MAAM,yBAAyB,KAAK,KAAK,gBAAgB;EACvF,IAAI,CAAC,aAAa,KAAK,GAAG,MAAM,IAAI,MAAM,yBAAyB,KAAK,KAAK,sBAAsB;EACnG,OAAO;GAAE,MAAM,KAAK;GAAM;GAAO;EAAY;CAC/C,CAAC;AACH"}
@@ -1,21 +0,0 @@
1
- import { a as SeoGraph, s as SeoNode } from "./graph-BxCY90vq.js";
2
- //#region src/core/page-heads.d.ts
3
- /** Plain document metadata, without framework or schema dependencies. */
4
- interface PageHead {
5
- readonly path: string;
6
- readonly title: string;
7
- readonly description: string;
8
- }
9
- interface PageHeadsOptions {
10
- /** Path globs: `*` matches a segment, `**` matches across segments. */
11
- readonly exclude?: ReadonlyArray<string>;
12
- /** Exclude noindex pages, redirects and parameter templates. Defaults to true. */
13
- readonly indexable?: boolean;
14
- }
15
- /** Select page nodes before the host resolves metadata; sitemap-disabled hubs remain pages. */
16
- declare function selectPageHeadNodes(graph: SeoGraph, options?: PageHeadsOptions): Array<SeoNode>;
17
- /** Extract resolved heads, naming the selected page when metadata is missing or blank. */
18
- declare function pageHeads(graph: SeoGraph, options?: PageHeadsOptions): Array<PageHead>;
19
- //#endregion
20
- export { selectPageHeadNodes as i, PageHeadsOptions as n, pageHeads as r, PageHead as t };
21
- //# sourceMappingURL=page-heads-DNkjMUEN.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"vite-graph-loader-01p1WxEq.js","names":[],"sources":["../src/config/vite-graph-loader.ts"],"sourcesContent":["/**\n * A graph loader that evaluates the host app's own graph module inside a\n * headless Vite server.\n *\n * The SEO graph is built *by the app*: its module reads the generated route\n * tree and whatever content collections it owns, so it pulls in the app's real\n * module graph — path aliases, virtual modules from content plugins, bare\n * imports that only exist inside a server runtime. A CLI cannot just `import()`\n * that from Node or Bun.\n *\n * So this boots a scoped, in-process Vite server (middleware mode, no HMR, no\n * watcher, no HTTP listener), `ssrLoadModule`s the app's graph module, and calls\n * its exported loader. The graph comes back as a live in-process object — there\n * is no serialization boundary for the commands to cross — and the server is\n * closed by {@link LoadedSeoGraph.dispose}.\n *\n * Everything host-shaped is an option: the app supplies its own {@link\n * ViteGraphLoaderOptions.plugins} (a content plugin, an MDX loader), {@link\n * ViteGraphLoaderOptions.stubs} for bare modules that only resolve inside its\n * server runtime, and {@link ViteGraphLoaderOptions.env} for variables its\n * modules parse at import. This file knows about none of them.\n */\n\nimport { createServer, type InlineConfig, type PluginOption } from \"vite\";\n\nimport type { SeoGraph } from \"../core/graph\";\nimport type { RobotsConfig } from \"../core/projections\";\n\n/** A graph, plus the release of whatever producing it acquired. */\nexport interface LoadedSeoGraph {\n readonly graph: SeoGraph;\n /**\n * The robots policy the app serves, when the loader knows it (the TanStack\n * Start loader reads it from the `pagegraph()` plugin). `pagegraph robots`\n * uses it instead of `seo.config.ts` fields.\n */\n readonly robots?: Pick<RobotsConfig, \"disallow\" | \"contentSignal\" | \"directives\"> | undefined;\n /** Called once the command is done with the graph, on success or failure. */\n readonly dispose: () => Promise<void>;\n}\n\n/**\n * Produces the SEO graph for the `pagegraph` CLI. A plain promise on purpose: a\n * config file must be writable without Effect, which the CLI keeps behind its\n * `bin` (an optional peer dependency).\n */\n/** JSON input that can be supplied by config files to an app graph export. */\nexport type GraphLoaderInput = null | boolean | number | string | ReadonlyArray<GraphLoaderInput> | { readonly [key: string]: GraphLoaderInput };\n\nexport type SeoGraphLoader<Input extends GraphLoaderInput | void = void> = (input: Input) => Promise<LoadedSeoGraph>;\n\nexport interface ViteGraphLoaderOptions {\n /** The app's Vite root — the directory its aliases and plugins resolve against. */\n readonly root: string;\n /** Module exporting the graph loader, root-relative (e.g. `/lib/seo/graph.ts`). */\n readonly entry: string;\n /** Named export on `entry` returning `Promise<SeoGraph>`. Defaults to `loadSeoGraph`. */\n readonly exportName?: string | undefined;\n /** Vite plugins the app's module graph needs — a content/MDX plugin, say. */\n readonly plugins?: ReadonlyArray<PluginOption> | undefined;\n /**\n * Bare or virtual module ids to replace with inert source, so a module that\n * only resolves inside the app's server runtime does not break the load.\n */\n readonly stubs?: Readonly<Record<string, string>> | undefined;\n /**\n * Variables to seed on `process.env` before the app's modules parse it. Vite\n * exposes prefixed values through `import.meta.env`, so this is how a module\n * that demands an env var at import time is satisfied. Existing values win —\n * a real `.env` is never clobbered.\n */\n readonly env?: Readonly<Record<string, string>> | undefined;\n}\n\nconst messageOf = (cause: unknown): string =>\n cause instanceof Error ? cause.message : String(cause);\n\n/**\n * Vite and its plugins write progress to stdout. The CLI reserves stdout for the\n * data plane, so redirect it to stderr for the duration of the load.\n *\n * Both the stream method and `console.log`/`info` are swapped: under Bun,\n * `console.log` writes to fd 1 natively rather than going through\n * `process.stdout.write`, so the stream swap alone would miss a plugin that\n * logs through the console.\n */\nexport const withCleanStdout = async <A>(run: () => Promise<A>): Promise<A> => {\n const originalWrite = process.stdout.write.bind(process.stdout);\n const originalLog = console.log;\n const originalInfo = console.info;\n process.stdout.write = process.stderr.write.bind(process.stderr) as typeof process.stdout.write;\n console.log = (...args: Array<unknown>) => console.error(...args);\n console.info = (...args: Array<unknown>) => console.error(...args);\n try {\n return await run();\n } finally {\n process.stdout.write = originalWrite;\n console.log = originalLog;\n console.info = originalInfo;\n }\n};\n\n/** Turn `{ id: source }` into a Vite plugin that resolves and loads each id. */\nconst stubPlugin = (stubs: Readonly<Record<string, string>>): PluginOption => {\n const virtualIdOf = (id: string) => `\\0seo-cli-stub:${id}`;\n const sourceByVirtualId = new Map(\n Object.entries(stubs).map(([id, source]) => [virtualIdOf(id), source]),\n );\n return {\n name: \"seo:stubs\",\n resolveId: (id) => (id in stubs ? virtualIdOf(id) : null),\n load: (id) => sourceByVirtualId.get(id) ?? null,\n };\n};\n\nconst seedEnv = (env: Readonly<Record<string, string>>): void => {\n for (const [key, value] of Object.entries(env)) {\n // oxlint-disable-next-line node/no-process-env -- CLI boundary: seeds the raw env BEFORE the app's own env module parses it; that module is the consumer here, not an option\n process.env[key] ??= value;\n }\n};\n\nconst inlineConfigFor = (options: ViteGraphLoaderOptions): InlineConfig => ({\n configFile: false,\n root: options.root,\n mode: \"production\",\n logLevel: \"error\",\n appType: \"custom\",\n clearScreen: false,\n server: { middlewareMode: true, hmr: false, watch: null },\n resolve: { tsconfigPaths: true },\n plugins: [\n ...(options.stubs === undefined ? [] : [stubPlugin(options.stubs)]),\n ...(options.plugins ?? []),\n ],\n});\n\n/**\n * Build a {@link SeoGraphLoader} that evaluates `entry` in a headless Vite\n * server rooted at `root`, and returns what its `exportName` export resolves to.\n */\nexport const viteGraphLoader =\n <Input extends GraphLoaderInput | void = void>(options: ViteGraphLoaderOptions): SeoGraphLoader<Input> =>\n async (input) => {\n const exportName = options.exportName ?? \"loadSeoGraph\";\n if (options.env) seedEnv(options.env);\n\n const server = await withCleanStdout(() => createServer(inlineConfigFor(options))).catch(\n (cause: unknown) => {\n throw new Error(`Could not start the Vite loader: ${messageOf(cause)}`);\n },\n );\n\n // The server is a resource from here on: a failed load must still close it,\n // and a successful one hands the close to the caller as `dispose`.\n const graph = await withCleanStdout(async () => {\n const module = (await server.ssrLoadModule(options.entry)) as Record<string, unknown>;\n const load = module[exportName];\n if (typeof load !== \"function\") {\n throw new Error(\n `${options.entry} has no \\`${exportName}\\` export (found: ${Object.keys(module).join(\", \") || \"nothing\"}).`,\n );\n }\n return (await load(input)) as SeoGraph;\n }).catch(async (cause: unknown) => {\n // Close on the way out, but never let a close failure bury the real\n // error: the reason the graph did not build is the useful one.\n await withCleanStdout(() => server.close()).catch(() => {});\n throw new Error(`Could not build the SEO graph: ${messageOf(cause)}`);\n });\n\n return { graph, dispose: () => withCleanStdout(() => server.close()) };\n };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA0EA,MAAM,aAAa,UACjB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;;;;;;;;;;AAWvD,MAAa,kBAAkB,OAAU,QAAsC;CAC7E,MAAM,gBAAgB,QAAQ,OAAO,MAAM,KAAK,QAAQ,MAAM;CAC9D,MAAM,cAAc,QAAQ;CAC5B,MAAM,eAAe,QAAQ;CAC7B,QAAQ,OAAO,QAAQ,QAAQ,OAAO,MAAM,KAAK,QAAQ,MAAM;CAC/D,QAAQ,OAAO,GAAG,SAAyB,QAAQ,MAAM,GAAG,IAAI;CAChE,QAAQ,QAAQ,GAAG,SAAyB,QAAQ,MAAM,GAAG,IAAI;CACjE,IAAI;EACF,OAAO,MAAM,IAAI;CACnB,UAAU;EACR,QAAQ,OAAO,QAAQ;EACvB,QAAQ,MAAM;EACd,QAAQ,OAAO;CACjB;AACF;;AAGA,MAAM,cAAc,UAA0D;CAC5E,MAAM,eAAe,OAAe,kBAAkB;CACtD,MAAM,oBAAoB,IAAI,IAC5B,OAAO,QAAQ,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,YAAY,CAAC,YAAY,EAAE,GAAG,MAAM,CAAC,CACvE;CACA,OAAO;EACL,MAAM;EACN,YAAY,OAAQ,MAAM,QAAQ,YAAY,EAAE,IAAI;EACpD,OAAO,OAAO,kBAAkB,IAAI,EAAE,KAAK;CAC7C;AACF;AAEA,MAAM,WAAW,QAAgD;CAC/D,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,GAAG,GAE3C,QAAQ,IAAI,SAAS;AAEzB;AAEA,MAAM,mBAAmB,aAAmD;CAC1E,YAAY;CACZ,MAAM,QAAQ;CACd,MAAM;CACN,UAAU;CACV,SAAS;CACT,aAAa;CACb,QAAQ;EAAE,gBAAgB;EAAM,KAAK;EAAO,OAAO;CAAK;CACxD,SAAS,EAAE,eAAe,KAAK;CAC/B,SAAS,CACP,GAAI,QAAQ,UAAU,KAAA,IAAY,CAAC,IAAI,CAAC,WAAW,QAAQ,KAAK,CAAC,GACjE,GAAI,QAAQ,WAAW,CAAC,CAC1B;AACF;;;;;AAMA,MAAa,mBACoC,YAC/C,OAAO,UAAU;CACf,MAAM,aAAa,QAAQ,cAAc;CACzC,IAAI,QAAQ,KAAK,QAAQ,QAAQ,GAAG;CAEpC,MAAM,SAAS,MAAM,sBAAsB,aAAa,gBAAgB,OAAO,CAAC,CAAC,CAAC,CAAC,OAChF,UAAmB;EAClB,MAAM,IAAI,MAAM,oCAAoC,UAAU,KAAK,GAAG;CACxE,CACF;CAoBA,OAAO;EAAE,OAAA,MAhBW,gBAAgB,YAAY;GAC9C,MAAM,SAAU,MAAM,OAAO,cAAc,QAAQ,KAAK;GACxD,MAAM,OAAO,OAAO;GACpB,IAAI,OAAO,SAAS,YAClB,MAAM,IAAI,MACR,GAAG,QAAQ,MAAM,YAAY,WAAW,oBAAoB,OAAO,KAAK,MAAM,CAAC,CAAC,KAAK,IAAI,KAAK,UAAU,GAC1G;GAEF,OAAQ,MAAM,KAAK,KAAK;EAC1B,CAAC,CAAC,CAAC,MAAM,OAAO,UAAmB;GAGjC,MAAM,sBAAsB,OAAO,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;GAC1D,MAAM,IAAI,MAAM,kCAAkC,UAAU,KAAK,GAAG;EACtE,CAAC;EAEe,eAAe,sBAAsB,OAAO,MAAM,CAAC;CAAE;AACvE"}
@@ -1,56 +0,0 @@
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