@brandon_m_behring/book-scaffold-astro 4.26.2 → 4.27.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 (49) hide show
  1. package/AGENTS.md +6 -0
  2. package/CLAUDE.md +35 -10
  3. package/LATEX_TO_MDX_MAPPING.md +1 -1
  4. package/LICENSE +27 -0
  5. package/LICENSE-CONTENT +19 -0
  6. package/README.md +33 -0
  7. package/components/AssessmentTest.astro +2 -1
  8. package/components/ChapterNav.astro +2 -2
  9. package/components/Cite.astro +2 -1
  10. package/components/NavContent.astro +2 -2
  11. package/components/PartReview.astro +2 -1
  12. package/components/Rationale.astro +3 -2
  13. package/components/Sidebar.astro +2 -1
  14. package/components/Term.astro +2 -1
  15. package/components/TipsCard.astro +2 -1
  16. package/components/VersionSelector.tsx +39 -36
  17. package/components/WeekRef.astro +2 -1
  18. package/components/XRef.astro +2 -1
  19. package/dist/components/VersionSelector.d.ts +16 -2
  20. package/dist/components/VersionSelector.mjs +18 -17
  21. package/dist/index.d.ts +24 -6
  22. package/dist/index.mjs +96 -8
  23. package/dist/schemas.d.ts +1 -1
  24. package/dist/schemas.mjs +8 -1
  25. package/dist/{types-Hue-uSeQ.d.ts → types-CZrkqzpC.d.ts} +84 -48
  26. package/layouts/Base.astro +23 -21
  27. package/package.json +7 -4
  28. package/pages/answers.astro +2 -1
  29. package/pages/chapters.astro +2 -1
  30. package/pages/exercises.astro +2 -1
  31. package/pages/index.astro +4 -3
  32. package/pages/search.astro +2 -1
  33. package/pages/tips.astro +2 -1
  34. package/recipes/01-add-math.md +40 -28
  35. package/recipes/04-component-library.md +14 -4
  36. package/recipes/05-deploy-cloudflare.md +44 -0
  37. package/recipes/06-mobile-first-layout.md +25 -2
  38. package/recipes/09-validation.md +18 -9
  39. package/recipes/15-defining-styles.md +7 -3
  40. package/recipes/19-prevalidate-hook.md +29 -83
  41. package/scripts/build-bib.mjs +7 -3
  42. package/scripts/build-labels.mjs +33 -16
  43. package/scripts/read-env.mjs +25 -0
  44. package/scripts/resolve-book-config.mjs +96 -0
  45. package/scripts/validate.mjs +125 -84
  46. package/src/lib/define-style.ts +25 -8
  47. package/src/lib/nav-href.ts +24 -2
  48. package/styles/layout.css +14 -8
  49. package/styles/tokens.css +8 -10
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@brandon_m_behring/book-scaffold-astro",
3
- "description": "Astro 6 + MDX toolkit for long-form technical books. Profile-aware (academic / tools / minimal); ships Tufte typography, KaTeX, BibTeX citations, Pagefind, Cloudflare Workers deploy. See PACKAGE_DESIGN.md for the API contract.",
4
- "version": "4.26.2",
3
+ "description": "Astro 6 + MDX toolkit for long-form technical books with five typed presets, Tufte typography, citations, search, PDF, and Cloudflare deployment.",
4
+ "version": "4.27.0",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Brandon Behring",
@@ -161,9 +161,11 @@
161
161
  "pedagogy",
162
162
  "examples",
163
163
  "CLAUDE.md",
164
+ "AGENTS.md",
164
165
  "README.md",
165
- "LATEX_TO_MDX_MAPPING.md",
166
- "examples"
166
+ "LICENSE",
167
+ "LICENSE-CONTENT",
168
+ "LATEX_TO_MDX_MAPPING.md"
167
169
  ],
168
170
  "scripts": {
169
171
  "build": "tsup",
@@ -195,6 +197,7 @@
195
197
  "@fontsource-variable/roboto": "^5.2.10",
196
198
  "@fontsource-variable/source-code-pro": "^5.2.7",
197
199
  "pagefind": "^1.5.2",
200
+ "vite": "^7.3.2",
198
201
  "yaml": "^2.9.0"
199
202
  },
200
203
  "devDependencies": {
@@ -26,6 +26,7 @@ import { render } from 'astro:content';
26
26
  import bookConfig from 'virtual:book-scaffold/book-config';
27
27
  import { getAllQuestions, groupByChapter } from '../src/lib/questions';
28
28
  import { assertKnownDomain } from '../src/lib/exam-domains';
29
+ import { normalizeBase } from '../src/lib/nav-href';
29
30
 
30
31
  // Presence-gate: only touch the collection when the directory holds files.
31
32
  const questionModules = import.meta.glob('/src/content/questions/**/*.{md,mdx}', {
@@ -51,7 +52,7 @@ const rendered = await Promise.all(
51
52
  const byChapter = groupByChapter(rendered);
52
53
  const total = rendered.length;
53
54
  // #142: practice-exam + chapter links must prefix the deploy base.
54
- const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
55
+ const baseUrl = normalizeBase(import.meta.env.BASE_URL);
55
56
  const practiceHref = (bookConfig.enabledRoutes ?? []).includes('practiceExam')
56
57
  ? `${baseUrl}practice-exam`
57
58
  : null;
@@ -26,9 +26,10 @@ import { getAllChapters, type Chapter } from '../src/lib/chapters';
26
26
  import { PROFILES } from '../src/profiles/index';
27
27
  import { fallbackChaptersRenderer } from '../src/profiles/renderers/fallback-chapters';
28
28
  import type { ChaptersRenderer, PartKey } from '../src/lib/chapters-renderer';
29
+ import { normalizeBase } from '../src/lib/nav-href';
29
30
 
30
31
  // #142: prefix BASE_URL so chapter-card links stay inside a non-root deploy base.
31
- const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
32
+ const baseUrl = normalizeBase(import.meta.env.BASE_URL);
32
33
 
33
34
  const profileName = (import.meta.env.BOOK_PROFILE ?? 'minimal') as keyof typeof PROFILES;
34
35
  const profileDef = PROFILES[profileName];
@@ -10,6 +10,7 @@
10
10
  * page (doesn't fail the build).
11
11
  */
12
12
  import Base from '../layouts/Base.astro';
13
+ import { normalizeBase } from '../src/lib/nav-href';
13
14
 
14
15
  // v4.4.0 fix: use Vite's import.meta.glob with a project-root-relative
15
16
  // path (same lesson as tips.astro). `/src/data/...` is consumer-project-
@@ -28,7 +29,7 @@ const chapters = Object.keys(byChapter).sort();
28
29
  const total = chapters.reduce((sum, c) => sum + byChapter[c].length, 0);
29
30
 
30
31
  // #142: chapter deep-links must prefix the deploy base.
31
- const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
32
+ const baseUrl = normalizeBase(import.meta.env.BASE_URL);
32
33
  ---
33
34
  <Base title="Exercises" description="All exercises in this book, grouped by chapter with deep links.">
34
35
  <article class="prose">
package/pages/index.astro CHANGED
@@ -34,15 +34,16 @@
34
34
  */
35
35
  import Base from '../layouts/Base.astro';
36
36
  import bookConfig from 'virtual:book-scaffold/book-config';
37
+ import { normalizeBase } from '../src/lib/nav-href';
37
38
 
38
39
  const title = bookConfig.title ?? 'book-scaffold-astro';
39
40
  const description = bookConfig.description;
40
41
  const portfolio = bookConfig.portfolio;
41
42
  const enabledRoutes = bookConfig.enabledRoutes;
42
43
 
43
- // Astro guarantees BASE_URL carries a trailing slash ('/' or '/foo/'), so
44
- // `${baseUrl}chapters/` resolves correctly under any deploy base (#142).
45
- const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
44
+ // Astro accepts bases with or without a trailing slash. Route every value
45
+ // through the shared helper before composing links (#142, #182).
46
+ const baseUrl = normalizeBase(import.meta.env.BASE_URL);
46
47
 
47
48
  // Map from internal route name → display label + URL. Only routes that
48
49
  // produce a single landing-list entry are listed here (frontmatter is a
@@ -11,10 +11,11 @@
11
11
  * `astro dev`. See README for dev-search workflow.
12
12
  */
13
13
  import Base from '../layouts/Base.astro';
14
+ import { normalizeBase } from '../src/lib/nav-href';
14
15
 
15
16
  // #142: Pagefind writes its bundle to dist/pagefind/, served under the deploy
16
17
  // base — so the asset URLs must prefix BASE_URL or they 404 under a non-root base.
17
- const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
18
+ const baseUrl = normalizeBase(import.meta.env.BASE_URL);
18
19
  ---
19
20
 
20
21
  <Base
package/pages/tips.astro CHANGED
@@ -10,6 +10,7 @@
10
10
  * page (doesn't fail the build).
11
11
  */
12
12
  import Base from '../layouts/Base.astro';
13
+ import { normalizeBase } from '../src/lib/nav-href';
13
14
 
14
15
  // v4.4.0 fix: use Vite's import.meta.glob with a project-root-relative
15
16
  // path. The previous `../../../src/data/tips.json` resolution failed when
@@ -27,7 +28,7 @@ const loadError = tipsEntry
27
28
  : 'src/data/tips.json not found — run `npx book-scaffold build-tips` to generate.';
28
29
 
29
30
  // #142: chapter back-links must prefix the deploy base.
30
- const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
31
+ const baseUrl = normalizeBase(import.meta.env.BASE_URL);
31
32
  ---
32
33
  <Base title="Tips" description="Numbered tips from this book, drawn from <Tip> instances in chapters.">
33
34
  <article class="prose">
@@ -1,27 +1,26 @@
1
1
  # Recipe 01 — Add math to your book (KaTeX)
2
2
 
3
- **Profile**: academic (gated by `BOOK_PROFILE=academic`)
3
+ **Profiles**: academic + research-portfolio (the presets that flag `katex: true`)
4
4
 
5
- **TL;DR**: Math is wired but disabled by default. Set `BOOK_PROFILE=academic` and the build adds `remark-math` + `rehype-katex` (strict mode) with the SSM macro library at `src/lib/katex-macros.ts`.
5
+ **TL;DR**: Math is wired but profile-gated. Compose your book from `academicStyle` (or `researchPortfolioStyle`) and `defineBookConfig` adds `remark-math` + `rehype-katex` (strict mode) with the scaffold's 37-macro `ssmMacros` library. Extend per-book with `defineBookConfig({ katexMacros })` — never by editing package source.
6
6
 
7
7
  ## How it works
8
8
 
9
- The conditional integration lives at the top of `astro.config.mjs`:
10
- - Reads `process.env.BOOK_PROFILE ?? 'minimal'`
11
- - For `academic`: dynamically imports `remark-math`, `rehype-katex`, and `ssmMacros`; adds them to the markdown pipeline
12
- - KaTeX CSS (`katex/dist/katex.min.css`) is always loaded by `Base.astro` academic books need it; minimal/tools books carry the cost (~60 KB) without rendering math (the CSS is inert without matching DOM)
9
+ All the wiring lives inside the package (`defineBookConfig`), not in your config file:
10
+
11
+ - `defineBookConfig` resolves the composed preset from your `styles` chain (falling back to `BOOK_PRESET`/`BOOK_PROFILE` from the environment or `.env`).
12
+ - For katex-flagged presets it dynamically imports `remark-math` + `rehype-katex` and registers them with `strict: 'error'` and the merged macro set (`ssmMacros` + your `katexMacros`).
13
+ - The integration injects `katex/dist/katex.min.css` **only** for katex presets (`package/src/integration.ts`) — tools/minimal books don't carry the ~60 KB.
14
+ - `katex`, `remark-math`, `rehype-katex` are **optional peerDependencies**: math books install them (`npm i katex remark-math rehype-katex`); other profiles skip them entirely.
13
15
 
14
16
  ## Enable math in your book
15
17
 
16
- 1. Set `BOOK_PROFILE=academic` at run time:
17
- ```bash
18
- BOOK_PROFILE=academic npm run dev # local hot-reload
19
- BOOK_PROFILE=academic npm run build # production build
20
- ```
21
- Or persist it in `.env`:
22
- ```
23
- BOOK_PROFILE=academic
18
+ 1. Use a katex preset in `astro.config.mjs` (a fresh `create-book --preset=academic` scaffold already does):
19
+ ```js
20
+ import { defineBookConfig, academicStyle } from '@brandon_m_behring/book-scaffold-astro';
21
+ export default await defineBookConfig({ styles: [academicStyle], site: 'https://…' });
24
22
  ```
23
+ Keep `.env`'s `BOOK_PRESET`/`BOOK_PROFILE` in sync — the content-collection schemas resolve the preset from it.
25
24
 
26
25
  2. Author math in MDX with `$...$` (inline) and `$$...$$` (display):
27
26
  ```mdx
@@ -33,39 +32,52 @@ The conditional integration lives at the top of `astro.config.mjs`:
33
32
  $$
34
33
  ```
35
34
 
36
- 3. Strict mode is on by default (`strict: 'error'` in `rehypeKatex` options). Unknown macros, malformed expressions, and unsupported AMS environments fail the build with a precise error. This is intentional — catch typos at write-time, not in production.
35
+ 3. Strict mode is on by default (`strict: 'error'`). Unknown macros, malformed expressions, and unsupported AMS environments fail the build with a precise error. This is intentional — catch typos at write-time, not in production.
37
36
 
38
37
  ## Macro library
39
38
 
40
- `src/lib/katex-macros.ts` defines 36 macros:
39
+ The package ships `ssmMacros` — **37 macros** (`package/src/lib/katex-macros.ts`):
40
+
41
41
  - 20 SSM-specific from the post-transformers academic reference: `\statevec`, `\statemat`, `\inputmat`, `\outputmat`, `\feedmat`, `\stepsize`, `\discA`, `\discB`, `\seqlen`, `\statedim`, `\inputdim`, `\scanop`, `\elemwise`, `\monodromy`, `\floquet`, `\lyapexp`, `\jacobian`, `\ddt`, `\pderiv`, `\spectralradius`
42
42
  - 16 general math: `\R`, `\C`, `\N`, `\Z`, `\E`, `\Prob`, `\norm`, `\ip`, `\abs`, `\argmax`, `\argmin`, `\diag`, `\tr`, `\spec`, `\rank`, `\bigO`
43
43
  - 1 compatibility alias: `\bm` → `\boldsymbol` (KaTeX doesn't ship `\bm`)
44
44
 
45
- To extend for your book, edit `src/lib/katex-macros.ts` and add new entries to the `ssmMacros` object:
45
+ **Read or spread it** (v4.27.0+, #177) from the main entry, or the `./lib` subpath:
46
+
46
47
  ```ts
47
- export const ssmMacros = {
48
- // existing macros...
49
- '\\mybook': '\\mathbb{B}', // 0-arg macro
50
- '\\myfunc': '\\mathrm{my}(#1)', // 1-arg macro
51
- };
48
+ import { ssmMacros } from '@brandon_m_behring/book-scaffold-astro';
49
+ ```
50
+
51
+ **Extend for your book — the supported path** (#22). `katexMacros` is shallow-merged **on top of** `ssmMacros`, so your entries win on collision:
52
+
53
+ ```js
54
+ export default await defineBookConfig({
55
+ styles: [academicStyle],
56
+ site: 'https://…',
57
+ katexMacros: {
58
+ '\\ate': '\\tau', // 0-arg macro
59
+ '\\propensity': 'e(#1)', // 1-arg macro
60
+ },
61
+ });
52
62
  ```
53
63
 
64
+ Do **not** edit `katex-macros.ts` — it lives inside `node_modules` and your changes vanish on the next install. If a macro is general enough for every book, file an issue instead.
65
+
54
66
  ## Common gotchas
55
67
 
56
68
  - **`\bm{x}` doesn't ship with KaTeX.** The macro library aliases it to `\boldsymbol{x}` (visually identical in stix-two / Computer Modern fonts).
57
69
  - **`\psmallmatrix` not supported by KaTeX.** Convert to `\begin{pmatrix} ... \end{pmatrix}` in your MDX source.
58
- - **Equation auto-numbering across a document is not supported by KaTeX.** Each `$$...$$` block is independent. Use `\tag{N}` for explicit numbering, or a per-chapter remark plugin (deferred; see PROFILES_DESIGN.md §6).
70
+ - **Equation auto-numbering across a document is not supported by KaTeX.** Each `$$...$$` block is independent. Use `\tag{N}` for explicit numbering; a per-chapter remark plugin is tracked as #146.
59
71
  - **`{,}` (LaTeX thousands separator) breaks MDX** — MDX parses `{...}` as a JSX expression. Use `1,000` not `1{,}000`.
60
72
  - **Math inside JSX components needs care.** `<Theorem>$x^2$</Theorem>` works because remark-math runs after MDX parses JSX, but escape any `{` or `}` in arguments.
61
73
 
62
74
  ## Canonical files
63
75
 
64
- - `astro.config.mjs:14-32` — profile branch + plugin wiring
65
- - `src/lib/katex-macros.ts` — full macro library (36 entries)
66
- - `src/layouts/Base.astro:24-30` — KaTeX CSS import
67
- - `package.json` — `katex ^0.16`, `remark-math ^6`, `rehype-katex ^7` deps
76
+ - `package/src/lib/katex-macros.ts` — the 37-entry `ssmMacros` library
77
+ - `package/src/config.ts` — profile-gated remark/rehype wiring + the `katexMacros` merge
78
+ - `package/src/integration.ts` — KaTeX CSS injection for katex presets
79
+ - your `astro.config.mjs` — `styles: [academicStyle]` + optional `katexMacros`
68
80
 
69
81
  ## Reference implementation
70
82
 
71
- [`~/Claude/post_transformers/guides/web/`](../) at commit `111ba26` (math first wired) through `2eaef5d` (current). The reference book has 6 academic chapters using these macros and exercises every edge case the scaffold supports.
83
+ [`~/Claude/post_transformers/guides/web/`](../) the academic reference book has 6 chapters using these macros and exercises every edge case the scaffold supports.
@@ -42,19 +42,29 @@ For full theorem-like environments (proof scaffolding, numbering), use `<Theorem
42
42
 
43
43
  ## Theorem family (academic profile)
44
44
 
45
- `src/components/Theorem.astro` — unified component for nine LaTeX-style environments via the `type` prop:
45
+ `src/components/Theorem.astro` — unified component for nine LaTeX-style environments via the canonical `kind` prop (`type` remains a legacy alias):
46
46
 
47
47
  ```mdx
48
- <Theorem type="theorem" id="thm:zoh-stability" label="ZOH stability">
48
+ <Theorem kind="theorem" id="thm:zoh-stability" name="ZOH stability">
49
49
  The bilinear discretization preserves stability iff $|\lambda \Delta t| < 1$.
50
50
  </Theorem>
51
51
 
52
- <Theorem type="proof">
52
+ <Theorem kind="proof">
53
53
  Direct algebra on the bilinear map.
54
54
  </Theorem>
55
55
  ```
56
56
 
57
- Supported `type` values: `theorem`, `proposition`, `lemma`, `corollary`, `definition`, `example`, `exercise`, `remark`, `proof`. Each gets its own bar color and numbering counter.
57
+ Supported kinds: `theorem`, `proposition`, `lemma`, `corollary`, `definition`, `example`, `exercise`, `remark`, `proof`. Each gets its own bar color. By default all kinds share one amsthm-style sequence, preserving the v4.26-and-earlier contract. To give each kind an independent sequence, set the persistent book/style option:
58
+
59
+ ```js
60
+ export default await defineBookConfig({
61
+ styles: [academicStyle],
62
+ numberStyle: 'per-kind', // or declare it in a composed defineStyle(...)
63
+ site: 'https://example.invalid',
64
+ });
65
+ ```
66
+
67
+ `build-labels` evaluates the real Astro config and records the resolved strategy. An explicit `label=` override is custom and unnumbered, so it consumes no counter in either mode. Omission defaults to `shared`, including legacy projects with no resolvable scaffold integration.
58
68
 
59
69
  ## Utility components (any profile)
60
70
 
@@ -28,6 +28,50 @@
28
28
 
29
29
  URL: `https://<project-name>.<your-account>.workers.dev`.
30
30
 
31
+ ## Default security headers
32
+
33
+ The scaffold integration emits `dist/_headers` at the end of every Astro
34
+ build. Cloudflare Static Assets reads that file for both Workers and Pages
35
+ deployments. The default applies:
36
+
37
+ - `Strict-Transport-Security`
38
+ - `X-Content-Type-Options`
39
+ - `Referrer-Policy`
40
+ - `Permissions-Policy`
41
+ - `Content-Security-Policy`
42
+
43
+ The CSP includes the allowances the shipped book UI needs: inline theme and
44
+ drawer scripts, Astro inline component styles, Pagefind WebAssembly,
45
+ Cloudflare Web Analytics, and images from the book itself, data URIs, or any
46
+ HTTPS origin.
47
+
48
+ Use a consumer file when you need route-specific rules or full ownership:
49
+
50
+ ```text
51
+ # public/_headers
52
+ /private/*
53
+ Cache-Control: no-store
54
+ ```
55
+
56
+ Astro copies `public/_headers` to `dist/_headers`; the integration detects it
57
+ and leaves it unchanged. To retain the four non-CSP defaults but replace the
58
+ entire CSP, configure the book:
59
+
60
+ ```js
61
+ export default await defineBookConfig({
62
+ styles: [academicStyle],
63
+ site: 'https://my-book.example',
64
+ securityHeaders: {
65
+ contentSecurityPolicy:
66
+ "default-src 'self'; img-src 'self' data: https://images.example; object-src 'none'",
67
+ },
68
+ });
69
+ ```
70
+
71
+ That value is a complete replacement, not an additive fragment. Set
72
+ `securityHeaders: false` if the deployment platform or another integration
73
+ owns the headers and you do not ship `public/_headers`.
74
+
31
75
  ## The `cd` prefix for monorepo Astro projects
32
76
 
33
77
  When `wrangler.toml` lives in a subdirectory (e.g. `guides/web/wrangler.toml`), Cloudflare runs commands from the repo root by default. Wrangler can't find its config there → deploy fails with "Could not detect a directory containing static files".
@@ -2,11 +2,11 @@
2
2
 
3
3
  **Profile**: any (layout is profile-agnostic; sidebar groups chapters differently per profile).
4
4
 
5
- **TL;DR**: Three-tier Tufte width (65/80/90ch) + 28/24/26ch sidenote column + left chapter-nav sidebar at ≥1024px. Mobile (<48rem) collapses to single column with inline sidenote asides; sidebar hides. All pure CSS, zero JS.
5
+ **TL;DR**: Three-tier Tufte width (65/80/90ch) + 28/24/26ch sidenote column + left chapter-nav sidebar at ≥1024px. Mobile (<48rem) collapses to single column with inline sidenote asides; sidebar hides. The layout is pure CSS; optional chrome controls hydrate independently.
6
6
 
7
7
  ## The three layers
8
8
 
9
- 1. **Floating chrome** (top-right, `position: fixed`): theme toggle + search + tools-profile islands (`ToolFilter`, `VersionSelector`) when `BOOK_PROFILE !== 'academic'`.
9
+ 1. **Floating chrome** (top-right, `position: fixed`): theme toggle + search + the `ToolFilter` island when `BOOK_PROFILE !== 'academic'`. `VersionSelector` is manual opt-in because its links must come from a real deployment manifest (see below).
10
10
  2. **Left sidebar** (`Sidebar.astro`): chapter nav grouped by part. Sticky to top, ≤100vh, independently scrollable. Hidden below 64rem (1024px).
11
11
  3. **Main content** (`.prose`): Tufte 2-column. Main text at `--measure-main`, sidenote column at `--measure-side`, both responsive to viewport tier.
12
12
 
@@ -45,6 +45,29 @@ Re-tune with Playwright + `browser_take_screenshot` at the four viewports above
45
45
 
46
46
  Default is `showSidebar={true}`. Set it false on full-bleed surfaces — landing pages, splash screens, search results — that have no chapter context (`Base.astro` still emits the page's single `<main>` landmark in that branch). Every chapter route inherits the default (true).
47
47
 
48
+ ## Opt-in version selector
49
+
50
+ `Base.astro` does not mount a version selector or manufacture version URLs. If
51
+ your deployment publishes multiple versions, mount the exported Preact island
52
+ where your book's navigation belongs and pass the real destinations directly:
53
+
54
+ ```astro
55
+ ---
56
+ import VersionSelector from '@brandon_m_behring/book-scaffold-astro/components/VersionSelector';
57
+
58
+ const versions = [
59
+ { href: '/', label: 'Latest', date: '2026-07-13', current: true },
60
+ { href: '/versions/v4.26/', label: 'v4.26', date: '2026-06-30' },
61
+ ];
62
+ ---
63
+
64
+ <VersionSelector versions={versions} client:idle />
65
+ ```
66
+
67
+ Each entry requires `href`, `label`, and `date`; `current` is optional. An
68
+ omitted or empty `versions` array renders nothing, so incomplete deployment
69
+ metadata cannot turn placeholder releases into live navigation.
70
+
48
71
  ## Customizing the sidebar
49
72
 
50
73
  `Sidebar.astro` reads `import.meta.env.BOOK_PROFILE` to decide grouping:
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Profile**: any (Cite checks skip under non-academic).
4
4
 
5
- **TL;DR**: `npm run validate` runs `scripts/validate.mjs` against all chapter MDX files. Catches typo'd bibkeys / XRef ids / Figure paths / internal links that `astro build` would either miss or surface with poor context. Auto-runs as `prebuild`; recommend wiring into pre-commit too.
5
+ **TL;DR**: `npm run validate` runs `scripts/validate.mjs` against all chapter MDX files. It first regenerates missing `labels.json` and `references.json`, then catches typo'd bibkeys / XRef ids / Figure paths / internal links that `astro build` would either miss or surface with poor context. Auto-runs as `prebuild`; recommend wiring into pre-commit too.
6
6
 
7
7
  ## What gets checked
8
8
 
@@ -13,13 +13,19 @@
13
13
  | `<Figure src="/...">` file exists under `public/` | all | Figure.astro emits a broken-image icon for missing files; build doesn't fail. |
14
14
  | `[text](/internal-link)` resolves to known chapter slug or top-level route | all | Astro won't fail on dead internal links. Warning, not error (regex misses dynamic routes). |
15
15
  | `<CodeRef path="..." line={N} />` path exists + line in bounds | all, if `BOOK_REPO_ROOT` set | Catches stale line numbers after code refactors in the paired experiments/ repo. |
16
- | `<Theorem>` has a resolvable `kind=` (or legacy `type=`); an id'd theorem resolves in `labels.json` (#121, #126) | all | An absent kind throws at build with less context; an unindexed id silently renders the heading unnumbered. |
16
+ | `<Theorem>` has a resolvable `kind=` (or legacy `type=`); an id'd theorem resolves in `labels.json`; a literal `n=` agrees with the index (#121, #126, #176) | all | An absent kind throws at build with less context; an unindexed id silently renders the heading unnumbered; a stale literal `n=` contradicts the heading/XRefs. Dynamic expressions and `label=` overrides are skipped. |
17
17
  | `<BookLink book= to=>` both present; `book=` registered in `siblingBooks` (#96) | all | Pre-flights the component's build-time throw across all files at once. |
18
18
  | Questions collection: unique `id`s + `domain` in `examDomains` (#112); `<Rationale appendix>` carries `for=` (#114, v4.21.0) | all, when `src/content/questions/` exists | Duplicate ids break the appendix/flashcards cross-ref key; an unregistered domain throws one-at-a-time at build; an appendix rationale without its anchor target throws at build. |
19
19
  | `los[].anchor` ↔ `{/* anchor: <slug> */}` prose markers agree both ways (#130, v4.20.0) | all, when frontmatter has `los:` | A declared objective whose prose marker is missing/misspelled (or an orphan marker) builds green otherwise — frontmatter↔prose drift only a hand audit would catch. |
20
20
 
21
21
  Validate also emits two **non-blocking shadow-route warnings** (exit code unaffected): a consumer-owned `src/pages/chapters/[...slug].astro` without `routes: { chapters: false }` (v4.6.0, #76), and a consumer-owned `src/pages/index.astro` without `routes: { landing: false }` (v4.20.0, #129 — Astro has announced this collision becomes a hard error). See [recipe 18](./18-chapter-route-ownership.md).
22
22
 
23
+ ## Missing generated artifacts
24
+
25
+ `src/data/labels.json` and `src/data/references.json` are derived and normally gitignored. v4.27+ `validate` self-heals either missing file unconditionally by invoking the package's own `build-labels` or `build-bib` command before loading data. Existing files are untouched. A failed child command stops validation with the child's original diagnostic and exit status.
26
+
27
+ `build-bib` resolves `BOOK_BIB_PATH` from the process environment first, then the project-root `.env`, then `./bibliography.bib`. A project with no bibliography still gets a deterministic empty `references.json`.
28
+
23
29
  ## What is NOT checked (already covered elsewhere)
24
30
 
25
31
  - **Frontmatter Zod validation** — `astro build` syncs content collections first; Zod errors there.
@@ -34,8 +40,9 @@ The validator's job is to fill the gaps `astro build` leaves, not duplicate it.
34
40
  // package.json
35
41
  {
36
42
  "scripts": {
37
- "validate": "node scripts/validate.mjs",
38
- "prebuild": "npm run build:assets && npm run validate"
43
+ "prevalidate": "npm run build:bib --if-present && npm run build:labels --if-present",
44
+ "validate": "book-scaffold validate",
45
+ "prebuild": "npm run validate --if-present"
39
46
  }
40
47
  }
41
48
  ```
@@ -61,7 +68,9 @@ For pre-commit: add to `.pre-commit-config.yaml`:
61
68
 
62
69
  ## Preset / chaptersBase resolution (v4.7.0+, #75)
63
70
 
64
- The validator resolves both `preset` and `chaptersBase` by consulting multiple sources in a documented order. Notable v4.7.0 addition: the v4.5+ canonical form
71
+ 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. 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.
72
+
73
+ `chaptersBase` resolution still consults `BOOK_CHAPTERS_DIR`, content configuration, then `src/content/chapters`. The v4.5+ canonical form is:
65
74
 
66
75
  ```ts
67
76
  // src/content.config.ts
@@ -80,13 +89,13 @@ Full precedence chain documented in [`PACKAGE_DESIGN.md §8 — Preset + chapter
80
89
  Exit code = total error count. On success:
81
90
 
82
91
  ```
83
- validate: ✓ 6 chapter(s) checked (profile=academic); no errors.
92
+ validate: ✓ 6 chapter(s) checked (profile=academic, number-style=shared); no errors.
84
93
  ```
85
94
 
86
95
  On failure, all issues listed at once with `file:line msg`:
87
96
 
88
97
  ```
89
- validate: ✗ 17 error(s) in 6 chapter(s) (profile=academic):
98
+ validate: ✗ 17 error(s) in 6 chapter(s) (profile=academic, number-style=shared):
90
99
  week05.mdx:102 Unknown XRef id "w4:prop:zoh-stability" — not in labels.json
91
100
  week11.mdx:37 Unknown XRef id "ch:week13" — not in labels.json
92
101
  ...
@@ -115,13 +124,13 @@ Keep the script regex-based; resist the urge to pull in MDX AST parsing. The scr
115
124
 
116
125
  - **Regex false negatives**: multi-line `<Cite\n key="...">` won't match. Authors should keep component attributes on one line; the build catches the residual cases.
117
126
  - **`<Figure src>` with `BOOK_FIGURES_PATH` override**: the validator checks `public/figures/<...>` (post-build location), not the source `figures/` directory. Run `npm run build:figures` before `npm run validate` if assets are stale.
118
- - **`labels.json` empty**: every XRef fires an error. Until you have a labels-building step (Phase 2.6 in post-transformers, deferred at scaffold v2.0), avoid `<XRef>` in chapter content — use direct markdown links instead.
127
+ - **`labels.json` exists but is stale**: self-healing only regenerates missing artifacts. Run `npm run build:labels` after changing IDs, kinds, slugs, or `numberStyle`.
119
128
 
120
129
  ## Canonical files
121
130
 
122
131
  - `scripts/validate.mjs` — the validator
123
132
  - `src/data/references.json` — emitted by `scripts/build-bib.mjs` (recipe 02)
124
- - `src/data/labels.json` — placeholder; populated by future labels-building step
133
+ - `src/data/labels.json` — emitted by `scripts/build-labels.mjs`
125
134
  - `src/components/{Cite,XRef,Figure,CodeRef}.astro` — components whose contracts validate.mjs enforces
126
135
 
127
136
  ## Reference implementation
@@ -46,7 +46,7 @@ defineStyle({
46
46
  extraIntegrations?: readonly AstroIntegration[];
47
47
  mdxComponentsModule?: string;
48
48
  markdown?: AstroUserConfig['markdown'];
49
- deploy?: 'pages' | 'workers'; // v4.0.0 NEW (#50)
49
+ deploy?: 'pages' | 'workers'; // RESERVED (#50, #180) — no runtime effect; wrangler shape is set at scaffold time by profile
50
50
  extra?: Record<string, unknown>; // scoped consumer-side metadata
51
51
  });
52
52
  ```
@@ -122,7 +122,8 @@ Different fields have different merge semantics. Documented:
122
122
 
123
123
  | Field | Strategy |
124
124
  |---|---|
125
- | `name`, `preset`, `site`, `deploy`, `mdxComponentsModule` | Shallow override (last wins) |
125
+ | `name`, `preset`, `site`, `deploy`, `mdxComponentsModule` | Shallow override (last defined wins) |
126
+ | `releaseStatus` | Shallow override (last defined object replaces the whole earlier object); `false` suppresses an inherited banner |
126
127
  | `routes` | Per-route spread (each route key independently overridable) |
127
128
  | `routes.frontmatter` | Per-route spread; later value (boolean OR object) wholly replaces earlier |
128
129
  | `katexMacros` | Object spread (per-macro override) |
@@ -157,7 +158,10 @@ defineBookConfig({ styles: [researchPortfolioStyle], ... });
157
158
  defineBookConfig({ styles: [BUILTIN_STYLES['research-portfolio']], ... });
158
159
  ```
159
160
 
160
- Each built-in style has a `name` matching its preset, a `preset` field, and sane `deploy` defaults (academic/tools/minimal → 'workers'; course-notes/research-portfolio → 'pages').
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).
161
165
 
162
166
  ---
163
167