@brandon_m_behring/book-scaffold-astro 4.31.0 → 5.0.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 (79) hide show
  1. package/CLAUDE.md +37 -2
  2. package/MIGRATION-v4-to-v5.md +183 -0
  3. package/README.md +30 -1
  4. package/components/AssessmentTest.astro +26 -5
  5. package/components/BookLink.astro +6 -3
  6. package/components/ChapterNav.astro +1 -1
  7. package/components/Cite.astro +37 -4
  8. package/components/ExerciseSolutions.astro +24 -4
  9. package/components/Flashcards.tsx +19 -11
  10. package/components/NavContent.astro +60 -11
  11. package/components/ObjectiveMap.astro +14 -2
  12. package/components/PartReview.astro +32 -4
  13. package/components/PatternTimeline.astro +6 -2
  14. package/components/Rationale.astro +20 -3
  15. package/components/Sidebar.astro +11 -3
  16. package/components/SourceArchive.astro +33 -3
  17. package/components/Term.astro +18 -1
  18. package/components/Theorem.astro +15 -5
  19. package/components/TipsCard.astro +40 -3
  20. package/components/XRef.astro +14 -2
  21. package/dist/components/ExamRunner.d.ts +1 -1
  22. package/dist/components/Flashcards.d.ts +6 -1
  23. package/dist/components/Flashcards.mjs +14 -11
  24. package/dist/{exam-manifest-X9IrX1G3.d.ts → exam-manifest-DttY7kyZ.d.ts} +1 -1
  25. package/dist/index.d.ts +76 -8
  26. package/dist/index.mjs +517 -43
  27. package/dist/schemas.d.ts +1 -1
  28. package/dist/schemas.mjs +311 -32
  29. package/dist/{types-DgSlAew3.d.ts → types-D1QZgKMO.d.ts} +73 -21
  30. package/layouts/Base.astro +38 -9
  31. package/layouts/Chapter.astro +10 -2
  32. package/package.json +4 -2
  33. package/pages/answers.astro +25 -6
  34. package/pages/book.astro +75 -0
  35. package/pages/chapters/[...slug].astro +39 -7
  36. package/pages/chapters-book.astro +17 -0
  37. package/pages/chapters.astro +87 -9
  38. package/pages/convergence.astro +40 -10
  39. package/pages/corpus-apparatus/answers.astro +15 -0
  40. package/pages/corpus-apparatus/convergence.astro +15 -0
  41. package/pages/corpus-apparatus/exercises.astro +15 -0
  42. package/pages/corpus-apparatus/flashcards.astro +15 -0
  43. package/pages/corpus-apparatus/glossary.astro +15 -0
  44. package/pages/corpus-apparatus/practice-exam.astro +15 -0
  45. package/pages/corpus-apparatus/print.astro +15 -0
  46. package/pages/corpus-apparatus/references.astro +15 -0
  47. package/pages/corpus-apparatus/tips.astro +15 -0
  48. package/pages/exercises.astro +18 -5
  49. package/pages/flashcards.astro +26 -5
  50. package/pages/glossary.astro +20 -3
  51. package/pages/index.astro +54 -1
  52. package/pages/practice-exam.astro +8 -2
  53. package/pages/print.astro +7 -2
  54. package/pages/references.astro +26 -6
  55. package/pages/search.astro +65 -4
  56. package/pages/tips.astro +17 -4
  57. package/recipes/09-validation.md +1 -1
  58. package/recipes/15-defining-styles.md +6 -6
  59. package/recipes/20-anki-export.md +15 -8
  60. package/recipes/21-multi-guide-single-app.md +293 -44
  61. package/recipes/22-responsive-nav-and-multibook-routing.md +7 -1
  62. package/recipes/README.md +2 -2
  63. package/scripts/build-bib.mjs +113 -35
  64. package/scripts/build-exercises.mjs +101 -24
  65. package/scripts/build-labels.mjs +199 -194
  66. package/scripts/build-tips.mjs +95 -23
  67. package/scripts/corpus-tooling.mjs +268 -0
  68. package/scripts/resolve-book-config.mjs +99 -1
  69. package/scripts/validate.mjs +676 -100
  70. package/scripts/walk-mdx.mjs +16 -4
  71. package/src/lib/book-link.ts +72 -0
  72. package/src/lib/chapters.ts +10 -4
  73. package/src/lib/corpus-collateral.ts +9 -0
  74. package/src/lib/corpus.ts +458 -0
  75. package/src/lib/define-style.ts +28 -15
  76. package/src/lib/exam-manifest.ts +4 -1
  77. package/src/lib/patterns.ts +15 -6
  78. package/src/lib/questions.ts +8 -3
  79. package/src/types.ts +603 -0
@@ -115,7 +115,7 @@ For pre-commit: add to `.pre-commit-config.yaml`:
115
115
 
116
116
  ## Preset / chaptersBase resolution (v4.7.0+, #75)
117
117
 
118
- The validator evaluates `astro.config.*` through Vite first. A resolved scaffold integration supplies the composed preset and `numberStyle`, so CLI tooling sees the same Style chain as the Astro build. The evaluated Astro `base` is read from that same config regardless of whether the scaffold integration is present; omission defaults to `/`. Without such an integration, the legacy preset chain remains: `--preset` → process `BOOK_PRESET`/`BOOK_PROFILE` → root `.env` → the literal value in `defineBookSchemas` → a warned `minimal` v4 compatibility fallback. Every selected value is checked against the five-preset enum; config-evaluation failures stop validation instead of silently using defaults.
118
+ The validator evaluates `astro.config.*` through Vite first. A resolved scaffold integration supplies the composed preset and `numberStyle`, so CLI tooling sees the same Style chain as the Astro build. The evaluated Astro `base` is read from that same config regardless of whether the scaffold integration is present; omission defaults to `/`. Without such an integration, the preset chain remains: `--preset` → process `BOOK_PRESET`/`BOOK_PROFILE` → root `.env` → the literal value in `defineBookSchemas`. Every selected value is checked against the five-preset enum; an absent preset, an invalid value, or config-evaluation failure stops validation instead of silently selecting `minimal`.
119
119
 
120
120
  `chaptersBase` resolution still consults `BOOK_CHAPTERS_DIR`, content configuration, then `src/content/chapters`. The v4.5+ canonical form is:
121
121
 
@@ -46,7 +46,6 @@ defineStyle({
46
46
  extraIntegrations?: readonly AstroIntegration[];
47
47
  mdxComponentsModule?: string;
48
48
  markdown?: AstroUserConfig['markdown'];
49
- deploy?: 'pages' | 'workers'; // RESERVED (#50, #180) — no runtime effect; wrangler shape is set at scaffold time by profile
50
49
  extra?: Record<string, unknown>; // scoped consumer-side metadata
51
50
  });
52
51
  ```
@@ -122,7 +121,7 @@ Different fields have different merge semantics. Documented:
122
121
 
123
122
  | Field | Strategy |
124
123
  |---|---|
125
- | `name`, `preset`, `site`, `deploy`, `mdxComponentsModule` | Shallow override (last defined wins) |
124
+ | `name`, `preset`, `site`, `mdxComponentsModule` | Shallow override (last defined wins) |
126
125
  | `releaseStatus` | Shallow override (last defined object replaces the whole earlier object); `false` suppresses an inherited banner |
127
126
  | `routes` | Per-route spread (each route key independently overridable) |
128
127
  | `routes.frontmatter` | Per-route spread; later value (boolean OR object) wholly replaces earlier |
@@ -158,10 +157,10 @@ defineBookConfig({ styles: [researchPortfolioStyle], ... });
158
157
  defineBookConfig({ styles: [BUILTIN_STYLES['research-portfolio']], ... });
159
158
  ```
160
159
 
161
- Each built-in style has a `name` matching its preset and a `preset` field. Its
162
- historical `deploy` value is reserved metadata only; it does not alter a
163
- generated or existing deployment. `create-book --preset` selects the initial
164
- `wrangler.toml`, after which the consumer owns that file (#180).
160
+ Each built-in style has a `name` matching its preset and a `preset` field. In
161
+ v5, a valid preset must resolve from a Style, corpus manifest, environment, or
162
+ `.env`; there is no implicit `minimal` fallback. `create-book --preset` still
163
+ selects the initial `wrangler.toml`, after which the consumer owns that file.
165
164
 
166
165
  ---
167
166
 
@@ -214,6 +213,7 @@ The v4.x release line is explicitly the iteration window for this API. Use it.
214
213
  ## See also
215
214
 
216
215
  - `MIGRATION-v3-to-v4.md` — step-by-step migration from v3 `preset:` shorthand
216
+ - `MIGRATION-v4-to-v5.md` — explicit preset and removed `deploy` migration
217
217
  - `PACKAGE_DESIGN.md §4` — `defineBookConfig` API reference
218
218
  - `PACKAGE_DESIGN.md §4a` — `defineStyle` API reference
219
219
  - `recipes/12-where-to-file-issues.md` — the broader consumer-driven evolution loop this fits into
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Profile**: any (most useful for `course-notes` and `research-portfolio`).
4
4
 
5
- **TL;DR**: The scaffold does **not** ship an `<AnkiCard>` component or an `extract-cards` CLI (see [PACKAGE_DESIGN.md §15a](../../PACKAGE_DESIGN.md#15a-deferred-scope-post-v4x) for why). This recipe shows how a consumer can roll their own — a small `<AnkiCard>` component plus a `scripts/extract-anki.mjs` extractor that walks the chapters collection and emits an Anki deck. ~120 lines of consumer-side code; no scaffold changes required.
5
+ **TL;DR**: The scaffold does **not** ship an `<AnkiCard>` component or an `extract-cards` CLI (see [PACKAGE_DESIGN.md §15b](../../PACKAGE_DESIGN.md#15b-deferred-scope) for why). This recipe shows how a consumer can roll their own — a small `<AnkiCard>` component plus a `scripts/extract-anki.mjs` extractor that walks the chapters collection and emits an Anki deck. ~120 lines of consumer-side code; no scaffold changes required.
6
6
 
7
7
  If your book is the **third** independent consumer to want this, please open an issue at [book-scaffold-astro](https://github.com/brandon-behring/book-scaffold-astro/issues) — that's the signal we need to consider promoting this to scaffold-level surface.
8
8
 
@@ -141,34 +141,41 @@ The scaffold deliberately does **not** opine on the choice — the JSON is the c
141
141
 
142
142
  ## Step 4 — Per-book grouping (if needed)
143
143
 
144
- The example above emits one deck per book. If you have a multi-book corpus (per the DLAI Study Notes pattern), gate emission on a `book` discriminator in chapter frontmatter:
144
+ The extractor above emits one ungrouped `cards.json` file. In first-class
145
+ corpus mode, derive the book from the namespaced chapter path/id rather than
146
+ repeating it in frontmatter:
145
147
 
146
148
  ```js
147
149
  const byBook = new Map();
148
150
  // ... inside the loop:
149
- const fm = parseFrontmatter(src); // bring your own YAML parser
150
- const book = fm.book ?? 'main';
151
+ const [book] = slug.split('/'); // <book>/<local-id>
151
152
  if (!byBook.has(book)) byBook.set(book, []);
152
153
  byBook.get(book).push({ ... });
153
154
  ```
154
155
 
155
- Then write one file per book key. Multi-book corpus routing is itself out of scope at v4.x ([deferred, see §15a](../../PACKAGE_DESIGN.md#15a-deferred-scope-post-v4x); tracked on #80) — if you need it, the same consumer-side pattern applies.
156
+ Then write one file per registered book key. Recipe 21 documents the corpus
157
+ manifest and namespaced-id contract; Anki extraction itself remains
158
+ consumer-owned.
156
159
 
157
160
  ## Common gotchas
158
161
 
159
162
  - **Stable GUIDs**: if you omit `id`, the script falls back to `${slug}-${index}`. Adding/removing cards above an existing card will shift indices and break review history. **Always set explicit `id` props** for cards you want to survive content edits.
160
163
  - **Markdown in slots**: Anki accepts HTML; MDX renders the slot content to HTML before this extractor sees it, so most formatting carries through. KaTeX math is a special case — the extractor sees raw `$...$` LaTeX; either pre-render with KaTeX server-side before extraction, or use Anki's MathJax integration.
161
- - **Pagefind**: `<AnkiCard>` instances are indexed by Pagefront like any prose. If you don't want flashcard fronts in search results, wrap in `<aside data-pagefind-ignore>`.
164
+ - **Pagefind**: `<AnkiCard>` instances are indexed by Pagefind like any prose. If you don't want flashcard fronts in search results, wrap in `<aside data-pagefind-ignore>`.
162
165
  - **Visual regression**: the component adds a styled block to every chapter that has cards. If you maintain visual baselines, regenerate them after first adoption.
163
166
 
164
167
  ## Why this isn't in the scaffold
165
168
 
166
- Per [PACKAGE_DESIGN.md §15a](../../PACKAGE_DESIGN.md#15a-deferred-scope-post-v4x): single consumer signal so far (DLAI), runtime dep concerns (`.apkg` is a SQLite-backed zip — needs an external builder), and the design space for per-book grouping is entangled with multi-book corpus routing (also deferred). When 2-3 consumers independently want this, it gets promoted.
169
+ Per [PACKAGE_DESIGN.md §15b](../../PACKAGE_DESIGN.md#15b-deferred-scope):
170
+ consumer signal remains limited and `.apkg` generation adds a non-trivial
171
+ SQLite-backed archive dependency. Corpus grouping is now available, but those
172
+ product/dependency questions still keep export outside the scaffold. When 2–3
173
+ consumers independently want it, it can be reconsidered.
167
174
 
168
175
  ## Canonical files
169
176
 
170
177
  - This recipe (consumer pattern)
171
- - [PACKAGE_DESIGN.md §15a](../../PACKAGE_DESIGN.md#15a-deferred-scope-post-v4x) — deferral rationale
178
+ - [PACKAGE_DESIGN.md §15b](../../PACKAGE_DESIGN.md#15b-deferred-scope) — deferral rationale
172
179
 
173
180
  ## Reference implementation
174
181
 
@@ -1,73 +1,322 @@
1
- # Recipe 21 — Multiple guides in one app (v4.20.0, #132)
1
+ # Recipe 21 — One app, multiple books with `defineBookCorpus` (v5, #80)
2
2
 
3
- **Profile**: any (proven on research-portfolio).
3
+ **Profile**: any, provided every book uses the same preset and Style chain.
4
4
 
5
- **TL;DR**: To host more than one guide/book in a single Astro app, keep ONE `chapters` collection rooted at `src/content/` and namespace every entry id by its guide folder via a `generateId` in the glob loader. The scaffold's existing rest-param route (`/chapters/[...slug]/`) then serves `/chapters/<guide>/<slug>/` for every guide with **no scaffold changes**. This is the blessed interim pattern until multibook (#80) ships first-class support.
5
+ **TL;DR**: Define one ordered, branded corpus manifest and pass the same object
6
+ to `defineBookConfig` and `defineBookSchemas`. Put chapters beneath
7
+ `src/content/<book>/`. The scaffold derives book identity from that path,
8
+ injects corpus and per-book routes, scopes navigation/search/generated data,
9
+ and preserves the established `/chapters/<book>/<slug>/` URL.
6
10
 
7
- ## The pattern
11
+ ## Define the corpus once
8
12
 
9
- Author each guide under its own folder:
13
+ Keep the manifest in a Node-loadable module shared by both configuration
14
+ entrypoints:
10
15
 
16
+ ```ts
17
+ // src/book-corpus.ts
18
+ import { defineBookCorpus } from '@brandon_m_behring/book-scaffold-astro';
19
+
20
+ export const corpus = defineBookCorpus({
21
+ preset: 'research-portfolio',
22
+ books: [
23
+ {
24
+ id: 'evaluation',
25
+ title: 'Evaluation Engineering',
26
+ description: 'How to evaluate production LLM systems.',
27
+ apparatus: ['references', 'glossary'],
28
+ },
29
+ {
30
+ id: 'llm-app-engineering',
31
+ title: 'LLM Application Engineering',
32
+ description: 'Patterns for dependable LLM applications.',
33
+ // apparatus omitted: inherit the application-enabled apparatus set
34
+ },
35
+ ],
36
+ });
37
+ ```
38
+
39
+ ```js
40
+ // astro.config.mjs
41
+ import {
42
+ defineBookConfig,
43
+ researchPortfolioStyle,
44
+ } from '@brandon_m_behring/book-scaffold-astro';
45
+ import { corpus } from './src/book-corpus.js';
46
+
47
+ export default await defineBookConfig({
48
+ site: 'https://guides.example.com',
49
+ title: 'Engineering Guides',
50
+ styles: [researchPortfolioStyle],
51
+ routes: {
52
+ glossary: true,
53
+ practiceExam: true,
54
+ },
55
+ corpus,
56
+ });
57
+ ```
58
+
59
+ ```ts
60
+ // src/content.config.ts
61
+ import { defineBookSchemas } from '@brandon_m_behring/book-scaffold-astro/schemas';
62
+ import { corpus } from './book-corpus.js';
63
+
64
+ export const { collections } = defineBookSchemas({ corpus });
11
65
  ```
66
+
67
+ The manifest is an explicit preset source. If a composed Style also supplies a
68
+ preset, it must match `corpus.preset`. Presets, Styles, Markdown plugins,
69
+ integrations, `site`, Astro `base`, and `examDomains` are application-wide in
70
+ v5; per-book variants are deliberately unsupported.
71
+
72
+ `defineBookCorpus` validates and freezes the complete registry immediately:
73
+
74
+ - `books` must be non-empty; its order is display/navigation order;
75
+ - each `id` is unique and matches `[a-z0-9]+(?:-[a-z0-9]+)*`;
76
+ - `assets`, `chapters`, `search`, `questions`, `glossary`, `frontmatter`,
77
+ `_astro`, `_og`, and `pagefind` are reserved; the three content names own
78
+ scaffold collection roots and cannot also identify chapter directories;
79
+ - every title is non-blank; and
80
+ - each `apparatus` list is duplicate-free and uses only known route names.
81
+
82
+ ## Author content by path
83
+
84
+ Corpus chapters default to a shared `./src/content` base. Only registered book
85
+ folders become chapters, so shared directories are not accidentally counted:
86
+
87
+ ```text
12
88
  src/content/
13
89
  ├── evaluation/
14
90
  │ ├── 01-why-evaluation.mdx
15
- │ └── ...
91
+ │ └── design/02-metrics.mdx
16
92
  ├── llm-app-engineering/
17
- ├── 01-why-llm-app-engineering.mdx
18
- │ └── ...
19
- └── frontmatter/ ← shared, excluded below
93
+ └── 01-architecture.mdx
94
+ ├── questions/
95
+ │ ├── evaluation/q-measurement.mdx
96
+ │ └── llm-app-engineering/q-retries.mdx
97
+ ├── glossary/
98
+ │ ├── evaluation/calibration.mdx
99
+ │ └── llm-app-engineering/idempotency.mdx
100
+ └── frontmatter/ # shared; never a chapter
101
+ ```
102
+
103
+ Convergence collateral is book-owned too. Keep each registry and its tool
104
+ timelines under the matching manifest id; two books may reuse pattern ids and
105
+ tool filenames without sharing a dashboard:
106
+
107
+ ```text
108
+ changelog/
109
+ ├── evaluation/
110
+ │ ├── patterns.yaml
111
+ │ └── tools/*.yaml
112
+ └── llm-app-engineering/
113
+ ├── patterns.yaml
114
+ └── tools/*.yaml
20
115
  ```
21
116
 
22
- Point the collection at the shared base and namespace ids by guide:
117
+ The legacy root paths `changelog/patterns.yaml` and `changelog/tools/*.yaml`
118
+ remain the single-book convention. Corpus routes do not read those global
119
+ collections.
120
+
121
+ The loader emits `<book>/<local-id>` collection ids. The local id is the
122
+ relative path below the book folder without `.md`/`.mdx`, unless frontmatter
123
+ sets a string `slug`. For example:
124
+
125
+ | Source | Entry id | Public chapter URL |
126
+ |---|---|---|
127
+ | `evaluation/01-intro.mdx` | `evaluation/01-intro` | `/chapters/evaluation/01-intro/` |
128
+ | `evaluation/design/02-metrics.mdx` | `evaluation/design/02-metrics` | `/chapters/evaluation/design/02-metrics/` |
129
+ | `evaluation/99-draft.mdx` with `slug: conclusion` | `evaluation/conclusion` | `/chapters/evaluation/conclusion/` |
130
+
131
+ Do not add a required `book:` frontmatter field. The registered first path
132
+ segment is authoritative. A legacy field may remain while migrating only when
133
+ it agrees with the path; disagreement fails validation. The same local slug,
134
+ label, question id, or glossary id may appear in different books, but duplicates
135
+ inside one book still fail.
136
+
137
+ Set `chaptersBase` only when the common content base lives elsewhere:
23
138
 
24
139
  ```ts
25
- // src/content.config.ts
26
- import { defineCollection } from 'astro:content';
27
- import { glob } from 'astro/loaders';
28
- import { researchPortfolioChapterSchema } from '@brandon_m_behring/book-scaffold-astro/schemas';
29
-
30
- const chapters = defineCollection({
31
- loader: glob({
32
- pattern: ['**/*.{md,mdx}', '!**/_*', '!frontmatter/**'],
33
- base: './src/content',
34
- generateId: ({ entry, data }) => {
35
- const guide = entry.split('/')[0];
36
- const slug =
37
- (data && data.slug) || entry.replace(/\.[^.]+$/, '').split('/').pop();
38
- return `${guide}/${slug}`; // → /chapters/<guide>/<slug>/
39
- },
40
- }),
41
- schema: researchPortfolioChapterSchema,
42
- });
140
+ defineBookSchemas({ corpus, chaptersBase: './guides/content' });
141
+ ```
43
142
 
44
- export const collections = { chapters };
143
+ The registered book directories must remain direct children of that base.
144
+
145
+ ## Canonical routes
146
+
147
+ Every generated href includes Astro's normalized deployment `base`. These
148
+ patterns are shown without that prefix:
149
+
150
+ | Surface | Route |
151
+ |---|---|
152
+ | Corpus landing | `/` |
153
+ | Corpus chapter index | `/chapters/` |
154
+ | Book landing | `/<book>/` |
155
+ | Book chapter index | `/chapters/<book>/` |
156
+ | Chapter | `/chapters/<book>/<slug>/` |
157
+ | Corpus search | `/search/` and `/search/?book=<book>` |
158
+ | Book apparatus | `/<book>/<route>/` |
159
+
160
+ Draft chapters do not generate paths. Book and apparatus params come only from
161
+ the manifest; arbitrary URL segments never become content selectors.
162
+
163
+ Corpus mode owns `chapterRoute`, `bookField`, `apparatusRoute`, and
164
+ `apparatusRoutes`, using `/chapters/:id/` and `/:book/:route/`. Do not set those
165
+ legacy custom-routing fields in a corpus configuration. They remain available
166
+ to single-book and consumer-owned v4-style routes. If a consumer filesystem
167
+ page overrides an injected corpus page, it must preserve the same params,
168
+ canonical URL, Pagefind metadata, and `base` handling.
169
+
170
+ ## Apparatus per book
171
+
172
+ The closed public apparatus set is:
173
+
174
+ ```text
175
+ references, print, convergence, tips, exercises, practice-exam,
176
+ glossary, flashcards, answers
45
177
  ```
46
178
 
47
- The scaffold's auto-injected `pages/chapters/[...slug].astro` routes on `params: { slug: entry.id }` — a namespaced id rides through unchanged, so both guides render with zero route overrides.
179
+ Application route defaults come from the preset and top-level `routes`
180
+ overrides. Manifest values use public URL slugs (`practice-exam`), while the
181
+ top-level route toggle remains camel-cased (`practiceExam`). For each book:
48
182
 
49
- ## Gotcha 1 the flat-slug footgun
183
+ - omit `apparatus` to inherit the application-enabled subset;
184
+ - set `apparatus: []` to expose none; or
185
+ - list an explicit subset to expose only those routes.
50
186
 
51
- **Without** the `generateId`, Astro's glob loader keys entry ids off the frontmatter `slug` (or filename) and routes FLAT at `/chapters/<slug>/`. That silently requires slugs to be **globally unique across all guides** — two guides each having an `introduction` chapter is a route collision, surfaced as a confusing router warning rather than a clear error. The `generateId` namespacing makes slugs only need uniqueness *within* a guide, which is what authors expect.
187
+ Naming a route that the application has disabled is a configuration error, not
188
+ a dead link. Only books exposing a route receive its static path, and every
189
+ apparatus renderer selects that book's questions, glossary entries, labels,
190
+ references, tips, exercises, or convergence registry. Flashcard progress is
191
+ persisted under a deployment-base and book-specific key, so repeated local
192
+ term ids do not share or erase study state. `examDomains` remains one
193
+ application-wide taxonomy in v5 even though question ids and pages are
194
+ book-scoped.
52
195
 
53
- ## Gotcha 2 — `validate` counts everything under the base
196
+ ## Search and links
54
197
 
55
- With `base: './src/content'`, `book-scaffold validate` walks the whole base — shared folders like `frontmatter/` are counted as "chapters" (e.g. `16 chapter(s)` for 13 + 2 real chapters + an authors page). The checks still run correctly per file; only the count is inflated. Excluding non-guide folders from the *loader* pattern does not affect the validator's walk. Guide-aware validation is part of the first-class multibook design (#80).
198
+ One build creates one Pagefind index. Book pages are tagged with the current
199
+ `book:<id>` filter; corpus landing content uses `surface:corpus`. `/search/`
200
+ searches everything by default, accepts only registered `?book=<id>` values,
201
+ and search opened from book chrome starts scoped to that book. Users can switch
202
+ back to all books. Pagefind result URLs already include Astro `base`; do not
203
+ rewrite them or build separate per-book indexes.
56
204
 
57
- ## Gotcha 3 the `/chapters/` index mixes guides
205
+ Use `<BookLink>` for an explicit cross-book link. A manifest book resolves
206
+ locally and an external `siblingBooks` key keeps its configured origin:
58
207
 
59
- The auto-injected `/chapters/` index lists every entry in the collection — i.e. all guides interleaved. If you want per-guide landing pages, add consumer-owned pages (e.g. `src/pages/evaluation/index.astro`) that `getCollection('chapters', (e) => e.id.startsWith('evaluation/'))`. A grouped-by-guide index is also on the #80 wishlist.
208
+ ```mdx
209
+ <BookLink book="evaluation" to="chapters/metrics#calibration">
210
+ calibration metrics
211
+ </BookLink>
212
+ ```
60
213
 
61
- ## When to expect first-class support
214
+ The local result is `/chapters/evaluation/metrics/#calibration` (plus Astro
215
+ `base`). Other relative targets resolve below `/<book>/`. Empty, absolute,
216
+ query-only, fragment-only, or traversal targets fail loudly. A key cannot be
217
+ owned by both `corpus.books` and `siblingBooks`.
62
218
 
63
- This recipe is the interim, zero-scaffold-change path. Issue [#80 (multibook)](https://github.com/brandon-behring/book-scaffold-astro/issues/80) tracks a `books`/`guides` registry that would emit per-guide indexes, a guides landing, and guide-scoped `validate` (the original #15 is closed; #80 is the live v5.x tracker). It was deferred pending 2–3 independent consumers; `guides-ai-engineering` (#132) is the second — file your use case on #80 to add weight.
219
+ ## Generated data and `--book`
64
220
 
65
- ## Canonical files
221
+ Corpus outputs keep the existing `src/data/*.json` filenames but use a strict,
222
+ versioned envelope:
223
+
224
+ ```json
225
+ {
226
+ "schemaVersion": 1,
227
+ "books": {
228
+ "evaluation": {},
229
+ "llm-app-engineering": {}
230
+ }
231
+ }
232
+ ```
233
+
234
+ This applies to `labels.json`, `references.json`, `sources.json`, `tips.json`,
235
+ and `exercises.json`. Payloads inside each book retain their established shape
236
+ and deterministic ordering. Components select the current book before looking
237
+ up a local key; there is no unscoped corpus alias.
238
+
239
+ These content-derived commands accept `--book <id>` in corpus mode:
240
+
241
+ ```bash
242
+ npx book-scaffold build-labels --book evaluation
243
+ npx book-scaffold build-bib --book evaluation
244
+ npx book-scaffold build-tips --book evaluation
245
+ npx book-scaffold build-exercises --book evaluation
246
+ npx book-scaffold validate --book evaluation
247
+ ```
66
248
 
67
- - `package/pages/chapters/[...slug].astro` the rest-param route that makes namespaced ids "just work"
68
- - `package/scripts/walk-mdx.mjs` `readChaptersBase` (how validate finds the base)
69
- - `package/recipes/12-where-to-file-issues.md` multi-book corpus routing is a known scaffold-issue category
249
+ Omit `--book` to process all books in manifest order. A selected producer run
250
+ updates that key in an existing valid envelope and preserves the other
251
+ registered keys; it rejects an existing malformed or manifest-mismatched
252
+ envelope. A full run rewrites all keys. Unknown ids, repeated/missing selectors,
253
+ and `--book` in a single-book project fail loudly. Figure and notebook
254
+ conversion remain application-wide and do not take a book selector.
255
+
256
+ Human diagnostics begin with `[book:<id>]`; explicitly named corpus totals use
257
+ `[book:corpus]`. Identical local ids in two books are checked independently.
258
+
259
+ ### One bibliography and source manifest
260
+
261
+ `bibliography.bib` (or `BOOK_BIB_PATH`) and `sources/manifest.yaml` remain
262
+ root-level, corpus-wide authoring inputs in v5. `build-bib` parses each once and
263
+ stores the result beneath every selected book key in `references.json` and
264
+ `sources.json`. Renderers still select a book namespace before resolving a
265
+ citation, so artifact access never becomes implicitly global.
266
+
267
+ Per-book bibliography/source input files are not part of the v5 contract. A
268
+ duplicate BibTeX key is therefore still an application-level authoring error.
269
+
270
+ ## Migrate the v4 Recipe 21 workaround
271
+
272
+ The v4 recipe used a hand-written collection and `generateId`. Migrate it in
273
+ six steps:
274
+
275
+ 1. Extract ordered book metadata into `defineBookCorpus`.
276
+ 2. Pass the same manifest to `defineBookConfig` and `defineBookSchemas`.
277
+ 3. Remove the hand-written chapter collection and custom `generateId`.
278
+ 4. Keep book folders under `src/content/`, or set one explicit shared
279
+ `chaptersBase`.
280
+ 5. Compare consumer-owned per-book index pages with the injected pages, then
281
+ remove duplicates.
282
+ 6. Run `build-labels`, `build-bib`, optional `build-tips`/`build-exercises`,
283
+ and `validate`; update scripts that read generated JSON to select from the
284
+ `books` envelope.
285
+
286
+ Existing `/chapters/<book>/<slug>/` URLs do not change, so this migration needs
287
+ no redirect layer. Remove legacy `book:` frontmatter when convenient; until
288
+ then it must match the containing registered folder.
289
+
290
+ ## Single-book compatibility
291
+
292
+ If a one-book application does not need corpus routes, leave corpus mode off.
293
+ Its `src/content/chapters/` loader, `/chapters/<slug>/` and `/chapters/` routes,
294
+ flat apparatus routes, and generated JSON shapes remain unchanged. `--book` is
295
+ intentionally rejected. The separate v5 removals—an explicit preset and
296
+ deletion of inert `deploy` configuration—are covered by
297
+ [`MIGRATION-v4-to-v5.md`](../MIGRATION-v4-to-v5.md).
298
+
299
+ ## Verify
300
+
301
+ ```bash
302
+ npx book-scaffold build-labels
303
+ npx book-scaffold build-bib
304
+ npx book-scaffold validate
305
+ npm run build
306
+ ```
307
+
308
+ Check `/`, `/chapters/`, both per-book landings and indexes, a chapter from each
309
+ book, every enabled apparatus route, `/search/`, and `/search/?book=<id>`. Repeat
310
+ under a non-root Astro `base` when the deployment uses one.
311
+
312
+ ## Canonical files
70
313
 
71
- ## Reference implementation
314
+ - `src/lib/corpus.ts` — manifest validation, identity, selectors, and artifact reader
315
+ - `src/schemas-entry.ts` — manifest-scoped loaders and `<book>/<local-id>` ids
316
+ - `src/integration.ts` + `pages/{book,chapters-book}.astro` — injected route ownership
317
+ - `scripts/corpus-tooling.mjs` — `--book` selection and envelope validation
318
+ - `MIGRATION-v4-to-v5.md` — release-wide v4 migration checklist
72
319
 
73
- `guides-ai-engineering` two guides (Evaluation + LLM Application Engineering) on `@brandon_m_behring/book-scaffold-astro@4.14.2`; both `dist/chapters/evaluation/why-evaluation/index.html` and `dist/chapters/llm-app-engineering/why-llm-app-engineering/index.html` build from the scaffold's injected route.
320
+ The original `guides-ai-engineering` two-guide layout is the compatibility
321
+ fixture for this migration: its established chapter URLs are the reason the v5
322
+ route remains `/chapters/<book>/<slug>/`.
@@ -1,4 +1,10 @@
1
- # Recipe 22 — Responsive navigation & multi-book routing (v4.26.0, #80)
1
+ # Recipe 22 — Responsive navigation & custom routing (v4.26.0, #80)
2
+
3
+ > **v5 corpus applications:** use
4
+ > [Recipe 21](./21-multi-guide-single-app.md) and `defineBookCorpus`. Corpus
5
+ > mode owns `chapterRoute`, `bookField`, `apparatusRoute`, and
6
+ > `apparatusRoutes`; the custom fields below remain for single-book applications
7
+ > and consumer-owned v4-style routes.
2
8
 
3
9
  The scaffold's navigation (left `Sidebar`, prev/next `ChapterNav`) is **book-aware**
4
10
  and **responsive**: it serves a single-book site unchanged, a multi-book consumer
package/recipes/README.md CHANGED
@@ -26,8 +26,8 @@ Terse pointers into canonical code for the most common book-authoring workflows.
26
26
  | 18 | [Chapter route ownership](18-chapter-route-ownership.md) | any | When to override the auto-injected `/chapters/[...slug]/` route |
27
27
  | 19 | [Prevalidate hook](19-prevalidate-hook.md) | any | Wire `prevalidate` to run `build:bib` + `build:labels` before `validate` |
28
28
  | 20 | [Anki deck export (consumer-side)](20-anki-export.md) | any (esp. course-notes, research-portfolio) | Roll-your-own `<AnkiCard>` + extractor; scaffold deliberately doesn't ship this |
29
- | 21 | [Multiple guides in one app](21-multi-guide-single-app.md) | any | `generateId` namespacing over one collection; flat-slug footgun; interim until multibook (#80) |
30
- | 22 | [Responsive navigation and multi-book routing](22-responsive-nav-and-multibook-routing.md) | any | Mobile/desktop nav, base-aware route ownership, and the interim multi-book routing contract |
29
+ | 21 | [Multiple books in one app](21-multi-guide-single-app.md) | any | First-class `defineBookCorpus`: routes, scoped data/search, `--book`, and v4 migration |
30
+ | 22 | [Responsive navigation and custom routing](22-responsive-nav-and-multibook-routing.md) | any | Mobile/desktop nav and the v4-compatible consumer-owned route-token API |
31
31
  | 23 | [Interactive demo substrate](23-interactive-demo-substrate.md) | any | Opt-in Preact shell, slider, stat cards, theme colors, a11y, and reduced motion |
32
32
  | 24 | [Figure authoring standard](24-figure-authoring-standard.md) | any | Warm–Tol semantics, Okabe–Ito series, dual-theme exports, contrast, and accessible descriptions |
33
33