pagegraph 0.0.0-tegami-trusted-publish-setup → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +388 -6
- package/dist/audit-B96V1x3q.js +1929 -0
- package/dist/audit-B96V1x3q.js.map +1 -0
- package/dist/audit.d.ts +557 -0
- package/dist/audit.js +2 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +47 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +77 -0
- package/dist/config.js +108 -0
- package/dist/config.js.map +1 -0
- package/dist/graph-BlLoEOw2.d.ts +122 -0
- package/dist/index.d.ts +141 -0
- package/dist/index.js +160 -0
- package/dist/index.js.map +1 -0
- package/dist/inspect-html-CHuoiO2s.js +452 -0
- package/dist/inspect-html-CHuoiO2s.js.map +1 -0
- package/dist/main-GFEobQTH.js +750 -0
- package/dist/main-GFEobQTH.js.map +1 -0
- package/dist/react.d.ts +12252 -0
- package/dist/react.js +411 -0
- package/dist/react.js.map +1 -0
- package/dist/vite.d.ts +28 -0
- package/dist/vite.js +308 -0
- package/dist/vite.js.map +1 -0
- package/package.json +120 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Saatvik Arya
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,6 +1,388 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
# pagegraph
|
|
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**.
|
|
7
|
+
|
|
8
|
+
```mermaid
|
|
9
|
+
flowchart LR
|
|
10
|
+
subgraph declare["declare once"]
|
|
11
|
+
route["staticData.seo<br/>+ head()"]
|
|
12
|
+
end
|
|
13
|
+
route --> seograph["SeoGraph<br/>(nodes + edges)"]
|
|
14
|
+
seograph --> sitemap["sitemap.xml"]
|
|
15
|
+
seograph --> robots["robots.txt"]
|
|
16
|
+
seograph --> check["pagegraph check · CI gate"]
|
|
17
|
+
route --> head["<head> meta · canonical<br/>Breadcrumbs · JSON-LD"]
|
|
18
|
+
gate["seoRouteConfig (vite)"] -. "fails undeclared pages" .-> route
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bun add pagegraph
|
|
25
|
+
bun add -D effect @effect/platform-bun # only if you use the CLI
|
|
26
|
+
bun add -D lighthouse # only for `pagegraph audit` performance evidence
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
| Entry | Exports | Peers |
|
|
30
|
+
| ---------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------- |
|
|
31
|
+
| `pagegraph` | `buildSeoGraph`, `renderSitemap`, `renderRobots`, `contentSignal`, `checkGraph`, `inspectHtml` | — |
|
|
32
|
+
| `pagegraph/react` | `createSeo` → `seoHead`, `Breadcrumbs`, JSON-LD generators | `react`, `@tanstack/react-router` |
|
|
33
|
+
| `pagegraph/vite` | `seoRouteConfig` coverage gate | `vite` |
|
|
34
|
+
| `pagegraph/config` | `defineSeoConfig`, `viteGraphLoader` | `vite` |
|
|
35
|
+
| `pagegraph/audit` | Audit services, scanner protocol, rules, and report schemas | `effect` |
|
|
36
|
+
| `pagegraph` bin | CLI over the same graph | `effect`, `@effect/platform-bun` (optional) |
|
|
37
|
+
|
|
38
|
+
The core and React entries have **zero runtime dependencies** — everything above is a
|
|
39
|
+
peer, and only the entries you import need theirs installed.
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
**1. Bind your site identity once.** Route files never see an origin or a brand name.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// lib/seo.ts
|
|
47
|
+
import { createSeo } from "pagegraph/react";
|
|
48
|
+
|
|
49
|
+
export const { seoHead } = createSeo({
|
|
50
|
+
origin: "https://example.com",
|
|
51
|
+
site: {
|
|
52
|
+
name: "Example",
|
|
53
|
+
logo: "/logo.png",
|
|
54
|
+
publisherLogo: "/publisher.png",
|
|
55
|
+
defaultImage: "/og.png",
|
|
56
|
+
defaultAuthor: { name: "Example Team" },
|
|
57
|
+
},
|
|
58
|
+
organization: {
|
|
59
|
+
legalName: "Example, Inc.",
|
|
60
|
+
description: "What the company does.",
|
|
61
|
+
sameAs: ["https://github.com/example"],
|
|
62
|
+
contactPoint: { contactType: "support", email: "hi@example.com" },
|
|
63
|
+
address: {
|
|
64
|
+
streetAddress: "123 Example Street",
|
|
65
|
+
addressLocality: "Example City",
|
|
66
|
+
addressRegion: "CA",
|
|
67
|
+
postalCode: "94105",
|
|
68
|
+
addressCountry: "US",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
website: { searchPath: "/docs?q={search_term_string}" },
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**2. Declare on the route.** Policy in `staticData.seo`, per-page content in `head()`.
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
export const Route = createFileRoute("/pricing")({
|
|
79
|
+
staticData: {
|
|
80
|
+
seo: {
|
|
81
|
+
kind: "page",
|
|
82
|
+
crumb: "Pricing",
|
|
83
|
+
sitemap: { priority: 0.9, changeFrequency: "weekly" },
|
|
84
|
+
related: ["/features", "/docs"],
|
|
85
|
+
link: { title: "Pricing", description: "Simple volume pricing." },
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
head: (ctx) =>
|
|
89
|
+
seoHead(ctx, {
|
|
90
|
+
title: "Pricing — Example",
|
|
91
|
+
description: "Simple volume pricing.",
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`seoHead` returns the `meta` + canonical `links` TanStack renders into `<head>` —
|
|
97
|
+
title, description, og/twitter cards, robots, and the JSON-LD each page warrants
|
|
98
|
+
(BreadcrumbList always; Article, FAQPage, Service, ItemList when the instance
|
|
99
|
+
declares them).
|
|
100
|
+
|
|
101
|
+
### Extensible JSON-LD
|
|
102
|
+
|
|
103
|
+
The built-in generators are conveniences, not a closed schema registry. Define any
|
|
104
|
+
`schema-dts` entity, link it to the plugin's stable site identities, and compose one
|
|
105
|
+
site graph:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import {
|
|
109
|
+
createSeo,
|
|
110
|
+
defineJsonLd,
|
|
111
|
+
extendJsonLd,
|
|
112
|
+
jsonLdRef,
|
|
113
|
+
} from "pagegraph/react";
|
|
114
|
+
|
|
115
|
+
export const seo = createSeo({
|
|
116
|
+
// site, organization, and website as above
|
|
117
|
+
jsonLd: {
|
|
118
|
+
site: (ids) => [
|
|
119
|
+
defineJsonLd({
|
|
120
|
+
"@type": "SoftwareApplication",
|
|
121
|
+
"@id": "https://example.com/#product",
|
|
122
|
+
name: "Example",
|
|
123
|
+
applicationCategory: "DeveloperApplication",
|
|
124
|
+
operatingSystem: "Web",
|
|
125
|
+
provider: jsonLdRef(ids.organization),
|
|
126
|
+
}),
|
|
127
|
+
],
|
|
128
|
+
transform: (entry) => {
|
|
129
|
+
if (entry.kind === "organization") {
|
|
130
|
+
return extendJsonLd(entry.document, {
|
|
131
|
+
slogan: "Ship with confidence.",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return entry.document;
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const siteGraph = seo.generateSiteGraphSchema();
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`generateSiteGraphSchema()` includes Organization, WebSite, and configured site
|
|
143
|
+
entities in one `@graph`. Organization and WebSite use stable `#organization` and
|
|
144
|
+
`#website` IDs; articles and services provided by the site reference the same
|
|
145
|
+
Organization. Existing individual generators remain available and are not transformed.
|
|
146
|
+
|
|
147
|
+
Add page-specific entities through `seoHead`:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
head: (ctx) =>
|
|
151
|
+
seo.seoHead(ctx, {
|
|
152
|
+
title: "Example for developers",
|
|
153
|
+
description: "A focused description of the product.",
|
|
154
|
+
jsonLd: [
|
|
155
|
+
defineJsonLd({
|
|
156
|
+
"@type": "SoftwareApplication",
|
|
157
|
+
name: "Example",
|
|
158
|
+
url: "https://example.com/product",
|
|
159
|
+
}),
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The transform sees discriminated generated and custom entries plus `origin`,
|
|
165
|
+
`canonical`, and `entityIds`. Return the document to keep it, no value (or `false`)
|
|
166
|
+
to suppress it, or an array to expand it. Output preserves caller order and never
|
|
167
|
+
silently deduplicates entities.
|
|
168
|
+
|
|
169
|
+
**3. Serve the projections.** Build the graph from your route tree, render strings.
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
// lib/seo/graph.ts — also the module the CLI loads
|
|
173
|
+
import { buildSeoGraph } from "pagegraph";
|
|
174
|
+
|
|
175
|
+
export const loadSeoGraph = () =>
|
|
176
|
+
buildSeoGraph({ routeTree, collections: [blogCollection] });
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
// routes/sitemap[.]xml.ts — robots[.]txt.ts is symmetric
|
|
181
|
+
import { renderRobots, renderSitemap } from "pagegraph";
|
|
182
|
+
|
|
183
|
+
renderSitemap(loadSeoGraph(), { origin, indexable: true });
|
|
184
|
+
renderRobots(loadSeoGraph(), {
|
|
185
|
+
origin,
|
|
186
|
+
indexable: true,
|
|
187
|
+
disallow: routeConfig.robotsExclusions,
|
|
188
|
+
contentSignal: "search=yes, ai-input=yes, ai-train=yes",
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`renderRobots` does not invent a Content-Signal. Pass `contentSignal` for
|
|
193
|
+
the value (the plugin prefixes `Content-Signal: `), `directives` for extra
|
|
194
|
+
group lines, and `transform` if you need to wrap or replace the whole file.
|
|
195
|
+
Preview hosts (`indexable: false`) drop `contentSignal` and `directives`;
|
|
196
|
+
`transform` still runs.
|
|
197
|
+
|
|
198
|
+
**4. Gate CI.** The same graph, the same rules, exit 1 on structural violations.
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
pagegraph check
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Typed paths
|
|
205
|
+
|
|
206
|
+
Augment `Register` (the same pattern TanStack Router uses) and `related` /
|
|
207
|
+
`redirectTo` are typed against your real route tree — a renamed route becomes a
|
|
208
|
+
compile error, not a dead link:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
declare module "pagegraph" {
|
|
212
|
+
interface Register {
|
|
213
|
+
paths: FileRouteTypes["fullPaths"];
|
|
214
|
+
kinds: "page" | "article" | "hub";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## The graph
|
|
220
|
+
|
|
221
|
+
`buildSeoGraph` walks the route tree and produces nodes (one per public URL) and
|
|
222
|
+
typed edges: `crumb-parent` (breadcrumb ancestry), `related` (deliberate
|
|
223
|
+
cross-links), `redirect`, and `collection-member`.
|
|
224
|
+
|
|
225
|
+
Dynamic pages — blog posts rendering through `/blog/$slug` — enter as
|
|
226
|
+
**collections**. Instances inherit the param route's declared policy, so
|
|
227
|
+
declarations stay the single source of truth:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
const blogCollection: SeoCollection = {
|
|
231
|
+
route: "/blog/$slug",
|
|
232
|
+
source: "blog",
|
|
233
|
+
instances: posts.map((p) => ({
|
|
234
|
+
path: `/blog/${p.slug}`,
|
|
235
|
+
title: p.title,
|
|
236
|
+
description: p.description,
|
|
237
|
+
publishedAt: p.date,
|
|
238
|
+
})),
|
|
239
|
+
};
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
`checkGraph` runs every rule against the graph and returns violations at two
|
|
243
|
+
severities:
|
|
244
|
+
|
|
245
|
+
- **structural** — internally broken declarations; these fail `pagegraph check` (exit 1):
|
|
246
|
+
dead or duplicate edges, path collisions, a redirect in the sitemap, a
|
|
247
|
+
sitemap/noindex contradiction, a `related` target with no `link` card, an
|
|
248
|
+
instance without a title.
|
|
249
|
+
- **editorial** — quality smells, reported but non-failing: duplicate or mis-sized
|
|
250
|
+
titles and descriptions.
|
|
251
|
+
|
|
252
|
+
## Coverage gate (Vite)
|
|
253
|
+
|
|
254
|
+
`seoRouteConfig` parses the route tree with `@tanstack/router-generator` (the same
|
|
255
|
+
parser as the router) and fails `vite build` when a page in an enforced group has
|
|
256
|
+
neither `staticData` nor `head`:
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
// vite.config.ts
|
|
260
|
+
import { seoRouteConfig } from "pagegraph/vite";
|
|
261
|
+
|
|
262
|
+
seoRouteConfig({
|
|
263
|
+
outputPath: fileURLToPath(new URL("./lib/route-config.ts", import.meta.url)),
|
|
264
|
+
publicGroups: ["(marketing)", "(docs)"],
|
|
265
|
+
enforceCoverageIn: ["(marketing)", "(docs)"],
|
|
266
|
+
alwaysDisallow: ["/dashboard", "/api"],
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
It also derives a small config module from the route tree: `robotsExclusions`
|
|
271
|
+
(feed to `renderRobots`) and `reservedSegments` (top-level segments an app must
|
|
272
|
+
not hand out as tenant/org slugs).
|
|
273
|
+
|
|
274
|
+
## CLI
|
|
275
|
+
|
|
276
|
+
The graph commands acquire your graph through `seo.config.ts` at the app root —
|
|
277
|
+
the `viteGraphLoader` evaluates your graph module inside a headless Vite server,
|
|
278
|
+
so path aliases, content plugins, and virtual modules all resolve:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
// seo.config.ts
|
|
282
|
+
import { defineSeoConfig, viteGraphLoader } from "pagegraph/config";
|
|
283
|
+
|
|
284
|
+
export default defineSeoConfig({
|
|
285
|
+
origin: "https://example.com",
|
|
286
|
+
disallow: routeConfig.robotsExclusions,
|
|
287
|
+
contentSignal: "search=yes, ai-input=yes, ai-train=yes",
|
|
288
|
+
loadGraph: viteGraphLoader({
|
|
289
|
+
root: import.meta.dirname,
|
|
290
|
+
entry: "/lib/seo/graph.ts",
|
|
291
|
+
exportName: "loadSeoGraph",
|
|
292
|
+
}),
|
|
293
|
+
});
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
pagegraph check # CI gate — exit 1 on structural violations
|
|
298
|
+
pagegraph graph # the graph as a tree · --format mermaid | json
|
|
299
|
+
pagegraph inspect /pricing # one node: policy, sitemap status, in/out edges
|
|
300
|
+
pagegraph inspect <url> --live # fetch a deployed page, validate its rendered <head>
|
|
301
|
+
pagegraph sitemap # print sitemap.xml
|
|
302
|
+
pagegraph robots # print robots.txt
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Stdout is data, stderr is status — `pagegraph check --json | jq` just works.
|
|
306
|
+
|
|
307
|
+
### Audit any website
|
|
308
|
+
|
|
309
|
+
`pagegraph audit` is framework-independent and does not need `seo.config.ts`. It
|
|
310
|
+
validates target URLs before making requests, follows redirects through the same
|
|
311
|
+
validation boundary, inspects the rendered document and discovery files, and
|
|
312
|
+
can collect Lighthouse evidence through a validating proxy.
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
pagegraph audit https://example.com
|
|
316
|
+
pagegraph audit https://example.com https://example.com/docs --json | jq
|
|
317
|
+
pagegraph audit https://localhost:3000 --allow-private --probe-only
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
The report keeps evidence and findings separate: scanners collect observations;
|
|
321
|
+
pure rules classify structural failures and editorial quality issues. One failed
|
|
322
|
+
scanner does not erase successful evidence from the others. JSON mode emits one
|
|
323
|
+
versioned document on stdout, while diagnostics and artifact paths stay on
|
|
324
|
+
stderr.
|
|
325
|
+
|
|
326
|
+
Use `--probe-only` when Lighthouse is unavailable or unnecessary. Private and
|
|
327
|
+
reserved destinations are rejected unless `--allow-private` is explicit; that
|
|
328
|
+
flag is intended for local development and CI fixtures.
|
|
329
|
+
|
|
330
|
+
### Compare audit artifacts
|
|
331
|
+
|
|
332
|
+
Write timestamped reports from two revisions, then compare their semantic SEO
|
|
333
|
+
outcomes without failing on timestamps, timing, or other raw scanner evidence:
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
pagegraph audit https://example.com --output-dir .audit/before
|
|
337
|
+
# deploy or check out the next revision
|
|
338
|
+
pagegraph audit https://example.com --output-dir .audit/after
|
|
339
|
+
pagegraph diff .audit/before/<report>.json .audit/after/<report>.json
|
|
340
|
+
pagegraph diff .audit/before/<report>.json .audit/after/<report>.json --json | jq
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
| Change | Outcome | Exit |
|
|
344
|
+
| --- | --- | ---: |
|
|
345
|
+
| Timestamp, warning text, or raw scanner evidence only | `unchanged` | 0 |
|
|
346
|
+
| Editorial finding or improvement | `changed` | 0 |
|
|
347
|
+
| Structural finding, scanner degradation, or lost coverage | `regressed` | 1 |
|
|
348
|
+
|
|
349
|
+
Inputs must satisfy the published `AuditReport` schema version 1. JSON mode
|
|
350
|
+
emits a separately versioned `audit-diff` document even when a regression makes
|
|
351
|
+
the command exit 1; operational and schema failures write only to stderr.
|
|
352
|
+
|
|
353
|
+
Raw scanner evidence remains in the source artifacts but is not interpreted by
|
|
354
|
+
the generic comparator. Performance gates should be expressed as scanner
|
|
355
|
+
findings with explicit thresholds rather than inferred from volatile Lighthouse
|
|
356
|
+
measurements. Finding payloads (`message`, `fix`, `observed`, and `expected`)
|
|
357
|
+
remain semantic: same-severity changes are reported as non-blocking `changed`
|
|
358
|
+
outcomes.
|
|
359
|
+
|
|
360
|
+
## Testing
|
|
361
|
+
|
|
362
|
+
Everything the CLI checks is a pure function you can call from a test:
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
import {
|
|
366
|
+
checkGraph,
|
|
367
|
+
hasStructuralViolations,
|
|
368
|
+
inspectHtml,
|
|
369
|
+
} from "pagegraph";
|
|
370
|
+
|
|
371
|
+
expect(hasStructuralViolations(checkGraph(loadSeoGraph()))).toBe(false);
|
|
372
|
+
|
|
373
|
+
// render a page however you like, then assert the head it actually ships
|
|
374
|
+
const report = inspectHtml(url, 200, html);
|
|
375
|
+
expect(report.issues).toEqual([]);
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## TanStack Start example
|
|
379
|
+
|
|
380
|
+
The [TanStack Start cookbook](./examples/tanstack-start/README.md) shows the complete wiring in
|
|
381
|
+
one place: route-declared metadata, the graph loader, sitemap and robots projections, JSON-LD,
|
|
382
|
+
the Vite coverage gate, and the CLI inspection and `pagegraph diff` workflow. It is intentionally
|
|
383
|
+
framework-neutral beyond the Start route seams, so you can copy the modules into an existing
|
|
384
|
+
Start app and keep your own route tree and content collections.
|
|
385
|
+
|
|
386
|
+
## License
|
|
387
|
+
|
|
388
|
+
MIT
|