@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.
- 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 +96 -8
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.mjs +8 -1
- package/dist/{types-Hue-uSeQ.d.ts → types-CZrkqzpC.d.ts} +84 -48
- package/layouts/Base.astro +23 -21
- 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 +7 -3
- 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 +25 -8
- package/src/lib/nav-href.ts +24 -2
- package/styles/layout.css +14 -8
- 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
|
|
4
|
-
"version": "4.
|
|
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
|
-
"
|
|
166
|
-
"
|
|
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": {
|
package/pages/answers.astro
CHANGED
|
@@ -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
|
|
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;
|
package/pages/chapters.astro
CHANGED
|
@@ -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
|
|
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];
|
package/pages/exercises.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 (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
|
|
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
|
|
44
|
-
//
|
|
45
|
-
const baseUrl = (import.meta.env.BASE_URL
|
|
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
|
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
|
```
|
|
@@ -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
|
|
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
|
|