@brandon_m_behring/book-scaffold-astro 4.26.3 → 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.
- package/AGENTS.md +6 -0
- package/CLAUDE.md +35 -10
- package/LATEX_TO_MDX_MAPPING.md +1 -1
- package/LICENSE +27 -0
- package/LICENSE-CONTENT +19 -0
- package/README.md +33 -0
- package/components/AssessmentTest.astro +2 -1
- package/components/ChapterNav.astro +2 -2
- package/components/Cite.astro +2 -1
- package/components/NavContent.astro +2 -2
- package/components/PartReview.astro +2 -1
- package/components/Rationale.astro +3 -2
- package/components/Sidebar.astro +2 -1
- package/components/Term.astro +2 -1
- package/components/TipsCard.astro +2 -1
- package/components/VersionSelector.tsx +39 -36
- package/components/WeekRef.astro +2 -1
- package/components/XRef.astro +2 -1
- package/dist/components/VersionSelector.d.ts +16 -2
- package/dist/components/VersionSelector.mjs +18 -17
- package/dist/index.d.ts +24 -6
- package/dist/index.mjs +89 -5
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.mjs +8 -1
- package/dist/{types-CWXP1S4b.d.ts → types-CZrkqzpC.d.ts} +58 -26
- package/layouts/Base.astro +20 -19
- package/package.json +7 -4
- package/pages/answers.astro +2 -1
- package/pages/chapters.astro +2 -1
- package/pages/exercises.astro +2 -1
- package/pages/index.astro +4 -3
- package/pages/search.astro +2 -1
- package/pages/tips.astro +2 -1
- package/recipes/01-add-math.md +40 -28
- package/recipes/04-component-library.md +14 -4
- package/recipes/05-deploy-cloudflare.md +44 -0
- package/recipes/06-mobile-first-layout.md +25 -2
- package/recipes/09-validation.md +18 -9
- package/recipes/15-defining-styles.md +5 -2
- package/recipes/19-prevalidate-hook.md +29 -83
- package/scripts/build-bib.mjs +7 -3
- package/scripts/build-labels.mjs +33 -16
- package/scripts/read-env.mjs +25 -0
- package/scripts/resolve-book-config.mjs +96 -0
- package/scripts/validate.mjs +125 -84
- package/src/lib/define-style.ts +15 -6
- package/src/lib/nav-href.ts +24 -2
- package/styles/layout.css +14 -8
- package/styles/tokens.css +8 -10
package/pages/search.astro
CHANGED
|
@@ -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
|
|
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
|
|
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">
|
package/recipes/01-add-math.md
CHANGED
|
@@ -1,27 +1,26 @@
|
|
|
1
1
|
# Recipe 01 — Add math to your book (KaTeX)
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Profiles**: academic + research-portfolio (the presets that flag `katex: true`)
|
|
4
4
|
|
|
5
|
-
**TL;DR**: Math is wired but
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
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.
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
|
|
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'`
|
|
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`
|
|
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
|
-
|
|
45
|
+
**Read or spread it** (v4.27.0+, #177) — from the main entry, or the `./lib` subpath:
|
|
46
|
+
|
|
46
47
|
```ts
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
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
|
-
- `
|
|
65
|
-
- `src/
|
|
66
|
-
- `src/
|
|
67
|
-
- `
|
|
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/`](../)
|
|
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`
|
|
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
|
|
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
|
|
52
|
+
<Theorem kind="proof">
|
|
53
53
|
Direct algebra on the bilinear map.
|
|
54
54
|
</Theorem>
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
Supported
|
|
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.
|
|
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 +
|
|
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:
|
package/recipes/09-validation.md
CHANGED
|
@@ -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.
|
|
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
|
-
"
|
|
38
|
-
"
|
|
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
|
|
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`
|
|
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` —
|
|
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'; //
|
|
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
|
```
|
|
@@ -158,7 +158,10 @@ defineBookConfig({ styles: [researchPortfolioStyle], ... });
|
|
|
158
158
|
defineBookConfig({ styles: [BUILTIN_STYLES['research-portfolio']], ... });
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
-
Each built-in style has a `name` matching its preset
|
|
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).
|
|
162
165
|
|
|
163
166
|
---
|
|
164
167
|
|
|
@@ -1,113 +1,59 @@
|
|
|
1
|
-
# Recipe 19 —
|
|
1
|
+
# Recipe 19 — generated data before validation (v4.27+)
|
|
2
2
|
|
|
3
|
-
`book-scaffold validate` checks
|
|
3
|
+
`book-scaffold validate` checks citations and cross-references against two derived, normally gitignored files:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- `src/data/references.json`, emitted by `book-scaffold build-bib`
|
|
6
|
+
- `src/data/labels.json`, emitted by `book-scaffold build-labels`
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## TL;DR
|
|
12
|
-
|
|
13
|
-
```json
|
|
14
|
-
{
|
|
15
|
-
"scripts": {
|
|
16
|
-
"prevalidate": "npm run build:bib && npm run build:labels --if-present",
|
|
17
|
-
"validate": "book-scaffold validate"
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
`npm run validate` → automatically runs `prevalidate` first (build:bib + build:labels) → then runs `validate`. CI and local behave identically; no separate `ci:validate` wrapper script needed.
|
|
23
|
-
|
|
24
|
-
---
|
|
25
|
-
|
|
26
|
-
## Why the workaround was needed
|
|
27
|
-
|
|
28
|
-
`brandon-behring/deploy-workflows@v1` (the reusable Cloudflare Workers deploy) runs `npm run <validate-command>` between `npm ci` and `npm run build`. Without the `prevalidate` hook OR an explicit wrapper script, the validate step ran against missing artifacts.
|
|
29
|
-
|
|
30
|
-
The Phase 1c first-deploy of `ssm-foundations` (2026-05-26) hit this — validate emitted 25+ "Unknown bibkey" errors that pointed at chapter content instead of the missing `references.json` artifact. The deploy-time fix shipped as a `ci:validate` wrapper:
|
|
8
|
+
Every profile can use theorem/Figure IDs and XRefs, so every generated book now carries the same npm lifecycle:
|
|
31
9
|
|
|
32
10
|
```json
|
|
33
11
|
{
|
|
34
12
|
"scripts": {
|
|
35
|
-
"
|
|
13
|
+
"prevalidate": "npm run build:bib --if-present && npm run build:labels --if-present",
|
|
14
|
+
"validate": "book-scaffold validate",
|
|
15
|
+
"prebuild": "npm run validate --if-present"
|
|
36
16
|
}
|
|
37
17
|
}
|
|
38
18
|
```
|
|
39
19
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
The cleaner long-term fix is the `prevalidate` npm-lifecycle hook (this recipe). v4.6.0's `create-book` automatically scaffolds it for academic + research-portfolio profiles.
|
|
43
|
-
|
|
44
|
-
---
|
|
20
|
+
`npm run validate` runs `prevalidate` automatically. `npm run build` runs `prebuild`, which delegates to that same path. Academic, tools, minimal, course-notes, and research-portfolio scaffolds use this identical contract.
|
|
45
21
|
|
|
46
|
-
##
|
|
22
|
+
## Direct CLI calls self-heal too
|
|
47
23
|
|
|
48
|
-
|
|
24
|
+
An npm hook cannot protect `npx book-scaffold validate` or a direct bin invocation. Starting in v4.27, the validator itself regenerates each missing artifact before loading it:
|
|
49
25
|
|
|
50
|
-
|
|
26
|
+
1. Missing `labels.json` invokes `build-labels`.
|
|
27
|
+
2. Missing `references.json` invokes `build-bib`, even when no default `bibliography.bib` exists; that valid no-bibliography case emits `{}`.
|
|
28
|
+
3. Existing files are not rewritten.
|
|
29
|
+
4. A child failure stops validation and preserves the original child diagnostic and non-zero status.
|
|
51
30
|
|
|
52
|
-
|
|
53
|
-
{
|
|
54
|
-
"scripts": {
|
|
55
|
-
- "ci:validate": "npm run build:bib && npm run build:labels --if-present && npm run validate",
|
|
56
|
-
+ "prevalidate": "npm run build:bib && npm run build:labels --if-present",
|
|
57
|
-
"validate": "book-scaffold validate"
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
```
|
|
31
|
+
This makes fresh checkouts deterministic without hiding stale existing data. After changing theorem IDs, kinds, slugs, bibliography entries, or `numberStyle`, explicitly rerun the corresponding build command.
|
|
61
32
|
|
|
62
|
-
|
|
33
|
+
## Bibliography path precedence
|
|
63
34
|
|
|
64
|
-
|
|
35
|
+
Books with a shared or non-root bibliography can set:
|
|
65
36
|
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
deploy:
|
|
69
|
-
uses: brandon-behring/deploy-workflows/.github/workflows/deploy-astro-worker.yml@v2
|
|
70
|
-
secrets: inherit
|
|
71
|
-
with:
|
|
72
|
-
working-directory: web
|
|
73
|
-
- validate-command: ci:validate
|
|
74
|
-
+ validate-command: validate
|
|
75
|
-
enable-pr-previews: true
|
|
37
|
+
```dotenv
|
|
38
|
+
BOOK_BIB_PATH=../shared/references.bib
|
|
76
39
|
```
|
|
77
40
|
|
|
78
|
-
|
|
41
|
+
`build-bib` resolves the process environment first, then the project-root `.env`, then `./bibliography.bib`. Relative paths are resolved from the book root. This same precedence applies when validation invokes `build-bib` during self-healing.
|
|
79
42
|
|
|
80
|
-
|
|
43
|
+
## Migrating older consumers
|
|
81
44
|
|
|
82
|
-
|
|
45
|
+
Replace custom `ci:validate` wrappers and profile-conditional hooks with the uniform scripts above. A reusable deploy workflow can call the normal `validate` script; no wrapper needs to repeat the artifact chain.
|
|
83
46
|
|
|
84
47
|
```diff
|
|
85
48
|
{
|
|
86
49
|
"scripts": {
|
|
87
|
-
- "
|
|
88
|
-
+ "
|
|
89
|
-
"
|
|
50
|
+
- "ci:validate": "npm run build:bib && npm run build:labels && npm run validate",
|
|
51
|
+
+ "prevalidate": "npm run build:bib --if-present && npm run build:labels --if-present",
|
|
52
|
+
"validate": "book-scaffold validate",
|
|
53
|
+
- "prebuild": "npm run build:bib && npm run build:labels && npm run validate"
|
|
54
|
+
+ "prebuild": "npm run validate --if-present"
|
|
90
55
|
}
|
|
91
56
|
}
|
|
92
57
|
```
|
|
93
58
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
---
|
|
97
|
-
|
|
98
|
-
## When `prevalidate` is NOT needed
|
|
99
|
-
|
|
100
|
-
Profiles that don't run cite-key or XRef validation don't need `prevalidate`. Specifically:
|
|
101
|
-
|
|
102
|
-
- `tools`, `minimal`: no `<Cite>` resolution against `references.json`.
|
|
103
|
-
- `course-notes`: depends on whether the consumer uses `<Cite>` (some do for source-tier attribution).
|
|
104
|
-
|
|
105
|
-
For these profiles, `book-scaffold validate` operates on chapter structure + XRef IDs only; `prevalidate` would be a no-op. The v4.6.0 create-book template adds `prevalidate` ONLY for academic + research-portfolio.
|
|
106
|
-
|
|
107
|
-
---
|
|
108
|
-
|
|
109
|
-
## Why this matters
|
|
110
|
-
|
|
111
|
-
Recipe 19 is part of [issue #76](https://github.com/brandon-behring/book-scaffold-astro/issues/76)'s v4.6 bundle. Companion: [recipe 18 — chapter-route ownership](./18-chapter-route-ownership.md), which fixes another silent-CI surface from the same Phase 1c first-deploys.
|
|
112
|
-
|
|
113
|
-
The `prevalidate` convention also closes [issue #77](https://github.com/brandon-behring/book-scaffold-astro/issues/77) — the v4.6 validator now emits a single re-framed error pointing at this recipe when `references.json` is missing, replacing the noisy 25-symptom output.
|
|
59
|
+
The historical profile split is intentionally gone: non-academic profiles still need labels, and an empty bibliography build is a supported no-op that creates the importable empty artifact.
|
package/scripts/build-bib.mjs
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
40
40
|
import { dirname, resolve } from 'node:path';
|
|
41
41
|
import { fileURLToPath } from 'node:url';
|
|
42
|
+
import { readEnvFile } from './read-env.mjs';
|
|
42
43
|
|
|
43
44
|
// --help / -h: non-mutating (closes #14).
|
|
44
45
|
const USAGE = `Usage: book-scaffold build-bib
|
|
@@ -49,7 +50,8 @@ AND sources/manifest.yaml -> src/data/sources.json (tools-profile sources for
|
|
|
49
50
|
the /references page). Either input may be absent.
|
|
50
51
|
|
|
51
52
|
Env:
|
|
52
|
-
BOOK_BIB_PATH Override path to .bib file (
|
|
53
|
+
BOOK_BIB_PATH Override path to .bib file (process env wins, then root
|
|
54
|
+
.env; default: ./bibliography.bib).
|
|
53
55
|
|
|
54
56
|
Options:
|
|
55
57
|
--help, -h Print this message and exit (non-mutating).
|
|
@@ -64,11 +66,13 @@ import '@citation-js/plugin-bibtex';
|
|
|
64
66
|
|
|
65
67
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
66
68
|
const PROJECT_ROOT = process.cwd();
|
|
69
|
+
const dotenv = readEnvFile(PROJECT_ROOT);
|
|
67
70
|
|
|
68
71
|
// Default: bibliography.bib at scaffold root.
|
|
69
72
|
// Override via BOOK_BIB_PATH=path/to/your.bib (absolute or relative to cwd).
|
|
70
|
-
const
|
|
71
|
-
|
|
73
|
+
const configuredBibPath = process.env.BOOK_BIB_PATH ?? dotenv.BOOK_BIB_PATH;
|
|
74
|
+
const BIB_PATH = configuredBibPath
|
|
75
|
+
? resolve(PROJECT_ROOT, configuredBibPath)
|
|
72
76
|
: resolve(PROJECT_ROOT, 'bibliography.bib');
|
|
73
77
|
const OUT_PATH = resolve(PROJECT_ROOT, 'src/data/references.json');
|
|
74
78
|
const SOURCES_PATH = resolve(PROJECT_ROOT, 'sources/manifest.yaml');
|