pagegraph 0.10.1 → 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 +163 -124
  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-uZk5Em9A.js +352 -0
  17. package/dist/checks-uZk5Em9A.js.map +1 -0
  18. package/dist/cli.js +3814 -3017
  19. package/dist/cli.js.map +1 -1
  20. package/dist/config.d.ts +10 -45
  21. package/dist/config.js +10 -98
  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-BE9KCTgb.js → content-kcEnQdfi.js} +4 -489
  25. package/dist/content-kcEnQdfi.js.map +1 -0
  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 -133
  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 +35 -13
  64. package/dist/content-BE9KCTgb.js.map +0 -1
  65. package/dist/vite.js.map +0 -1
package/README.md CHANGED
@@ -1,21 +1,22 @@
1
1
  # pagegraph
2
2
 
3
- Route-declared SEO for TanStack Router. Each route declares its SEO policy once, in
4
- `staticData.seo` — the sitemap, `robots.txt`, breadcrumbs, JSON-LD, cross-links, and
5
- the CI check all derive from that one declaration. There is no second place to update,
6
- so nothing drifts. A public page that ships without a declaration **fails the build**.
3
+ Route-declared SEO for TanStack Start and TanStack Router. Each route declares its SEO
4
+ policy and head once, in `staticData.seo` — the `<head>`, sitemap, `robots.txt`,
5
+ breadcrumbs, JSON-LD, cross-links, and the CI check all derive from that one
6
+ declaration. There is no second place to update, so nothing drifts. A public page that
7
+ ships without a declaration **fails the build**.
7
8
 
8
9
  ```mermaid
9
10
  flowchart LR
10
11
  subgraph declare["declare once"]
11
- route["staticData.seo<br/>+ head()"]
12
+ route["staticData.seo (+ head)<br/>head: seo.head"]
12
13
  end
13
- route --> seograph["SeoGraph<br/>(nodes + edges)"]
14
+ route --> seograph["SeoGraph<br/>(built by the Vite plugin)"]
14
15
  seograph --> sitemap["sitemap.xml"]
15
16
  seograph --> robots["robots.txt"]
16
17
  seograph --> check["pagegraph check · CI gate"]
17
18
  route --> head["&lt;head&gt; meta · canonical<br/>Breadcrumbs · JSON-LD"]
18
- gate["seoRouteConfig (vite)"] -. "fails undeclared pages" .-> route
19
+ gate["pagegraph() (vite)"] -. "fails undeclared pages" .-> route
19
20
  ```
20
21
 
21
22
  ## Install
@@ -25,23 +26,43 @@ bun add pagegraph
25
26
  bun add -D lighthouse # only for `pagegraph audit` performance evidence
26
27
  ```
27
28
 
28
- | Entry | Exports | Peers |
29
- | ---------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------- |
30
- | `pagegraph` | `buildSeoGraph`, `renderSitemap`, `renderRobots`, `contentSignal`, `checkGraph`, `checkRenderedCoverage`, `decodeRenderedEdgeArtifact`, `inspectHtml` | — |
31
- | `pagegraph/react` | `createSeo` → `seoHead`, `Breadcrumbs`, JSON-LD generators | `react`, `@tanstack/react-router` |
32
- | `pagegraph/vite` | `seoRouteConfig` coverage gate | `vite` |
33
- | `pagegraph/config` | `defineSeoConfig`, `viteGraphLoader` | `vite` |
34
- | `pagegraph/audit` | Audit services, scanner protocol, rules, and report schemas | `effect` |
35
- | `pagegraph` bin | CLI over the same graph | bundled — runs on Bun |
29
+ ## What runs where
36
30
 
37
- The core and React import graphs have **zero runtime dependencies** — everything they use is a
38
- peer, and only the entries you import need those peers loaded.
31
+ Every entry belongs to one place. Runtime entries are safe in a Worker, an SSR server, or a
32
+ browser; build entries load your app through Vite, parse routes natively, or drive Node I/O.
33
+
34
+ | Entry | Runs in | Exports | Peers |
35
+ | ------------------ | ----------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------- |
36
+ | `pagegraph` | runtime (Worker, SSR, browser) | `buildSeoGraph`, `contentCollection`, `renderSitemap`, `renderRobots`, `pageHeads`, `graphToJson`/`graphFromJson`, checks, `inspectHtml` | — |
37
+ | `pagegraph/react` | runtime | `createSeo` → `seo.head`, `seoHead`, `Breadcrumbs`, JSON-LD generators | `react`, `@tanstack/react-router` |
38
+ | `pagegraph/tanstack-start/server` | runtime (Start server routes) | `robotsTxt`, `sitemapXml`, `seoGraph`, `seoSite` | the `pagegraph()` plugin |
39
+ | `pagegraph/tanstack-start` | build (`vite.config.ts`, `seo.config.ts`) | `pagegraph()` Vite plugin, `tanstackStartGraph`, `evaluateAppGraph` | `vite`, `@tanstack/router-generator` |
40
+ | `pagegraph/vite` | build (`vite.config.ts`) | `seoRouteConfig` coverage gate | `vite`, `@tanstack/router-generator` |
41
+ | `pagegraph/config` | build (`seo.config.ts`, CLI) | `defineSeoConfig`, `viteGraphLoader`, `loadPageHeads` | `vite` |
42
+ | `pagegraph/audit` | build / CLI (Node or Bun) | Audit services, scanner protocol, rules, and report schemas | `effect` |
43
+ | `pagegraph` bin | CLI (Bun) | CLI over the same graph | bundled |
44
+
45
+ Build entries resolve to a stub under the `workerd`, `worker`, and `browser` export conditions.
46
+ A Worker or browser bundle that reaches one fails at import with an error naming the entry,
47
+ instead of crashing on a native binding when a request first runs the code. The runtime entries
48
+ have **zero runtime dependencies** beyond their listed peers; a test pins each entry's import
49
+ graph.
39
50
 
40
51
  The CLI **bundles PageGraph's Effect and TypeSafe provider runtime**, so it does not depend on the
41
- app's Effect RC; it runs on [Bun](https://bun.sh) (`bunx pagegraph`). Agentic workflows load the
42
- OpenCode SDK and its private Effect runtime only when invoked. `vite` stays a peer — graph commands
52
+ app's Effect version; it runs on [Bun](https://bun.sh) (`bunx pagegraph`). Agentic workflows load the
53
+ OpenCode SDK only when invoked. The first workflow installs pinned OpenCode 2.0.22 into
54
+ `~/.cache/pagegraph/opencode-2.0.22-2`, outside the app's dependency tree; subsequent workflows
55
+ reuse the verified cache. This keeps consumer dependency overrides away from OpenCode's declared
56
+ Effect and Drizzle versions. Executor plugins must use OpenCode's Effect version (`4.0.0-rc.112`).
57
+ Bun and network access to npm are required for the first install. The pinned OpenCode client's
58
+ HTTP error handling retains the method, path, status, and response body for diagnostics;
59
+ undeclared error bodies are capped at 16 KiB.
60
+ Discovery-only research gets one continuation to complete provider evidence. Every workflow failure
61
+ after run creation writes `failure.json` in the run directory, with the stage, session ID when available,
62
+ transcript, and cause chain. `vite` stays a peer — graph commands
43
63
  load your app through Vite at runtime — and `lighthouse` is only needed by `pagegraph audit`.
44
- Importing `pagegraph/audit` programmatically still needs `effect`.
64
+ Importing `pagegraph/audit` programmatically needs `effect@^4.0.0`. The Effect and TypeSafe
65
+ peers remain optional for other library entry points and the bundled CLI.
45
66
 
46
67
  ### Agent skill
47
68
 
@@ -54,7 +75,7 @@ pagegraph skills get core
54
75
 
55
76
  The skill is maintained at [skill-data/core/SKILL.md](skill-data/core/SKILL.md) and compiled into the CLI, so the instructions match the installed Pagegraph version. `skills get core` prints Markdown to stdout; `--json` returns a structured payload.
56
77
 
57
- ## Quick start
78
+ ## Quick start (TanStack Start)
58
79
 
59
80
  **1. Bind your site identity once.** Route files never see an origin or a brand name.
60
81
 
@@ -62,7 +83,7 @@ The skill is maintained at [skill-data/core/SKILL.md](skill-data/core/SKILL.md)
62
83
  // lib/seo.ts
63
84
  import { createSeo } from "pagegraph/react";
64
85
 
65
- export const { seoHead } = createSeo({
86
+ const site = createSeo({
66
87
  origin: "https://example.com",
67
88
  site: {
68
89
  name: "Example",
@@ -76,19 +97,17 @@ export const { seoHead } = createSeo({
76
97
  description: "What the company does.",
77
98
  sameAs: ["https://github.com/example"],
78
99
  contactPoint: { contactType: "support", email: "hi@example.com" },
79
- address: {
80
- streetAddress: "123 Example Street",
81
- addressLocality: "Example City",
82
- addressRegion: "CA",
83
- postalCode: "94105",
84
- addressCountry: "US",
85
- },
86
100
  },
87
101
  website: { searchPath: "/docs?q={search_term_string}" },
88
102
  });
103
+
104
+ export const seo = site; // seo.head for static routes
105
+ export const { seoHead } = site;
89
106
  ```
90
107
 
91
- **2. Declare on the route.** Policy in `staticData.seo`, per-page content in `head()`.
108
+ **2. Declare each page once.** `staticData.seo` holds the crawl policy and the head
109
+ together; `seo.head` renders it. The graph and the checks read the same
110
+ `staticData.seo.head` — the page and the graph cannot disagree.
92
111
 
93
112
  ```tsx
94
113
  export const Route = createFileRoute("/pricing")({
@@ -100,124 +119,112 @@ export const Route = createFileRoute("/pricing")({
100
119
  related: ["/features", "/docs"],
101
120
  link: { title: "Pricing", description: "Simple volume pricing." },
102
121
  modifiedAt: "2026-09-29", // sitemap <lastmod> and the freshness report
122
+ head: {
123
+ title: "Pricing — Example",
124
+ description: "Simple volume pricing.",
125
+ faqs: pricingFaqs,
126
+ },
103
127
  },
104
128
  },
105
- head: (ctx) =>
106
- seoHead(ctx, {
107
- title: "Pricing — Example",
108
- description: "Simple volume pricing.",
109
- }),
129
+ head: seo.head,
130
+ component: PricingPage,
110
131
  });
111
132
  ```
112
133
 
113
- `seoHead` returns the `meta` + canonical `links` TanStack renders into `<head>` —
134
+ `seo.head` returns the `meta` + canonical `links` TanStack renders into `<head>` —
114
135
  title, description, og/twitter cards, robots, and the JSON-LD each page warrants
115
- (BreadcrumbList always; Article, FAQPage, Service, ItemList when the instance
116
- declares them).
136
+ (BreadcrumbList always; Article, FAQPage, Service, ItemList when the head declares
137
+ them). A route that renders from `loaderData` calls `seoHead(ctx, instance)` in its
138
+ own `head()` and declares a `titleTemplate` (`"%s | Example Blog"`): `seoHead` applies it,
139
+ and the graph applies it to the route's collection pages.
117
140
 
118
- ### Extensible JSON-LD
119
-
120
- The built-in generators are conveniences, not a closed schema registry. Define any
121
- `schema-dts` entity, link it to the plugin's stable site identities, and compose one
122
- site graph:
141
+ **3. Register the plugin.** It builds the graph once per build (and on change in dev)
142
+ inside your app's Vite pipeline — path aliases, `define`s, and content plugins such as
143
+ Fumadocs apply, with no stubs — and ships it to the server runtime as data.
123
144
 
124
145
  ```ts
125
- import {
126
- createSeo,
127
- defineJsonLd,
128
- extendJsonLd,
129
- jsonLdRef,
130
- } from "pagegraph/react";
131
-
132
- export const seo = createSeo({
133
- // site, organization, and website as above
134
- jsonLd: {
135
- site: (ids) => [
136
- defineJsonLd({
137
- "@type": "SoftwareApplication",
138
- "@id": "https://example.com/#product",
139
- name: "Example",
140
- applicationCategory: "DeveloperApplication",
141
- operatingSystem: "Web",
142
- provider: jsonLdRef(ids.organization),
143
- }),
144
- ],
145
- transform: (entry) => {
146
- if (entry.kind === "organization") {
147
- return extendJsonLd(entry.document, {
148
- slogan: "Ship with confidence.",
149
- });
150
- }
151
- return entry.document;
152
- },
146
+ // vite.config.ts
147
+ import { pagegraph } from "pagegraph/tanstack-start";
148
+
149
+ pagegraph({
150
+ origin: "https://example.com",
151
+ indexable: process.env.DEPLOY_ENV === "production", // previews disallow everything
152
+ robots: { contentSignal: "search=yes, ai-input=yes, ai-train=yes" },
153
+ collections: "src/lib/seo/collections.ts",
154
+ routeConfig: {
155
+ outputPath: fileURLToPath(new URL("./src/lib/route-config.ts", import.meta.url)),
156
+ publicGroups: ["(marketing)", "(docs)"],
157
+ enforceCoverageIn: ["(marketing)", "(docs)"],
158
+ alwaysDisallow: ["/dashboard", "/api"],
153
159
  },
154
160
  });
155
-
156
- const siteGraph = seo.generateSiteGraphSchema();
157
161
  ```
158
162
 
159
- `generateSiteGraphSchema()` includes Organization, WebSite, and configured site
160
- entities in one `@graph`. Organization and WebSite use stable `#organization` and
161
- `#website` IDs; articles and services provided by the site reference the same
162
- Organization. Existing individual generators remain available and are not transformed.
163
-
164
- Add page-specific entities through `seoHead`:
165
-
166
163
  ```ts
167
- head: (ctx) =>
168
- seo.seoHead(ctx, {
169
- title: "Example for developers",
170
- description: "A focused description of the product.",
171
- jsonLd: [
172
- defineJsonLd({
173
- "@type": "SoftwareApplication",
174
- name: "Example",
175
- url: "https://example.com/product",
176
- }),
177
- ],
178
- });
164
+ // src/lib/seo/collections.ts — evaluated in the app's Vite pipeline
165
+ import { contentCollection } from "pagegraph";
166
+ import { blogSource } from "../blog-source"; // e.g. a Fumadocs loader()
167
+
168
+ export const collections = () => [
169
+ contentCollection({
170
+ route: "/blog/$slug",
171
+ source: "blog",
172
+ pages: blogSource.getPages(),
173
+ entry: (page) => ({
174
+ title: page.data.title,
175
+ description: page.data.description,
176
+ publishedAt: page.data.date,
177
+ related: page.data.related,
178
+ }),
179
+ }),
180
+ ];
179
181
  ```
180
182
 
181
- The transform sees discriminated generated and custom entries plus `origin`,
182
- `canonical`, and `entityIds`. Return the document to keep it, no value (or `false`)
183
- to suppress it, or an array to expand it. Output preserves caller order and never
184
- silently deduplicates entities.
183
+ The plugin evaluates every route file that declares `staticData` — never the generated
184
+ route tree — in a Node environment named `pagegraph`. A route that imports a module
185
+ only the server runtime provides (`cloudflare:workers`) fails with its file name; add an
186
+ app-only route to `exclude` (route-file globs).
185
187
 
186
- **3. Serve the projections.** Build the graph from your route tree, render strings.
188
+ **4. Serve robots.txt and sitemap.xml.** One line each; the output depends on the
189
+ deployment's origin and indexability, so they stay routes.
187
190
 
188
191
  ```ts
189
- // lib/seo/graph.ts — also the module the CLI loads
190
- import { buildSeoGraph } from "pagegraph";
192
+ // src/routes/robots[.]txt.ts — sitemap[.]xml.ts is symmetric with sitemapXml
193
+ import { robotsTxt } from "pagegraph/tanstack-start/server";
191
194
 
192
- export const loadSeoGraph = () =>
193
- buildSeoGraph({ routeTree, collections: [blogCollection] });
195
+ export const Route = createFileRoute("/robots.txt")({ server: { handlers: { GET: robotsTxt } } });
194
196
  ```
195
197
 
198
+ **5. Gate CI.** The CLI reads the same graph through `seo.config.ts`:
199
+
196
200
  ```ts
197
- // routes/sitemap[.]xml.ts — robots[.]txt.ts is symmetric
198
- import { renderRobots, renderSitemap } from "pagegraph";
201
+ // seo.config.ts
202
+ import { defineSeoConfig } from "pagegraph/config";
203
+ import { tanstackStartGraph } from "pagegraph/tanstack-start";
199
204
 
200
- renderSitemap(loadSeoGraph(), { origin, indexable: true });
201
- renderRobots(loadSeoGraph(), {
202
- origin,
203
- indexable: true,
204
- disallow: routeConfig.robotsExclusions,
205
- contentSignal: "search=yes, ai-input=yes, ai-train=yes",
205
+ export default defineSeoConfig({
206
+ origin: "https://example.com",
207
+ loadGraph: tanstackStartGraph({ root: import.meta.dirname }),
206
208
  });
207
209
  ```
208
210
 
211
+ ```bash
212
+ pagegraph check
213
+ ```
214
+
215
+ ### Without TanStack Start
216
+
217
+ `buildSeoGraph({ routeTree, collections })` builds the same graph from any TanStack
218
+ Router tree; render it with `renderSitemap(graph, { origin, indexable })` and
219
+ `renderRobots(graph, { origin, indexable, disallow, contentSignal })`, and point
220
+ `seo.config.ts` at it with `viteGraphLoader` (see [CLI](#cli)).
221
+
209
222
  `renderRobots` does not invent a Content-Signal. Pass `contentSignal` for
210
223
  the value (the plugin prefixes `Content-Signal: `), `directives` for extra
211
224
  group lines, and `transform` if you need to wrap or replace the whole file.
212
225
  Preview hosts (`indexable: false`) drop `contentSignal` and `directives`;
213
226
  `transform` still runs.
214
227
 
215
- **4. Gate CI.** The same graph, the same rules, exit 1 on structural violations.
216
-
217
- ```bash
218
- pagegraph check
219
- ```
220
-
221
228
  ## Agentic workflows
222
229
 
223
230
  PageGraph can combine the deterministic route graph with a project-owned embedded OpenCode agent,
@@ -285,6 +292,11 @@ After research is validated, the run directory contains `research.json`: an inco
285
292
  with the collected evidence, decision inputs, and OpenCode transcript. A downstream failure reports
286
293
  its path so the evidence remains available for diagnosis. Only a completed workflow writes
287
294
  `run.json` and `summary.md`; a research checkpoint is not a final recommendation.
295
+ Model JSON drops null properties and array elements before validation. Schema validation collects all
296
+ issues for one read-only repair turn. Every error after run creation writes `failure.json` in that
297
+ run directory with the workflow, stage, available OpenCode session ID, transcript, and full cause
298
+ chain. The error reports the artifact path and a next step; a failed artifact write preserves the
299
+ original error with a note.
288
300
 
289
301
  | Command | Result |
290
302
  | --- | --- |
@@ -373,9 +385,10 @@ route's date is never inherited by its instances. Undated pages emit no
373
385
 
374
386
  ## Coverage gate (Vite)
375
387
 
376
- `seoRouteConfig` parses the route tree with `@tanstack/router-generator` (the same
377
- parser as the router) and fails `vite build` when a page in an enforced group has
378
- neither `staticData` nor `head`:
388
+ `pagegraph({ routeConfig })` — or `seoRouteConfig` from `pagegraph/vite` in a Router-only
389
+ app — parses the route tree with `@tanstack/router-generator` (the same parser as the
390
+ router) and fails `vite build` when a page in an enforced group has neither `staticData`
391
+ nor `head`:
379
392
 
380
393
  ```ts
381
394
  // vite.config.ts
@@ -390,14 +403,15 @@ seoRouteConfig({
390
403
  ```
391
404
 
392
405
  It also derives a small config module from the route tree: `robotsExclusions`
393
- (feed to `renderRobots`) and `reservedSegments` (top-level segments an app must
394
- not hand out as tenant/org slugs).
406
+ (the plugin adds them to robots.txt; a Router-only app feeds them to `renderRobots`) and
407
+ `reservedSegments` (top-level segments an app must not hand out as tenant/org slugs).
395
408
 
396
409
  ## CLI
397
410
 
398
- The graph commands acquire your graph through `seo.config.ts` at the app root —
399
- the `viteGraphLoader` evaluates your graph module inside a headless Vite server,
400
- so path aliases, content plugins, and virtual modules all resolve:
411
+ The graph commands acquire your graph through `seo.config.ts` at the app root. A
412
+ TanStack Start app uses `tanstackStartGraph` (above), which also supplies the robots
413
+ policy. Otherwise `viteGraphLoader` evaluates your graph module inside a headless Vite
414
+ server, so path aliases, content plugins, and virtual modules all resolve:
401
415
 
402
416
  ```ts
403
417
  // seo.config.ts
@@ -417,6 +431,28 @@ export default defineSeoConfig({
417
431
  });
418
432
  ```
419
433
 
434
+ Build-time consumers of page titles and descriptions read the same graph:
435
+
436
+ ```ts
437
+ import { loadPageHeads } from "pagegraph/config"; // build-only
438
+ import { tanstackStartGraph } from "pagegraph/tanstack-start";
439
+
440
+ const heads = await loadPageHeads(tanstackStartGraph({ root: import.meta.dirname }), {
441
+ exclude: ["/docs", "/docs/**"],
442
+ });
443
+ ```
444
+
445
+ `selectPageHeadNodes(graph, options)` selects the page nodes `pageHeads` reads.
446
+ `pageHeads(graph, options)` returns `{ path, title, description }[]` from each node's
447
+ `head` (a declared `staticData.seo.head`, or a collection instance through its route's title
448
+ template), requiring nonblank titles and descriptions and naming the page on failure. `loadPageHeads`
449
+ always disposes the acquired graph, including when validation fails. Page nodes
450
+ carry a sitemap declaration or an instance; layouts are excluded. By default,
451
+ selection excludes noindex pages, redirects and parameter templates, while retaining
452
+ sitemap-disabled hubs. Set `indexable: false` to include those nonindexable nodes.
453
+ `exclude` accepts exact paths and path globs (`*` within a segment, `**` across
454
+ segments). Docs are ordinary pages; excluding them is caller policy, not a graph kind.
455
+
420
456
  ```bash
421
457
  pagegraph check # CI gate — exit 1 on structural violations
422
458
  pagegraph check --site <url> # also check every declared page's rendered HTML
@@ -736,8 +772,11 @@ import {
736
772
  hasStructuralViolations,
737
773
  inspectHtml,
738
774
  } from "pagegraph";
775
+ import { evaluateAppGraph } from "pagegraph/tanstack-start";
739
776
 
740
- expect(hasStructuralViolations(checkGraph(loadSeoGraph()))).toBe(false);
777
+ // the graph the plugin ships, evaluated through your Vite config
778
+ const { graph } = await evaluateAppGraph({ root: appRoot, mode: "production" });
779
+ expect(hasStructuralViolations(checkGraph(graph))).toBe(false);
741
780
 
742
781
  // render a page however you like, then assert the head it actually ships
743
782
  const report = inspectHtml(url, 200, html);
package/dist/audit.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import "./content-DVL2iyGU.js";
1
+ import "./content-B1cPtL8b.js";
2
2
  import * as Context from "effect/Context";
3
3
  import * as Effect from "effect/Effect";
4
4
  import * as Layer from "effect/Layer";
package/dist/audit.js CHANGED
@@ -1,4 +1,4 @@
1
- import { l as extractAnchors, r as extractPageContent, t as checkPageContent } from "./content-BE9KCTgb.js";
1
+ import { l as extractAnchors, r as extractPageContent, t as checkPageContent } from "./content-kcEnQdfi.js";
2
2
  import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import * as Context from "effect/Context";
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,6 @@
1
+ import { t as buildOnlyError } from "../message-DT_CjUdt.js";
2
+ //#region src/build-only/audit.ts
3
+ throw buildOnlyError("pagegraph/audit");
4
+ //#endregion
5
+
6
+ //# sourceMappingURL=audit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.js","names":[],"sources":["../../src/build-only/audit.ts"],"sourcesContent":["import { buildOnlyError } from \"./message\";\n\nthrow buildOnlyError(\"pagegraph/audit\");\n"],"mappings":";;AAEA,MAAM,eAAe,iBAAiB"}
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,6 @@
1
+ import { t as buildOnlyError } from "../message-DT_CjUdt.js";
2
+ //#region src/build-only/config.ts
3
+ throw buildOnlyError("pagegraph/config");
4
+ //#endregion
5
+
6
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","names":[],"sources":["../../src/build-only/config.ts"],"sourcesContent":["import { buildOnlyError } from \"./message\";\n\nthrow buildOnlyError(\"pagegraph/config\");\n"],"mappings":";;AAEA,MAAM,eAAe,kBAAkB"}
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,6 @@
1
+ import { t as buildOnlyError } from "../message-DT_CjUdt.js";
2
+ //#region src/build-only/tanstack-start.ts
3
+ throw buildOnlyError("pagegraph/tanstack-start");
4
+ //#endregion
5
+
6
+ //# sourceMappingURL=tanstack-start.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tanstack-start.js","names":[],"sources":["../../src/build-only/tanstack-start.ts"],"sourcesContent":["import { buildOnlyError } from \"./message\";\n\nthrow buildOnlyError(\"pagegraph/tanstack-start\");\n"],"mappings":";;AAEA,MAAM,eAAe,0BAA0B"}
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,6 @@
1
+ import { t as buildOnlyError } from "../message-DT_CjUdt.js";
2
+ //#region src/build-only/vite.ts
3
+ throw buildOnlyError("pagegraph/vite");
4
+ //#endregion
5
+
6
+ //# sourceMappingURL=vite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite.js","names":[],"sources":["../../src/build-only/vite.ts"],"sourcesContent":["import { buildOnlyError } from \"./message\";\n\nthrow buildOnlyError(\"pagegraph/vite\");\n"],"mappings":";;AAEA,MAAM,eAAe,gBAAgB"}