@brandon_m_behring/book-scaffold-astro 4.30.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.
- package/CLAUDE.md +40 -3
- package/MIGRATION-v4-to-v5.md +183 -0
- package/README.md +30 -1
- package/bin/book-scaffold.mjs +1 -1
- package/components/AssessmentTest.astro +26 -5
- package/components/BookLink.astro +6 -3
- package/components/ChapterNav.astro +1 -1
- package/components/Cite.astro +37 -4
- package/components/ExerciseSolutions.astro +24 -4
- package/components/Figure.astro +7 -6
- package/components/Flashcards.tsx +19 -11
- package/components/NavContent.astro +60 -11
- package/components/ObjectiveMap.astro +14 -2
- package/components/PartReview.astro +32 -4
- package/components/PatternTimeline.astro +6 -2
- package/components/Rationale.astro +20 -3
- package/components/Sidebar.astro +11 -3
- package/components/SourceArchive.astro +33 -3
- package/components/Term.astro +18 -1
- package/components/Theorem.astro +15 -5
- package/components/TipsCard.astro +40 -3
- package/components/XRef.astro +14 -2
- package/dist/components/ExamRunner.d.ts +1 -1
- package/dist/components/Flashcards.d.ts +6 -1
- package/dist/components/Flashcards.mjs +14 -11
- package/dist/{exam-manifest-X9IrX1G3.d.ts → exam-manifest-DttY7kyZ.d.ts} +1 -1
- package/dist/index.d.ts +76 -8
- package/dist/index.mjs +517 -43
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.mjs +311 -32
- package/dist/{types-DgSlAew3.d.ts → types-D1QZgKMO.d.ts} +73 -21
- package/layouts/Base.astro +38 -9
- package/layouts/Chapter.astro +10 -2
- package/package.json +6 -2
- package/pages/answers.astro +25 -6
- package/pages/book.astro +75 -0
- package/pages/chapters/[...slug].astro +39 -7
- package/pages/chapters-book.astro +17 -0
- package/pages/chapters.astro +87 -9
- package/pages/convergence.astro +40 -10
- package/pages/corpus-apparatus/answers.astro +15 -0
- package/pages/corpus-apparatus/convergence.astro +15 -0
- package/pages/corpus-apparatus/exercises.astro +15 -0
- package/pages/corpus-apparatus/flashcards.astro +15 -0
- package/pages/corpus-apparatus/glossary.astro +15 -0
- package/pages/corpus-apparatus/practice-exam.astro +15 -0
- package/pages/corpus-apparatus/print.astro +15 -0
- package/pages/corpus-apparatus/references.astro +15 -0
- package/pages/corpus-apparatus/tips.astro +15 -0
- package/pages/exercises.astro +18 -5
- package/pages/flashcards.astro +26 -5
- package/pages/glossary.astro +20 -3
- package/pages/index.astro +54 -1
- package/pages/practice-exam.astro +8 -2
- package/pages/print.astro +7 -2
- package/pages/references.astro +26 -6
- package/pages/search.astro +65 -4
- package/pages/tips.astro +17 -4
- package/recipes/03-asset-pipelines.md +12 -4
- package/recipes/09-validation.md +1 -1
- package/recipes/15-defining-styles.md +6 -6
- package/recipes/16-tikz-figures.md +13 -2
- package/recipes/20-anki-export.md +15 -8
- package/recipes/21-multi-guide-single-app.md +293 -44
- package/recipes/22-responsive-nav-and-multibook-routing.md +7 -1
- package/recipes/24-figure-authoring-standard.md +241 -0
- package/recipes/README.md +3 -2
- package/scripts/build-bib.mjs +113 -35
- package/scripts/build-exercises.mjs +101 -24
- package/scripts/build-figures.mjs +13 -10
- package/scripts/build-labels.mjs +199 -194
- package/scripts/build-tips.mjs +95 -23
- package/scripts/corpus-tooling.mjs +268 -0
- package/scripts/render-notebooks.mjs +2 -1
- package/scripts/resolve-book-config.mjs +99 -1
- package/scripts/sync-figure-tokens.mjs +44 -0
- package/scripts/validate.mjs +676 -100
- package/scripts/walk-mdx.mjs +16 -4
- package/src/lib/book-link.ts +72 -0
- package/src/lib/chapters.ts +10 -4
- package/src/lib/corpus-collateral.ts +9 -0
- package/src/lib/corpus.ts +458 -0
- package/src/lib/define-style.ts +28 -15
- package/src/lib/exam-manifest.ts +4 -1
- package/src/lib/figure-palette.mjs +162 -0
- package/src/lib/figure.mjs +113 -37
- package/src/lib/patterns.ts +15 -6
- package/src/lib/questions.ts +8 -3
- package/src/types.ts +603 -0
- package/styles/tokens.css +73 -9
package/pages/tips.astro
CHANGED
|
@@ -11,18 +11,31 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import Base from '../layouts/Base.astro';
|
|
13
13
|
import { normalizeBase } from '../src/lib/nav-href';
|
|
14
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
15
|
+
import { selectBookArtifact } from '../src/lib/corpus';
|
|
16
|
+
|
|
17
|
+
interface Props { bookId?: string; }
|
|
18
|
+
const { bookId } = Astro.props;
|
|
14
19
|
|
|
15
20
|
// v4.4.0 fix: use Vite's import.meta.glob with a project-root-relative
|
|
16
21
|
// path. The previous `../../../src/data/tips.json` resolution failed when
|
|
17
22
|
// the auto-injected route was processed from node_modules (or any context
|
|
18
23
|
// other than a workspace symlink). `/src/data/...` is consumer-project-rooted
|
|
19
24
|
// in Vite's resolver and works across all consumer build contexts.
|
|
20
|
-
|
|
25
|
+
type TipIndexEntry = { n: number; title: string; chapter: string; preview: string };
|
|
26
|
+
const tipsModules = import.meta.glob<{ default: unknown }>(
|
|
21
27
|
'/src/data/tips.json',
|
|
22
28
|
{ eager: true },
|
|
23
29
|
);
|
|
24
30
|
const tipsEntry = tipsModules['/src/data/tips.json'];
|
|
25
|
-
const tips = tipsEntry
|
|
31
|
+
const tips = tipsEntry
|
|
32
|
+
? selectBookArtifact<TipIndexEntry[]>(
|
|
33
|
+
tipsEntry.default,
|
|
34
|
+
bookConfig.corpus,
|
|
35
|
+
bookId,
|
|
36
|
+
'src/data/tips.json',
|
|
37
|
+
)
|
|
38
|
+
: [];
|
|
26
39
|
const loadError = tipsEntry
|
|
27
40
|
? null
|
|
28
41
|
: 'src/data/tips.json not found — run `npx book-scaffold build-tips` to generate.';
|
|
@@ -30,7 +43,7 @@ const loadError = tipsEntry
|
|
|
30
43
|
// #142: chapter back-links must prefix the deploy base.
|
|
31
44
|
const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
32
45
|
---
|
|
33
|
-
<Base title="Tips" description="Numbered tips from this book, drawn from <Tip> instances in chapters.">
|
|
46
|
+
<Base title="Tips" description="Numbered tips from this book, drawn from <Tip> instances in chapters." bookId={bookId}>
|
|
34
47
|
<article class="prose">
|
|
35
48
|
<h1>Tips</h1>
|
|
36
49
|
{loadError && (
|
|
@@ -51,7 +64,7 @@ const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
|
51
64
|
<p class="tips-index-preview">{tip.preview}</p>
|
|
52
65
|
)}
|
|
53
66
|
<p class="tips-index-meta">
|
|
54
|
-
From <a href={`${baseUrl}chapters/${tip.chapter}/`}>{tip.chapter}</a>
|
|
67
|
+
From <a href={`${baseUrl}chapters/${bookId ? `${bookId}/` : ''}${tip.chapter}/`}>{tip.chapter}</a>
|
|
55
68
|
</p>
|
|
56
69
|
</li>
|
|
57
70
|
))}
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
**Profile**: any (build:figures and build:notebooks both graceful-skip when source dirs / tools are absent).
|
|
4
4
|
|
|
5
|
-
**TL;DR**: Put PDFs in `figures/`, Jupyter notebooks in `notebooks
|
|
5
|
+
**TL;DR**: Put PDFs in `figures/`, Jupyter notebooks in `notebooks/`, then run
|
|
6
|
+
`npm run build:figures` and `npm run build:notebooks` explicitly. Output:
|
|
7
|
+
`public/figures/*.svg` (PDF→SVG via `pdftocairo`) and
|
|
8
|
+
`public/notebooks/*.html` (ipynb→HTML via `uv run jupyter nbconvert`). These
|
|
9
|
+
optional system-tool pipelines are not part of `prebuild`; generated books run
|
|
10
|
+
validation there instead.
|
|
6
11
|
|
|
7
12
|
## How each pipeline works
|
|
8
13
|
|
|
@@ -11,6 +16,7 @@
|
|
|
11
16
|
- **Source**: `figures/` at scaffold root (override via `BOOK_FIGURES_PATH` env var; e.g. `BOOK_FIGURES_PATH=../shared/figures` to share with a LaTeX sibling)
|
|
12
17
|
- **Output**: `public/figures/<same-subdir-structure>/<stem>.svg`
|
|
13
18
|
- **Tool**: `pdftocairo` (poppler-utils) with `pdftoppm` fallback for malformed SVG
|
|
19
|
+
- **Theming**: canonical Warm–Tol / Okabe–Ito colors and neutral paints are mapped to theme-aware CSS variables; see [Recipe 24](24-figure-authoring-standard.md)
|
|
14
20
|
- **Idempotency**: skips when SVG mtime >= PDF mtime
|
|
15
21
|
- **Graceful skip**: if `pdftocairo` or `pdftoppm` not on PATH (Cloudflare build container), warns and exits 0 — committed SVGs under `public/figures/` are served as-is
|
|
16
22
|
|
|
@@ -58,8 +64,9 @@ Cloudflare Workers build containers don't have `pdftocairo` or `uv` installed. T
|
|
|
58
64
|
|
|
59
65
|
1. **Commit derived artifacts** (recommended for low-friction deploy):
|
|
60
66
|
- Edit `.gitignore`: remove the `public/figures/` and `public/notebooks/` lines.
|
|
61
|
-
- Run `npm run build:
|
|
62
|
-
|
|
67
|
+
- Run `npm run build:figures` and `npm run build:notebooks` locally; commit
|
|
68
|
+
the generated outputs.
|
|
69
|
+
- CI serves the committed artifacts without needing either optional system tool.
|
|
63
70
|
- Trade-off: ~3 MB of binary artifacts in git history.
|
|
64
71
|
|
|
65
72
|
2. **Install poppler + uv in CI**: prepend `apt-get install -y poppler-utils && curl -LsSf https://astral.sh/uv/install.sh | sh && ...` to the build command. More setup; cleaner repo.
|
|
@@ -71,12 +78,13 @@ post_transformers chose option 1 (see commit `f7fa75d`).
|
|
|
71
78
|
- **Notebooks should be output-free** for a clean rendered HTML — clear outputs before committing, or use `nbstripout` as a pre-commit hook. Cells with embedded outputs render those outputs in the HTML; this may or may not be what you want.
|
|
72
79
|
- **Stub-size threshold** at 1500 bytes is empirical from post_transformers — adjust via `NOTEBOOK_STUB_BYTES` if your placeholder notebooks are larger.
|
|
73
80
|
- **`pdftocairo` produces a tiny SVG** for some PDF inputs (vector layers ungrouped, etc.). The script auto-falls back to `pdftoppm -r 200 -png` at 200 DPI when SVG output is < 200 bytes.
|
|
81
|
+
- **Do not pre-blend semantic fills** (`warmblue!13`, for example). Export the canonical base color with a separate opacity so the SVG rewrite can preserve its role in dark mode. Recipe 24 has TikZ and matplotlib examples.
|
|
74
82
|
|
|
75
83
|
## Canonical files
|
|
76
84
|
|
|
77
85
|
- `scripts/build-figures.mjs:23-32` — path resolution + env overrides
|
|
78
86
|
- `scripts/render-notebooks.mjs:30-46` — same
|
|
79
|
-
- `package.json` `
|
|
87
|
+
- `package.json` `build:figures` / `build:notebooks` — explicit authoring commands
|
|
80
88
|
- `.gitignore` — toggles whether artifacts are committed
|
|
81
89
|
|
|
82
90
|
## Reference implementation
|
package/recipes/09-validation.md
CHANGED
|
@@ -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
|
|
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`, `
|
|
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.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
`wrangler.toml`, after which the consumer owns that file
|
|
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
|
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
## TL;DR
|
|
6
6
|
|
|
7
|
-
Drop `figures/<topic>/diagram.tex` (standalone TikZ source). Run
|
|
7
|
+
Drop `figures/<topic>/diagram.tex` (standalone TikZ source). Run
|
|
8
|
+
`npm run build:figures` explicitly; figure conversion is not wired into
|
|
9
|
+
`prebuild` because TeX and Poppler are optional system tools. The command emits
|
|
10
|
+
`public/figures/<topic>/diagram.svg`, ready to reference in MDX as
|
|
11
|
+
`<Figure src="/figures/<topic>/diagram.svg" />`.
|
|
8
12
|
|
|
9
13
|
## The TikZ source
|
|
10
14
|
|
|
@@ -25,6 +29,13 @@ Use the `standalone` document class; configure for SVG via `tikz` option. Recomm
|
|
|
25
29
|
|
|
26
30
|
The `border=2mm` adds a small margin around the figure so it doesn't crop right at the edge.
|
|
27
31
|
|
|
32
|
+
For theme-aware color, define the canonical Warm–Tol or Okabe–Ito hex and use
|
|
33
|
+
`fill opacity` rather than an xcolor pre-blend such as `warmblue!13`. The base
|
|
34
|
+
color survives PDF export and `build-figures` can map it to `--fig-*` or
|
|
35
|
+
`--series-*`; the pre-blended RGB cannot preserve its meaning. See
|
|
36
|
+
[Recipe 24 — Figure authoring standard](24-figure-authoring-standard.md) for
|
|
37
|
+
the palette, accessible description pattern, and dual-theme release gate.
|
|
38
|
+
|
|
28
39
|
## Discovery rule
|
|
29
40
|
|
|
30
41
|
`build-figures` walks `figures/` (or `BOOK_FIGURES_PATH`) for both `.pdf` and `.tex` files. For each `.tex` source:
|
|
@@ -136,6 +147,6 @@ layout: wide # widens --measure-main to 80ch; omit (or 'default') for the s
|
|
|
136
147
|
|
|
137
148
|
## See also
|
|
138
149
|
|
|
139
|
-
- `recipes/
|
|
150
|
+
- `recipes/24-figure-authoring-standard.md` — palette, matplotlib/TikZ authoring, dark mode, and accessibility
|
|
140
151
|
- `PACKAGE_DESIGN.md §7` — peer dependencies (lists `pdflatex` as optional system dep)
|
|
141
152
|
- `PACKAGE_DESIGN.md §8` — `book-scaffold` CLI reference (build-figures subcommand)
|
|
@@ -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 §
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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 §
|
|
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 §
|
|
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 —
|
|
1
|
+
# Recipe 21 — One app, multiple books with `defineBookCorpus` (v5, #80)
|
|
2
2
|
|
|
3
|
-
**Profile**: any
|
|
3
|
+
**Profile**: any, provided every book uses the same preset and Style chain.
|
|
4
4
|
|
|
5
|
-
**TL;DR**:
|
|
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
|
-
##
|
|
11
|
+
## Define the corpus once
|
|
8
12
|
|
|
9
|
-
|
|
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
|
-
│
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
196
|
+
## Search and links
|
|
54
197
|
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
208
|
+
```mdx
|
|
209
|
+
<BookLink book="evaluation" to="chapters/metrics#calibration">
|
|
210
|
+
calibration metrics
|
|
211
|
+
</BookLink>
|
|
212
|
+
```
|
|
60
213
|
|
|
61
|
-
|
|
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
|
-
|
|
219
|
+
## Generated data and `--book`
|
|
64
220
|
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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`
|
|
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 &
|
|
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
|