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 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
- # Placeholder package
2
-
3
- This empty package was published by [Tegami](https://tegami.fuma-nama.dev) to configure npm trusted publishing.
4
-
5
- The real package contents will be published via CI with OIDC.
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["&lt;head&gt; 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