@t09tanaka/stoneage 0.1.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 (47) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +21 -0
  3. package/README.md +768 -0
  4. package/dist/agent-skill.d.ts +1 -0
  5. package/dist/agent-skill.js +140 -0
  6. package/dist/assets.d.ts +125 -0
  7. package/dist/assets.js +341 -0
  8. package/dist/cli.d.ts +236 -0
  9. package/dist/cli.js +3077 -0
  10. package/dist/core.d.ts +473 -0
  11. package/dist/core.js +2897 -0
  12. package/dist/data.d.ts +121 -0
  13. package/dist/data.js +358 -0
  14. package/dist/deploy.d.ts +34 -0
  15. package/dist/deploy.js +203 -0
  16. package/dist/dev.d.ts +36 -0
  17. package/dist/dev.js +313 -0
  18. package/dist/example.d.ts +134 -0
  19. package/dist/example.js +1272 -0
  20. package/dist/fragment/client.d.ts +13 -0
  21. package/dist/fragment/client.js +150 -0
  22. package/dist/fragment.d.ts +15 -0
  23. package/dist/fragment.js +27 -0
  24. package/dist/html.d.ts +57 -0
  25. package/dist/html.js +208 -0
  26. package/dist/images.d.ts +95 -0
  27. package/dist/images.js +292 -0
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.js +1 -0
  30. package/dist/migration.d.ts +157 -0
  31. package/dist/migration.js +983 -0
  32. package/dist/optimize.d.ts +15 -0
  33. package/dist/optimize.js +215 -0
  34. package/dist/pagination.d.ts +26 -0
  35. package/dist/pagination.js +62 -0
  36. package/dist/paths.d.ts +1 -0
  37. package/dist/paths.js +10 -0
  38. package/dist/search.d.ts +32 -0
  39. package/dist/search.js +55 -0
  40. package/dist/testing.d.ts +66 -0
  41. package/dist/testing.js +97 -0
  42. package/dist/validate.d.ts +2 -0
  43. package/dist/validate.js +1 -0
  44. package/jsx-loader.mjs +52 -0
  45. package/jsx-register.mjs +7 -0
  46. package/package.json +135 -0
  47. package/tsconfig.base.json +16 -0
package/README.md ADDED
@@ -0,0 +1,768 @@
1
+ # StoneAge
2
+
3
+ StoneAge is a data-site static site generator for publishing large, structured
4
+ datasets as fast, plain HTML.
5
+
6
+ It is not intended to be a general-purpose web application framework. The core
7
+ use case is a site with tens of thousands of generated pages, normalized data
8
+ exports, stable URLs, rich metadata, and a build pipeline that can regenerate
9
+ only the pages affected by changed inputs.
10
+
11
+ ## Start Here
12
+
13
+ - Build a new site: start with [Getting Started](docs/getting-started.md), then
14
+ use [Site Build Guide](docs/site-build.md) for route families, artifacts,
15
+ metadata, assets, validation, and publishing.
16
+ - Define reusable templates: read [Components](docs/components.md) for TSX,
17
+ `html()`, `renderToString()`, static components, and opt-in islands.
18
+ - Migrate an existing SvelteKit static site: run the audit command and follow
19
+ [Migration Guide](docs/migration.md). StoneAge migration is not an adapter;
20
+ it converts routes, data, endpoints, and layouts into explicit static build
21
+ inputs.
22
+ - Understand the data pipeline: read [Data Flow](docs/data-flow.md) for
23
+ normalized data, public artifacts, view models, and dependency tracking.
24
+
25
+ ## Goals
26
+
27
+ - Generate plain HTML as the default output.
28
+ - Avoid required runtime hydration payloads for page rendering.
29
+ - Keep data normalization separate from HTML rendering.
30
+ - Generate JSON, XML, CSV, and other non-HTML artifacts through explicit
31
+ artifact routes that share dependency caching without becoming page payloads.
32
+ - Regenerate pages incrementally from input hashes and dependency metadata.
33
+ - Remove stale page and artifact outputs from previous full builds when they
34
+ disappear from the current manifest.
35
+ - Provide typed route definitions for large page families.
36
+ - Reject duplicate page paths, duplicate artifact paths, and page/artifact
37
+ output file collisions before rendering starts.
38
+ - Split large listing pages with pagination helpers and stable first-page URLs.
39
+ - Configure trailing slash behavior for canonical URLs, sitemap entries, link
40
+ validation, and output file paths.
41
+ - Split large sitemaps into a sitemap index plus `sitemap-N.xml` URL set files
42
+ when a site wants bounded sitemap file sizes.
43
+ - Add page-level prefetch links for likely next navigations without requiring a
44
+ Service Worker. Sites can also opt into a tiny Service Worker prefetch layer
45
+ that only warms URLs declared by the current page, and can disable duplicate
46
+ native prefetch links when the Service Worker is the intended strategy.
47
+ - Generate metadata such as titles, descriptions, canonical URLs, language,
48
+ favicon links, Open Graph tags, Twitter card tags, noindex robots tags,
49
+ structured custom head tags, and sitemaps during HTML generation. Open Graph
50
+ output can be disabled at the site level and re-enabled for high-value pages.
51
+ - Write publishing manifests such as `robots.txt` and generic
52
+ `redirects.json` files from the same build configuration.
53
+ - Keep assets small and explicit: shared CSS, page-type CSS when needed, and
54
+ hand-authored JavaScript only for interactive islands.
55
+ - Optimize local raster images into responsive variants when optional `sharp`
56
+ support is available, while keeping builds functional when it is not.
57
+ - Allow component-style HTML composition, including optional TSX components,
58
+ without adding a client hydration runtime.
59
+ - Ship validation commands for generated page counts, missing links, exports,
60
+ public JSON/CSV syntax, sitemap integrity, missing asset references, and large
61
+ HTML outputs.
62
+
63
+ ## Non-Goals
64
+
65
+ - StoneAge is not a React, Vue, or Svelte application framework.
66
+ - StoneAge does not require client-side hydration to display generated pages.
67
+ - StoneAge does not mix public data exports with HTML-only view models.
68
+ - StoneAge does not bundle source or normalized data into the published site by
69
+ default.
70
+ - StoneAge does not assume every generated page should be rebuilt on every run.
71
+
72
+ ## Architecture
73
+
74
+ StoneAge is a TypeScript framework with Vite integration where it is useful for
75
+ asset bundling and development ergonomics.
76
+
77
+ The build pipeline is organized around three separate stages:
78
+
79
+ 1. Normalize source data into internal JSON, CSV, or API snapshots.
80
+ 2. Derive public data artifacts and HTML view models from those normalized
81
+ snapshots.
82
+ 3. Render HTML pages, metadata, sitemaps, assets, and validation reports from
83
+ the view models.
84
+
85
+ The incremental build cache records source hashes, page dependencies, output
86
+ paths, generated links, and metadata. A changed member record, session record, or
87
+ conversation record only invalidates pages that depend on that input.
88
+ View models with record-level dependencies avoid inheriting a broad public data
89
+ artifact hash, while view models without record dependencies fall back to the
90
+ artifact hash for correctness.
91
+ StoneAge also fingerprints route family metadata defaults, page metadata,
92
+ canonical URLs, site-level head settings, title templates, structured custom
93
+ head tags, stylesheet references, and page-local asset/island declarations so
94
+ metadata-only changes do not leave stale HTML head output behind.
95
+ For very large sites that already compute a complete input hash, pass
96
+ `buildFingerprint` to skip route enumeration entirely when the hash matches the
97
+ previous build report. Pass `renderFingerprint` for source templates, CSS
98
+ inputs, and artifact renderers; when it changes, StoneAge invalidates cached
99
+ page and artifact outputs even if their data dependencies are unchanged.
100
+ Full builds remove manifest-managed page and artifact outputs that no longer
101
+ exist in the current route plan. Partial builds preserve omitted outputs because
102
+ they intentionally describe only a subset of the site.
103
+
104
+ ## Example Route Families
105
+
106
+ StoneAge supports route families such as:
107
+
108
+ ```text
109
+ /members/:id/:year/
110
+ /conversations/:id/
111
+ /sessions/:id/
112
+ /bills/:id/
113
+ ```
114
+
115
+ Route implementations should be typed generation functions, not ad hoc string
116
+ concatenation spread through project code.
117
+
118
+ Use typed route helpers to bind a pattern and its required params in one place:
119
+
120
+ ```ts
121
+ import {
122
+ defineRouteFamily,
123
+ pathForRedirect,
124
+ patternParamMatcher,
125
+ } from "@t09tanaka/stoneage";
126
+
127
+ const members = defineRouteFamily({
128
+ name: "members",
129
+ pattern: "/members/:id/:year/",
130
+ paramMatchers: {
131
+ year: patternParamMatcher(/^\d{4}$/),
132
+ },
133
+ metadata: {
134
+ description: "A generated member page",
135
+ ogType: "profile",
136
+ },
137
+ entries: () => [
138
+ {
139
+ params: { id: "m1", year: 2026 },
140
+ dependencies: ["member:member-001"],
141
+ metadata: {
142
+ title: "Member profile",
143
+ },
144
+ render: () =>
145
+ `<main><a href="${members.path({ id: "m1", year: 2026 })}">Profile</a></main>`,
146
+ },
147
+ ],
148
+ });
149
+
150
+ const legacyMemberUrl = pathForRedirect("/members/:id/", { id: "m1" });
151
+ ```
152
+
153
+ ## Project Status
154
+
155
+ This repository contains the release-candidate implementation of the data-site SSG core:
156
+ typed route and artifact families, incremental dependency caching, metadata and
157
+ sitemap generation, client asset and image helpers, validation commands,
158
+ deployment manifest generation, a large benchmark example, and package audit
159
+ checks. The migration audit also covers SvelteKit route entries, layout chains,
160
+ page actions, static endpoint methods, and reset route files so a large static
161
+ site can be moved without introducing an adapter layer.
162
+
163
+ ## Example Site And Performance Work
164
+
165
+ The example site is part of the product, not a throwaway demo. It contains
166
+ enough generated data to expose real scaling problems: many records, many route
167
+ families, large index pages, metadata-heavy detail pages, exports, and sitemap
168
+ generation.
169
+
170
+ The framework measures the example site quantitatively and optimizes from
171
+ those measurements. Useful metrics include build time, incremental build time,
172
+ generated page count, total output size, largest HTML files, CSS and JavaScript
173
+ payloads, image output size, image optimization results, route-family write
174
+ counts, link-check time, sitemap size, and Lighthouse results for selected
175
+ pages.
176
+
177
+ The large example benchmark disables Open Graph tags by default to keep repeated
178
+ detail pages small while preserving title, description, canonical, and sitemap
179
+ metadata.
180
+
181
+ The example keeps internal record IDs separate from public URL slugs. Dependency
182
+ tracking and optional exports keep stable source IDs, while generated HTML,
183
+ links, and sitemap entries use shorter stable route slugs to reduce repeated
184
+ payload.
185
+
186
+ Tailwind CSS may be used as the default styling pipeline, but generated CSS must
187
+ remain small and predictable. StoneAge can build Tailwind output from explicit
188
+ route/component class candidates, which avoids scanning generated HTML at large
189
+ page counts. Existing Vite, PostCSS, or Tailwind pipelines can also stay outside
190
+ the HTML generator: build CSS as a normal asset, read the Vite manifest when
191
+ needed, and attach the resulting stylesheet paths through page metadata or the
192
+ client asset registry.
193
+
194
+ Client JavaScript is opt-in per page. Declare small assets or islands once, then
195
+ attach their ids from page metadata:
196
+
197
+ ```ts
198
+ import { defineClientAssets, island, stylesheet } from "@t09tanaka/stoneage/assets";
199
+ import { html, islandMount, renderToString } from "@t09tanaka/stoneage/html";
200
+
201
+ const client = defineClientAssets({
202
+ search: [
203
+ stylesheet("/assets/search.css", { source: "asset-build/search.css" }),
204
+ island("/assets/search.js", {
205
+ source: "asset-build/search.js",
206
+ attributes: { "data-island": "search" },
207
+ }),
208
+ ],
209
+ });
210
+
211
+ await buildSite({
212
+ outDir: "dist",
213
+ site,
214
+ publishing: { headers: true },
215
+ assets: {
216
+ client,
217
+ },
218
+ routes: [
219
+ {
220
+ name: "search",
221
+ pattern: "/search/",
222
+ entries: async () => [
223
+ {
224
+ params: {},
225
+ metadata: {
226
+ title: "Search",
227
+ description: "Search the archive",
228
+ islands: ["search"],
229
+ },
230
+ render: () =>
231
+ renderToString(
232
+ html(
233
+ "main",
234
+ null,
235
+ islandMount("search", {
236
+ key: "site-search",
237
+ props: { index: "/search.json" },
238
+ }),
239
+ ),
240
+ ),
241
+ },
242
+ ],
243
+ },
244
+ ],
245
+ });
246
+ ```
247
+
248
+ Assets declared with a `source` file are copied with a content hash in the
249
+ filename, their HTML references are rewritten, and `publishing.headers` adds an
250
+ immutable cache-control entry. Plain string paths and `assets.public` entries
251
+ continue to use fixed filenames.
252
+
253
+ For Google Fonts CSS2 subsets, use `googleFontsStylesheet("/assets/fonts.css",
254
+ { families: ["Zen Maru Gothic:wght@700;900", "Outfit:wght@700;900"], text:
255
+ subsetText })`. StoneAge fetches the CSS and WOFF2 files at build time, rewrites
256
+ `@font-face` URLs to local content-hashed assets, and emits immutable cache
257
+ headers for the generated CSS and font files.
258
+
259
+ For Vite builds, `loadViteBuildAssetsFromManifest("dist/.vite/manifest.json", {
260
+ sourceDir: "dist", base: "/", entriesOnly: true })` reads a Vite manifest and
261
+ returns both `assets.client` and `assets.public` inputs. That lets a separate
262
+ Vite, PostCSS, or Tailwind pipeline produce hashed CSS, JS, and imported static
263
+ assets while StoneAge handles page injection and output-directory sync. Imported
264
+ chunk CSS is emitted before entry CSS, duplicate stylesheet paths are removed,
265
+ and `{ entriesOnly: true }` can keep non-entry chunks out of the client asset
266
+ registry while still copying emitted public files. Use `assetsFromViteManifest()`
267
+ or `publicAssetsFromViteManifest()` when a build script already has the manifest
268
+ object in memory and only needs one side of the mapping.
269
+
270
+ Use `copyPublicAssets()` when those external pipelines write assets outside the
271
+ StoneAge output directory. It copies files into stable public paths, rejects
272
+ paths that would escape the output directory or collide with another configured
273
+ asset, and skips unchanged files so asset sync does not force unrelated page
274
+ rewrites. Full builds remove managed public assets that were listed in the
275
+ previous report but are no longer configured; partial builds preserve them.
276
+ Pages still render as plain HTML and no hydration payload is generated by
277
+ default. `islandMount()` emits a root `data-island` element and only writes an
278
+ adjacent JSON props script when a page explicitly passes props for that island.
279
+
280
+ Deferred fragments can move supplemental HTML out of the initial page while
281
+ keeping the output static. Use `defineFragmentFamily()` for fragment outputs and
282
+ `deferredFragment()` from `@t09tanaka/stoneage/fragment` for the placeholder.
283
+ Fragments are emitted under `/_fragments/` as `.html` artifacts with
284
+ `text/html; charset=utf-8` and `x-robots-tag: noindex`; they are not pages, do
285
+ not receive metadata or canonical URLs, and are not added to sitemaps. Keep a
286
+ custom fallback link when you want the no-JavaScript path to use a canonical
287
+ page instead of the default fragment self-link. Pass `fallback: null` only when
288
+ the deferred content can be omitted without JavaScript. When fragments are
289
+ configured, StoneAge emits and wires the built-in client controller
290
+ automatically; pass `fragments: { families, client: false }` to keep manual
291
+ wiring. The controller supports `visible`, `details-open`, `idle`, and `manual`
292
+ triggers.
293
+
294
+ The package entrypoints are explicit. Import core build APIs from
295
+ `@t09tanaka/stoneage`, client asset helpers from `@t09tanaka/stoneage/assets`,
296
+ data-flow helpers from `@t09tanaka/stoneage/data`, deploy artifact helpers from
297
+ `@t09tanaka/stoneage/deploy`, TSX helpers from `@t09tanaka/stoneage/html`,
298
+ deferred fragment helpers from `@t09tanaka/stoneage/fragment`,
299
+ image helpers from `@t09tanaka/stoneage/images`, SvelteKit migration audit
300
+ helpers from `@t09tanaka/stoneage/migration`, optimization helpers from
301
+ `@t09tanaka/stoneage/optimize`, pagination helpers from
302
+ `@t09tanaka/stoneage/pagination`, and optional search-index helpers from
303
+ `@t09tanaka/stoneage/search`.
304
+
305
+ Data is injected at build time to prebuild the static site. Public JSON or CSV
306
+ exports are optional publishing features, not required page payloads.
307
+ Normalized snapshots can live outside `outDir` or under an internal
308
+ `.stoneage/normalized` path; they are not published unless you expose them
309
+ through artifact routes.
310
+
311
+ Large data producers can pass a complete `contentFingerprint` plus
312
+ `cacheManifestPath` to `writeNormalizedJsonData()` or
313
+ `writeNormalizedCsvData()`. When the fingerprint and manifest entry match an
314
+ existing snapshot, StoneAge reuses the snapshot metadata without rendering the
315
+ JSON/CSV string or reading the existing snapshot body:
316
+
317
+ ```ts
318
+ await writeNormalizedJsonData({
319
+ key: "public:members",
320
+ path: ".stoneage/normalized/members.json",
321
+ data: members,
322
+ contentFingerprint: { hash: membersContentHash },
323
+ cacheManifestPath: ".stoneage/normalized-manifest.json",
324
+ });
325
+ ```
326
+
327
+ The fingerprint must cover the full rendered snapshot content. This fast path
328
+ is for pipelines that already hash normalized records or shards before HTML
329
+ rendering.
330
+
331
+ Non-HTML outputs use artifact routes. They are cached and reported alongside
332
+ HTML pages, but they do not receive HTML metadata and are not added to the
333
+ sitemap:
334
+
335
+ ```ts
336
+ import {
337
+ artifactEntriesFromPublicData,
338
+ definePublicDataJsonArtifacts,
339
+ renderCsvArtifact,
340
+ writeNormalizedJsonData,
341
+ } from "@t09tanaka/stoneage/data";
342
+
343
+ const publicMembers = await writeNormalizedJsonData({
344
+ key: "public:members",
345
+ path: "data/normalized/members.json",
346
+ data: members,
347
+ sourceDependencies: [{ key: "source:members", file: "data/source/members.csv" }],
348
+ });
349
+
350
+ await buildSite({
351
+ outDir: "dist",
352
+ site,
353
+ routes,
354
+ artifacts: [
355
+ {
356
+ name: "csvExports",
357
+ pattern: "/exports/:name.csv",
358
+ entries: () =>
359
+ artifactEntriesFromPublicData([publicMembers], {
360
+ params: () => ({ name: "members" }),
361
+ render: (artifact) =>
362
+ renderCsvArtifact(artifact.data, [
363
+ { header: "id", value: (member) => member.id },
364
+ { header: "name", value: (member) => member.name },
365
+ ]),
366
+ }),
367
+ },
368
+ definePublicDataJsonArtifacts([publicMembers], {
369
+ path: (artifact) => `/data/${artifact.key.replace("public:", "")}.json`,
370
+ }),
371
+ ],
372
+ });
373
+ ```
374
+
375
+ `artifactEntriesFromPublicData()` can turn normalized public data artifacts into
376
+ artifact route entries while preserving their source dependencies. This is the
377
+ intended migration path for endpoint-style outputs such as `site-data.json`,
378
+ `search-index.json`, public CSV exports, and other SvelteKit `+server.ts`
379
+ equivalents. `renderJsonArtifact()` and `renderCsvArtifact()` provide small,
380
+ framework-independent renderers for those public exports. When a migrated
381
+ SvelteKit `+server.ts` endpoint needs response-style metadata, return
382
+ `jsonArtifact()`, `csvArtifact()`, `xmlArtifact()`, `textArtifact()`, or
383
+ `artifactResponse()` from `@t09tanaka/stoneage`; StoneAge writes the static body and records
384
+ `contentType`, `status`, and `headers` in the generated manifest.
385
+ When `publishing` is enabled, artifact `contentType` and headers are also
386
+ collected into a host-neutral `headers.json` manifest unless
387
+ `publishing.headers` is set to `false`. Deployment tooling can translate that
388
+ manifest to provider-specific header rules without mixing endpoint metadata into
389
+ page renderers.
390
+ `definePublicDataJsonArtifacts()` can publish split JSON data files plus a
391
+ small `site-data.json` manifest that lists their public paths. The manifest
392
+ depends on the list of exported files, while each data file keeps its own source
393
+ and public-data dependencies.
394
+
395
+ Optimized image variants can be consumed as structured attributes or rendered
396
+ directly as compact HTML:
397
+
398
+ ```ts
399
+ import { optimizeImages, pictureHtmlFor } from "@t09tanaka/stoneage/images";
400
+
401
+ const result = await optimizeImages({
402
+ outDir: "dist",
403
+ images: [{ source: "assets/member.jpg", publicPath: "/images/member.jpg" }],
404
+ widths: [320, 640, 960],
405
+ formats: ["avif", "webp", "jpeg"],
406
+ });
407
+
408
+ const picture = pictureHtmlFor(result.generated, {
409
+ src: "/images/member.jpg",
410
+ width: 960,
411
+ height: 640,
412
+ alt: "Member portrait",
413
+ sizes: "(min-width: 48rem) 33vw, 100vw",
414
+ loading: "lazy",
415
+ decoding: "async",
416
+ fallbackFormat: "jpeg",
417
+ });
418
+ ```
419
+
420
+ Paginated index routes can keep navigation data outside templates:
421
+
422
+ ```ts
423
+ import { paginationLinks } from "@t09tanaka/stoneage/pagination";
424
+
425
+ const nav = paginationLinks(page.page, conversations.length, {
426
+ pageSize: 50,
427
+ pathForPage: (pageNumber) =>
428
+ pageNumber === 1 ? "/conversations/" : `/conversations/page/${pageNumber}/`,
429
+ });
430
+ ```
431
+
432
+ The helper returns `previous`, `next`, and a stable page list. It does not render
433
+ HTML, so components can decide whether to expose links, metadata head tags, or
434
+ prefetch hints.
435
+
436
+ Search indexes are just optional artifacts. `defineSearchIndexArtifact()` can
437
+ publish a stable `search-index.json` for client-side search without making that
438
+ JSON required for ordinary page rendering:
439
+
440
+ ```ts
441
+ import { defineSearchIndexArtifact } from "@t09tanaka/stoneage/search";
442
+
443
+ await buildSite({
444
+ outDir: "dist",
445
+ site,
446
+ routes,
447
+ artifacts: [
448
+ defineSearchIndexArtifact({
449
+ records: members,
450
+ dependencies: members.map((member) => ({
451
+ key: `member:${member.id}`,
452
+ value: member,
453
+ })),
454
+ document: (member) => ({
455
+ id: member.id,
456
+ title: member.name,
457
+ url: `/members/${member.slug}/${member.year}/`,
458
+ tags: ["member", member.district],
459
+ }),
460
+ }),
461
+ ],
462
+ });
463
+ ```
464
+
465
+ Route URLs use trailing slashes by default and write directory-style output such
466
+ as `members/m1/2026/index.html`. Sites that need extension-style output can set
467
+ `trailingSlash: "never"`:
468
+
469
+ ```ts
470
+ await buildSite({
471
+ outDir: "dist",
472
+ trailingSlash: "never",
473
+ site,
474
+ routes,
475
+ });
476
+ ```
477
+
478
+ This normalizes generated paths, canonical URLs, sitemap entries, internal link
479
+ validation, and page-local prefetch hints. Route renderers still own their HTML
480
+ body links, so projects should emit body hrefs that match their chosen URL
481
+ style.
482
+
483
+ For large route families, entries may declare their internal body links with
484
+ `links`. When present, StoneAge records those normalized links directly instead
485
+ of scanning the rendered HTML for anchors:
486
+
487
+ ```ts
488
+ {
489
+ params: { id: member.slug },
490
+ links: conversations.map((conversation) => `/conversations/${conversation.slug}/`),
491
+ render: () => renderMember(member, conversations),
492
+ }
493
+ ```
494
+
495
+ Large sites can split sitemap output while keeping `sitemap.xml` as the public
496
+ entry point:
497
+
498
+ ```ts
499
+ await buildSite({
500
+ outDir: "dist",
501
+ site,
502
+ sitemap: {
503
+ maxUrlsPerFile: 10_000,
504
+ },
505
+ routes,
506
+ });
507
+ ```
508
+
509
+ When the indexable page count exceeds the limit, StoneAge writes `sitemap.xml`
510
+ as a sitemap index and writes child URL sets such as `sitemap-1.xml` and
511
+ `sitemap-2.xml`. `validate` follows the index and compares the combined child
512
+ URLs against the generated manifest. It also reports sitemap index entries that
513
+ point at missing child sitemap files.
514
+
515
+ Useful commands:
516
+
517
+ ```sh
518
+ npm run benchmark
519
+ npm run benchmark -- --trailing-slash never
520
+ npm run benchmark -- --public-data-exports
521
+ npm run benchmark:large
522
+ npm run benchmark:large:compare
523
+ npm run check:publish
524
+ npm run optimize
525
+ npm run deploy -- --provider netlify
526
+ npm run dev -- --out-dir example/dist --watch src --watch docs --build-command "npm run benchmark"
527
+ npm run audit:sveltekit -- --routes-dir ../example-sveltekit/src/routes --params-dir ../example-sveltekit/src/params --json --report migration/sveltekit-audit.json
528
+ npm run audit:sveltekit -- --routes-dir ../example-sveltekit/src/routes --params-dir ../example-sveltekit/src/params --migration-plan --json --report migration/sveltekit-plan.json
529
+ npm run audit:sveltekit -- --routes-dir ../example-sveltekit/src/routes --params-dir ../example-sveltekit/src/params --emit-stubs migration/stubs
530
+ npm run inspect -- --dependency member:member-001
531
+ npm run --silent inspect -- --dependency member:member-001 --json
532
+ npm run validate
533
+ npm run validate -- --max-html-bytes 25000 --max-css-bytes 50000 --max-js-bytes 10000 --max-public-file-bytes 2000000 --max-total-public-bytes 50000000 --max-image-bytes 10000000 --max-dependency-fanout 500
534
+ npm run validate -- --validation-config validation.json
535
+ npm run validate -- --write-validation-config validation.json
536
+ npm run --silent validate -- --json --report example/dist/.stoneage/validation-summary.json
537
+ npm run check:package
538
+ npx @t09tanaka/stoneage --help
539
+ ```
540
+
541
+ `inspect --dependency <key>` reads the generated manifest and lists the pages and
542
+ artifacts that currently depend on that key. Use `--json` for scripts that plan
543
+ partial rebuilds or audit the dependency graph.
544
+
545
+ `dev` serves the generated output directory and injects a small Server-Sent
546
+ Events reload client into HTML responses. Use `--build-command <command>` with
547
+ one or more `--watch <path>` options to rebuild on source changes; browsers
548
+ reload only after the build command succeeds. StoneAge ignores the output
549
+ directory, `.git`, and `node_modules` while watching to avoid rebuild loops.
550
+
551
+ `benchmark --compare-to <file>` compares the current `.stoneage/benchmark.json`
552
+ metrics with a previous benchmark report and fails the command when size metrics
553
+ grow beyond `--max-size-regression-percent` (default `0`). The comparison is
554
+ limited to deterministic payload metrics such as total/public output bytes,
555
+ HTML/CSS/JS/image/data bytes, HTML averages, and p95 HTML size. Build times are
556
+ reported but not used as pass/fail gates because they are environment-dependent.
557
+ Use `--comparison-report <file>` when CI
558
+ should persist the pass/fail result, compared metric count, baseline path,
559
+ current report path, and individual regressions as JSON.
560
+ Use `npm run benchmark:large:compare` for CI baseline comparisons against the
561
+ committed `baselines/benchmark-large.json` report. Use `npm run check:publish`
562
+ when a publish gate should run that benchmark comparison and validate the output
563
+ with `validation.json`. Runtime compression is expected to be handled by the
564
+ serving layer, such as CloudFront, so routine benchmark and publish checks do not
565
+ precompress or compare Brotli/Gzip transfer estimates.
566
+
567
+ `validate` reads the generated `.stoneage/report.json`, split data-flow reports
568
+ (`.stoneage/data-flow-summary.json` and
569
+ `.stoneage/data-flow-dependencies.json`), and sitemap output, then reports page counts,
570
+ artifact counts, managed public asset counts and copy/skip counts, missing links, required metadata issues, duplicate or missing
571
+ canonical URLs, sitemap drift, dependency fanout, route/artifact data-flow
572
+ summaries, public JSON/CSV exports, sitemap URL count, sitemap file count,
573
+ duplicate sitemap URLs, invalid sitemap URL values, missing child sitemap files,
574
+ JSON parse failures, basic CSV structural failures,
575
+ missing title/description/canonical tags in generated HTML, generated canonical
576
+ URL duplicates, canonical paths that do not match the manifest,
577
+ framework hydration payload markers or runtime `__data.json` page data
578
+ references,
579
+ missing CSS/JS/image/icon/OGP/Twitter image/video poster references from generated HTML, missing or
580
+ size-mismatched manifest outputs, duplicate manifest output paths, stale or
581
+ mismatched `headers.json` entries for artifact response metadata, invalid or
582
+ stale `redirects.json` entries, invalid or missing sitemap references in
583
+ `robots.txt`, stale Netlify `_redirects` or `_headers` deploy artifacts, public
584
+ HTML files that are not listed in the manifest, missing local prefetch targets,
585
+ budget failures, public output file bytes by type, precompressed transfer
586
+ sidecar coverage when explicitly requested, referenced stylesheet asset bytes, public CSS and JavaScript
587
+ asset bytes, image asset bytes, the largest HTML files, the largest generated
588
+ artifacts, the largest public output files, the largest referenced stylesheets,
589
+ the largest public CSS and JavaScript files, and the largest public image files.
590
+ The optional HTML, CSS, JavaScript, public-file, total-public, image, Brotli,
591
+ and Gzip budgets are byte limits for generated output. Use public-file budgets
592
+ to catch a single oversized sitemap, image, or export, total-public budgets to
593
+ catch whole-site payload growth, image budgets to catch image optimization
594
+ regressions, and route family page budgets and artifact family output budgets
595
+ catch accidental output growth inside high-cardinality route groups or export
596
+ families before the whole site budget is exceeded. Brotli/Gzip sidecars remain
597
+ available through `optimize` for hosts that need precompressed files, but the
598
+ default publish gate leaves runtime compression to the serving layer.
599
+ Page, artifact, route family, artifact family, HTML, and dependency fanout
600
+ budgets are also available in `buildSite({ validation })`, so the generated
601
+ `.stoneage/report.json` records publish-gate failures even before the standalone
602
+ `validate` command runs.
603
+ The optional dependency fanout budget limits
604
+ how many generated pages and artifacts a single data dependency can invalidate;
605
+ it is available both in build validation and in the `validate` command.
606
+ Use `--validation-config <file>` to load stable CI validation settings from a
607
+ JSON file. CLI budget flags override values from the file, and
608
+ `--require-precompressed` is available only for deployments that intentionally
609
+ ship precompressed sidecars:
610
+
611
+ ```json
612
+ {
613
+ "budgets": {
614
+ "maxPages": 22172,
615
+ "maxRouteFamilyPages": {
616
+ "categoryIndexes": 1,
617
+ "yearIndexes": 1,
618
+ "members": 250,
619
+ "sessions": 120,
620
+ "conversationIndexes": 1000,
621
+ "conversations": 20000,
622
+ "bills": 800
623
+ },
624
+ "maxHtmlBytes": 1810,
625
+ "maxPublicFileBytes": 738738,
626
+ "maxCssBytes": 376,
627
+ "maxJsBytes": 518,
628
+ "maxTotalPublicBytes": 16780313,
629
+ "maxImageBytes": 17000,
630
+ "maxDependencyFanout": 2
631
+ }
632
+ }
633
+ ```
634
+ Unknown top-level or budget keys are rejected so configuration typos do not
635
+ silently weaken publish checks. JSON validation summaries and report files
636
+ include the effective validation options, config path, and config SHA-256 hash
637
+ when one was used, so CI artifacts can show which publish gate was applied.
638
+ Build scripts can share the same file for build-time validation:
639
+
640
+ ```ts
641
+ import { buildSite, loadValidationConfig } from "@t09tanaka/stoneage";
642
+
643
+ await buildSite({
644
+ outDir: "dist",
645
+ site,
646
+ validation: await loadValidationConfig("validation.json"),
647
+ routes,
648
+ });
649
+ ```
650
+
651
+ `loadValidationConfig()` returns the build-time `requiredMetadata` and `budgets`
652
+ settings, plus the source config path and SHA-256 hash that are copied into the
653
+ generated build validation report. The CLI-only `requirePrecompressed` flag is
654
+ still validated so one shared file can be used by both build scripts and
655
+ `stoneage validate`.
656
+ Use `--write-validation-config <file>` after measuring a representative output
657
+ to write an exact baseline config from the current page count, artifact count,
658
+ largest HTML file, largest public file, largest public CSS and JavaScript files,
659
+ total public bytes, image bytes, and largest
660
+ dependency fanout. The generated file is intentionally strict; edit it when a
661
+ site wants explicit growth headroom.
662
+ Public JSON/CSV export validation covers both `data/` exports and generated
663
+ artifact routes such as `/exports/*.csv` or `/search-index.json`.
664
+
665
+ Use `--json` when CI or another tool should consume the validation summary from
666
+ stdout. Use `--report <path>` to write the same summary to a JSON file.
667
+ `check:package` builds the package, audits the packed file list and public
668
+ subpath imports, installs the tarball into a temporary project, and runs the
669
+ installed `stoneage --help` bin.
670
+
671
+ `audit-sveltekit` statically scans a SvelteKit route tree and reports candidate
672
+ StoneAge page routes, artifact routes, dynamic params, param matchers, endpoint
673
+ content kinds, universal `+page.ts` / `+page.js` and `+layout.ts` /
674
+ `+layout.js` load modules, and migration warnings. Warnings include
675
+ optional/rest params, missing matcher files, sibling dynamic routes, route-group
676
+ page duplicates, page/artifact same-path outputs, and static paths shadowed by
677
+ bare dynamic routes. It is not an adapter and does not execute SvelteKit code.
678
+ Use it to build a route migration checklist before replacing `+page.server.ts`,
679
+ universal load modules, and `+server.ts` files with normalized data producers,
680
+ typed route families, and artifact routes. Add `--migration-plan` to include
681
+ deterministic route family and artifact family names, StoneAge patterns, matcher
682
+ wiring, renderer hints for endpoint artifacts, universal load flags, and
683
+ collision-test candidates in the JSON report. Add
684
+ `--emit-stubs <dir>` to also generate migration scaffolding from that plan; it
685
+ implies `--migration-plan` and writes `data.ts`, `routes.ts`, `artifacts.ts`,
686
+ `param-matchers.ts`, `build.ts`, and `collision-tests.md` under the target
687
+ directory. The stubs are intentionally non-adapter code: they import StoneAge
688
+ helpers, include TODOs for normalized data entries, render bodies, and build
689
+ wiring, and do not import SvelteKit modules. Stub emission is no-overwrite by
690
+ default so existing migration files are not silently replaced; the programmatic
691
+ `emitSvelteKitMigrationStubs(plan, dir, { overwrite: true })` API is available
692
+ when regeneration is intentional.
693
+
694
+ Publishing manifests are opt-in:
695
+
696
+ ```ts
697
+ import { redirectToRoute, redirectsFromRecords } from "@t09tanaka/stoneage";
698
+
699
+ await buildSite({
700
+ outDir: "dist",
701
+ site,
702
+ publishing: {
703
+ robots: {
704
+ rules: [{ userAgent: "*", allow: ["/"], disallow: ["/drafts/"] }],
705
+ },
706
+ redirects: [
707
+ redirectToRoute({
708
+ from: "/old-members/:id/",
709
+ fromParams: { id: "m1" },
710
+ to: "/members/:id/:year/",
711
+ toParams: { id: "m1", year: 2026 },
712
+ status: 301,
713
+ }),
714
+ ...redirectsFromRecords(memberRecords, {
715
+ from: "/members/:id/",
716
+ fromParams: (member) => ({ id: member.slug }),
717
+ to: "/members/:id/:year/",
718
+ toParams: (member) => ({ id: member.slug, year: member.canonicalYear }),
719
+ }),
720
+ ],
721
+ },
722
+ routes,
723
+ });
724
+ ```
725
+
726
+ StoneAge writes `robots.txt` with a default `Sitemap` line and writes
727
+ `redirects.json` and `headers.json` as host-neutral manifests that deployment
728
+ tooling can translate to provider-specific formats. Redirects are not pages and
729
+ are not added to the sitemap; the canonical HTML page should own metadata and
730
+ sitemap membership. `headers.json` is derived from artifact response metadata
731
+ plus managed immutable public assets such as content-hashed CSS, JS, and fonts.
732
+ Run `deploy --provider netlify` after validation to translate those host-neutral
733
+ manifests into Netlify `_redirects` and `_headers` files without adding provider
734
+ syntax to route or artifact definitions. Sites that publish `buildSite` output
735
+ directly to Netlify or Cloudflare Pages can instead set
736
+ `publishing.nativeRedirects: "netlify"` to write `_redirects` during the build;
737
+ when publishing headers are enabled, the same build also writes `_headers` while
738
+ keeping `headers.json` available. Set `publishing.redirectHtml: true` to
739
+ additionally emit a static `<meta http-equiv="refresh">` HTML page at each
740
+ internal redirect's `from` path, so redirects also work on plain static hosts
741
+ that do not interpret `redirects.json` or `_redirects`. A `from` that collides
742
+ with a real page, artifact, or public asset is skipped (the existing file is
743
+ never overwritten), and non-internal sources are left to the manifest. Like
744
+ `redirects.json` itself, these stubs are not swept on incremental builds, so
745
+ clear `dist/` for a clean rebuild after removing redirect rules.
746
+
747
+ `benchmark` measures generated output sizes without precompressing public files;
748
+ runtime Brotli/Gzip compression is expected to be handled by the serving layer.
749
+ The benchmark command accepts `--trailing-slash always` or
750
+ `--trailing-slash never` so the example site can be measured with either URL
751
+ style. `optimize` writes `.br` and `.gz` sidecars for public
752
+ HTML, CSS, JavaScript, XML, JSON, CSV, text, and SVG files while leaving the
753
+ internal `.stoneage` cache untouched. `validate` reports how many compressible
754
+ public files have Brotli and Gzip sidecars and lists missing sidecars in the
755
+ JSON summary when precompressed sidecar validation is explicitly requested. Pass
756
+ `--require-precompressed` only for deployment targets that require missing
757
+ sidecars to become validation issues.
758
+
759
+ ## Documentation
760
+
761
+ - [Getting Started](docs/getting-started.md)
762
+ - [Site Build Guide](docs/site-build.md)
763
+ - [Components](docs/components.md)
764
+ - [Testing Components](docs/testing.md)
765
+ - [Migration Guide](docs/migration.md)
766
+ - [Architecture](docs/architecture.md)
767
+ - [Data Flow](docs/data-flow.md)
768
+ - [Performance](docs/performance.md)