@brandon_m_behring/book-scaffold-astro 4.30.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +40 -3
- package/MIGRATION-v4-to-v5.md +183 -0
- package/README.md +30 -1
- package/bin/book-scaffold.mjs +1 -1
- package/components/AssessmentTest.astro +26 -5
- package/components/BookLink.astro +6 -3
- package/components/ChapterNav.astro +1 -1
- package/components/Cite.astro +37 -4
- package/components/ExerciseSolutions.astro +24 -4
- package/components/Figure.astro +7 -6
- package/components/Flashcards.tsx +19 -11
- package/components/NavContent.astro +60 -11
- package/components/ObjectiveMap.astro +14 -2
- package/components/PartReview.astro +32 -4
- package/components/PatternTimeline.astro +6 -2
- package/components/Rationale.astro +20 -3
- package/components/Sidebar.astro +11 -3
- package/components/SourceArchive.astro +33 -3
- package/components/Term.astro +18 -1
- package/components/Theorem.astro +15 -5
- package/components/TipsCard.astro +40 -3
- package/components/XRef.astro +14 -2
- package/dist/components/ExamRunner.d.ts +1 -1
- package/dist/components/Flashcards.d.ts +6 -1
- package/dist/components/Flashcards.mjs +14 -11
- package/dist/{exam-manifest-X9IrX1G3.d.ts → exam-manifest-DttY7kyZ.d.ts} +1 -1
- package/dist/index.d.ts +76 -8
- package/dist/index.mjs +517 -43
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.mjs +311 -32
- package/dist/{types-DgSlAew3.d.ts → types-D1QZgKMO.d.ts} +73 -21
- package/layouts/Base.astro +38 -9
- package/layouts/Chapter.astro +10 -2
- package/package.json +6 -2
- package/pages/answers.astro +25 -6
- package/pages/book.astro +75 -0
- package/pages/chapters/[...slug].astro +39 -7
- package/pages/chapters-book.astro +17 -0
- package/pages/chapters.astro +87 -9
- package/pages/convergence.astro +40 -10
- package/pages/corpus-apparatus/answers.astro +15 -0
- package/pages/corpus-apparatus/convergence.astro +15 -0
- package/pages/corpus-apparatus/exercises.astro +15 -0
- package/pages/corpus-apparatus/flashcards.astro +15 -0
- package/pages/corpus-apparatus/glossary.astro +15 -0
- package/pages/corpus-apparatus/practice-exam.astro +15 -0
- package/pages/corpus-apparatus/print.astro +15 -0
- package/pages/corpus-apparatus/references.astro +15 -0
- package/pages/corpus-apparatus/tips.astro +15 -0
- package/pages/exercises.astro +18 -5
- package/pages/flashcards.astro +26 -5
- package/pages/glossary.astro +20 -3
- package/pages/index.astro +54 -1
- package/pages/practice-exam.astro +8 -2
- package/pages/print.astro +7 -2
- package/pages/references.astro +26 -6
- package/pages/search.astro +65 -4
- package/pages/tips.astro +17 -4
- package/recipes/03-asset-pipelines.md +12 -4
- package/recipes/09-validation.md +1 -1
- package/recipes/15-defining-styles.md +6 -6
- package/recipes/16-tikz-figures.md +13 -2
- package/recipes/20-anki-export.md +15 -8
- package/recipes/21-multi-guide-single-app.md +293 -44
- package/recipes/22-responsive-nav-and-multibook-routing.md +7 -1
- package/recipes/24-figure-authoring-standard.md +241 -0
- package/recipes/README.md +3 -2
- package/scripts/build-bib.mjs +113 -35
- package/scripts/build-exercises.mjs +101 -24
- package/scripts/build-figures.mjs +13 -10
- package/scripts/build-labels.mjs +199 -194
- package/scripts/build-tips.mjs +95 -23
- package/scripts/corpus-tooling.mjs +268 -0
- package/scripts/render-notebooks.mjs +2 -1
- package/scripts/resolve-book-config.mjs +99 -1
- package/scripts/sync-figure-tokens.mjs +44 -0
- package/scripts/validate.mjs +676 -100
- package/scripts/walk-mdx.mjs +16 -4
- package/src/lib/book-link.ts +72 -0
- package/src/lib/chapters.ts +10 -4
- package/src/lib/corpus-collateral.ts +9 -0
- package/src/lib/corpus.ts +458 -0
- package/src/lib/define-style.ts +28 -15
- package/src/lib/exam-manifest.ts +4 -1
- package/src/lib/figure-palette.mjs +162 -0
- package/src/lib/figure.mjs +113 -37
- package/src/lib/patterns.ts +15 -6
- package/src/lib/questions.ts +8 -3
- package/src/types.ts +603 -0
- package/styles/tokens.css +73 -9
package/layouts/Base.astro
CHANGED
|
@@ -69,8 +69,12 @@ import NavContent from '../components/NavContent.astro';
|
|
|
69
69
|
// book-config virtual module (was landing-config in v4.5.1).
|
|
70
70
|
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
71
71
|
import { normalizeBase } from '../src/lib/nav-href';
|
|
72
|
+
import { resolveCorpusBook } from '../src/lib/corpus';
|
|
72
73
|
|
|
73
|
-
const profile = import.meta.env.BOOK_PROFILE
|
|
74
|
+
const profile = import.meta.env.BOOK_PROFILE;
|
|
75
|
+
if (!profile) {
|
|
76
|
+
throw new Error('book-scaffold runtime is missing the integration-defined BOOK_PROFILE.');
|
|
77
|
+
}
|
|
74
78
|
|
|
75
79
|
interface Props {
|
|
76
80
|
title: string;
|
|
@@ -86,6 +90,10 @@ interface Props {
|
|
|
86
90
|
ogImage?: string;
|
|
87
91
|
/** v4.6.0: Open Graph type. Defaults to 'website'; Chapter.astro passes 'article'. */
|
|
88
92
|
ogType?: string;
|
|
93
|
+
/** v5.0.0 (#80): resolved manifest book for metadata, search, and Pagefind. */
|
|
94
|
+
bookId?: string;
|
|
95
|
+
/** v5.0.0 (#80): non-book Pagefind surface such as the corpus landing. */
|
|
96
|
+
pagefindSurface?: string;
|
|
89
97
|
}
|
|
90
98
|
|
|
91
99
|
const {
|
|
@@ -96,8 +104,14 @@ const {
|
|
|
96
104
|
showChrome = true,
|
|
97
105
|
ogImage,
|
|
98
106
|
ogType = 'website',
|
|
107
|
+
bookId,
|
|
108
|
+
pagefindSurface,
|
|
99
109
|
} = Astro.props;
|
|
100
110
|
|
|
111
|
+
const corpusBook = bookId && bookConfig.corpus
|
|
112
|
+
? resolveCorpusBook(bookConfig.corpus, bookId)
|
|
113
|
+
: null;
|
|
114
|
+
|
|
101
115
|
// Automatic tools chrome (ToolFilter) renders only for non-academic
|
|
102
116
|
// profiles AND when the page opts in via showChrome (default true). A
|
|
103
117
|
// landing/hub page (no chapter filtering) passes showChrome={false} to
|
|
@@ -114,14 +128,22 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site).toString();
|
|
|
114
128
|
// per-page Astro.props.ogImage or book-level bookConfig.seo.ogImage. No
|
|
115
129
|
// '/og-default.png' fallback to avoid broken-link meta tags on consumers
|
|
116
130
|
// without an OG image authored.
|
|
117
|
-
const resolvedOgImage = ogImage ?? bookConfig.seo?.ogImage ?? null;
|
|
131
|
+
const resolvedOgImage = ogImage ?? corpusBook?.image ?? bookConfig.seo?.ogImage ?? null;
|
|
118
132
|
const absoluteOgImage = resolvedOgImage
|
|
119
133
|
? new URL(resolvedOgImage, Astro.site).toString()
|
|
120
134
|
: null;
|
|
121
135
|
const twitterHandle = bookConfig.seo?.twitterHandle ?? null;
|
|
122
|
-
const ogSiteName = bookConfig.title ?? title;
|
|
123
|
-
const ogDescription = description ?? bookConfig.description ?? '';
|
|
136
|
+
const ogSiteName = corpusBook?.title ?? bookConfig.title ?? title;
|
|
137
|
+
const ogDescription = description ?? corpusBook?.description ?? bookConfig.description ?? '';
|
|
124
138
|
const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
139
|
+
const searchHref = bookId
|
|
140
|
+
? `${baseUrl}search/?book=${encodeURIComponent(bookId)}`
|
|
141
|
+
: `${baseUrl}search/`;
|
|
142
|
+
const pagefindFilter = bookId
|
|
143
|
+
? `book:${bookId}`
|
|
144
|
+
: pagefindSurface
|
|
145
|
+
? `surface:${pagefindSurface}`
|
|
146
|
+
: undefined;
|
|
125
147
|
---
|
|
126
148
|
|
|
127
149
|
<!doctype html>
|
|
@@ -142,7 +164,7 @@ const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
|
142
164
|
<meta name="color-scheme" content="light dark" />
|
|
143
165
|
<meta name="generator" content={Astro.generator} />
|
|
144
166
|
<title>{title}</title>
|
|
145
|
-
{
|
|
167
|
+
{ogDescription && <meta name="description" content={ogDescription} />}
|
|
146
168
|
|
|
147
169
|
{/* v4.6.0: SEO meta-tag block (issue #76 Primary). canonical + og:* +
|
|
148
170
|
twitter:* on every page; article:* lives in Chapter.astro via the
|
|
@@ -150,7 +172,7 @@ const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
|
150
172
|
only when an image is explicitly set (per D3, avoids broken-link
|
|
151
173
|
OG tags). sitemap link points at @astrojs/sitemap's auto-emitted
|
|
152
174
|
index. */}
|
|
153
|
-
<link rel="canonical" href={canonicalURL} />
|
|
175
|
+
<link rel="canonical" href={canonicalURL} data-pagefind-meta="url[href]" />
|
|
154
176
|
<link rel="sitemap" type="application/xml" href={`${baseUrl}sitemap-index.xml`} />
|
|
155
177
|
<meta property="og:title" content={title} />
|
|
156
178
|
{ogDescription && <meta property="og:description" content={ogDescription} />}
|
|
@@ -207,7 +229,7 @@ const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
|
207
229
|
)}
|
|
208
230
|
{showToolsChrome && <ToolFilter client:idle />}
|
|
209
231
|
<a
|
|
210
|
-
href={
|
|
232
|
+
href={searchHref}
|
|
211
233
|
class="chrome-button"
|
|
212
234
|
aria-label="Search the book"
|
|
213
235
|
title="Search"
|
|
@@ -243,10 +265,17 @@ const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
|
243
265
|
{showSidebar ? (
|
|
244
266
|
<div class="layout-with-sidebar">
|
|
245
267
|
<Sidebar />
|
|
246
|
-
<main
|
|
268
|
+
<main
|
|
269
|
+
class="layout-main"
|
|
270
|
+
data-pagefind-body={pagefindFilter ? true : undefined}
|
|
271
|
+
data-pagefind-filter={pagefindFilter}
|
|
272
|
+
><slot /></main>
|
|
247
273
|
</div>
|
|
248
274
|
) : (
|
|
249
|
-
<main
|
|
275
|
+
<main
|
|
276
|
+
data-pagefind-body={pagefindFilter ? true : undefined}
|
|
277
|
+
data-pagefind-filter={pagefindFilter}
|
|
278
|
+
><slot /></main>
|
|
250
279
|
)}
|
|
251
280
|
{/*
|
|
252
281
|
Theme-change hook (#103, v4.14.2). Whenever the EFFECTIVE theme changes,
|
package/layouts/Chapter.astro
CHANGED
|
@@ -31,8 +31,10 @@ import ChapterNav from '../components/ChapterNav.astro';
|
|
|
31
31
|
interface Props {
|
|
32
32
|
entry: CollectionEntry<'chapters'>;
|
|
33
33
|
headings: MarkdownHeading[];
|
|
34
|
+
/** v5.0.0 (#80): manifest identity derived from the entry id. */
|
|
35
|
+
bookId?: string;
|
|
34
36
|
}
|
|
35
|
-
const { entry, headings } = Astro.props;
|
|
37
|
+
const { entry, headings, bookId } = Astro.props;
|
|
36
38
|
|
|
37
39
|
// v4.6.0: article:* meta-tag data. All fields are Zod-optional in the
|
|
38
40
|
// chapter schemas (academic / tools / course-notes / research-portfolio),
|
|
@@ -40,7 +42,12 @@ const { entry, headings } = Astro.props;
|
|
|
40
42
|
// (the top-level defineBookConfig field) so single-author books don't
|
|
41
43
|
// repeat themselves per chapter.
|
|
42
44
|
const articleAuthor =
|
|
43
|
-
(entry.data as { author?: string }).author ??
|
|
45
|
+
(entry.data as { author?: string }).author ??
|
|
46
|
+
(bookId && bookConfig.corpus
|
|
47
|
+
? bookConfig.corpus.books.find((book) => book.id === bookId)?.author
|
|
48
|
+
: undefined) ??
|
|
49
|
+
bookConfig.author ??
|
|
50
|
+
null;
|
|
44
51
|
const articlePublished = (entry.data as { published?: Date }).published;
|
|
45
52
|
const articleUpdated = (entry.data as { updated?: Date }).updated;
|
|
46
53
|
const articleTags = ((entry.data as { tags?: string[] }).tags ?? []) as string[];
|
|
@@ -85,6 +92,7 @@ const sectionMapLabel = labelParts.length > 0 ? labelParts.join(' · ') : undefi
|
|
|
85
92
|
description={entry.data.description}
|
|
86
93
|
ogType="article"
|
|
87
94
|
ogImage={chapterImage}
|
|
95
|
+
bookId={bookId}
|
|
88
96
|
>
|
|
89
97
|
<Fragment slot="head">
|
|
90
98
|
{articleAuthor && <meta property="article:author" content={articleAuthor} />}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brandon_m_behring/book-scaffold-astro",
|
|
3
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
|
+
"version": "5.0.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Brandon Behring",
|
|
@@ -156,6 +156,7 @@
|
|
|
156
156
|
"src/profiles",
|
|
157
157
|
"src/profile-kit.ts",
|
|
158
158
|
"src/schemas.ts",
|
|
159
|
+
"src/types.ts",
|
|
159
160
|
"components",
|
|
160
161
|
"styles",
|
|
161
162
|
"layouts",
|
|
@@ -170,10 +171,13 @@
|
|
|
170
171
|
"README.md",
|
|
171
172
|
"LICENSE",
|
|
172
173
|
"LICENSE-CONTENT",
|
|
173
|
-
"LATEX_TO_MDX_MAPPING.md"
|
|
174
|
+
"LATEX_TO_MDX_MAPPING.md",
|
|
175
|
+
"MIGRATION-v4-to-v5.md"
|
|
174
176
|
],
|
|
175
177
|
"scripts": {
|
|
176
178
|
"build": "tsup",
|
|
179
|
+
"sync:figure-tokens": "node scripts/sync-figure-tokens.mjs --write",
|
|
180
|
+
"check:figure-tokens": "node scripts/sync-figure-tokens.mjs --check",
|
|
177
181
|
"check:types": "tsc -p tests/types/tsconfig.json",
|
|
178
182
|
"prepublishOnly": "npm run build && npm run check:types",
|
|
179
183
|
"test": "node --test tests/*.test.mjs"
|
package/pages/answers.astro
CHANGED
|
@@ -27,6 +27,10 @@ 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
29
|
import { normalizeBase } from '../src/lib/nav-href';
|
|
30
|
+
import { corpusBookHasApparatusRoute } from '../src/lib/corpus';
|
|
31
|
+
|
|
32
|
+
interface Props { bookId?: string; }
|
|
33
|
+
const { bookId } = Astro.props;
|
|
30
34
|
|
|
31
35
|
// Presence-gate: only touch the collection when the directory holds files.
|
|
32
36
|
const questionModules = import.meta.glob('/src/content/questions/**/*.{md,mdx}', {
|
|
@@ -34,9 +38,11 @@ const questionModules = import.meta.glob('/src/content/questions/**/*.{md,mdx}',
|
|
|
34
38
|
import: 'default',
|
|
35
39
|
eager: true,
|
|
36
40
|
});
|
|
37
|
-
const hasQuestions =
|
|
41
|
+
const hasQuestions = bookId
|
|
42
|
+
? Object.keys(questionModules).some((path) => path.includes(`/questions/${bookId}/`))
|
|
43
|
+
: Object.keys(questionModules).length > 0;
|
|
38
44
|
|
|
39
|
-
const entries = hasQuestions ? await getAllQuestions() : [];
|
|
45
|
+
const entries = hasQuestions ? await getAllQuestions(bookId) : [];
|
|
40
46
|
for (const entry of entries) {
|
|
41
47
|
assertKnownDomain(bookConfig.examDomains, entry.data.domain, { id: entry.data.id });
|
|
42
48
|
}
|
|
@@ -53,18 +59,30 @@ const byChapter = groupByChapter(rendered);
|
|
|
53
59
|
const total = rendered.length;
|
|
54
60
|
// #142: practice-exam + chapter links must prefix the deploy base.
|
|
55
61
|
const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
56
|
-
const
|
|
57
|
-
|
|
62
|
+
const bookRoutePrefix = bookId ? `${bookId}/` : '';
|
|
63
|
+
const hasPracticeExam = bookId && bookConfig.corpus
|
|
64
|
+
? corpusBookHasApparatusRoute(
|
|
65
|
+
bookConfig.corpus,
|
|
66
|
+
bookId,
|
|
67
|
+
'practice-exam',
|
|
68
|
+
bookConfig.apparatusRoutes,
|
|
69
|
+
)
|
|
70
|
+
: (bookConfig.enabledRoutes ?? []).includes('practiceExam');
|
|
71
|
+
const practiceHref = hasPracticeExam
|
|
72
|
+
? `${baseUrl}${bookRoutePrefix}practice-exam`
|
|
58
73
|
: null;
|
|
59
74
|
|
|
60
75
|
/** A string chapter ref is a slug (schema kebab-case branch) → linkable. */
|
|
61
76
|
function chapterHref(chapter: number | string): string | null {
|
|
62
|
-
return typeof chapter === 'string'
|
|
77
|
+
return typeof chapter === 'string'
|
|
78
|
+
? `${baseUrl}chapters/${bookRoutePrefix}${chapter}/`
|
|
79
|
+
: null;
|
|
63
80
|
}
|
|
64
81
|
---
|
|
65
82
|
<Base
|
|
66
83
|
title="Answers & rationales"
|
|
67
84
|
description="Answer appendix: every practice question with its correct answer and full rationale, grouped by chapter."
|
|
85
|
+
bookId={bookId}
|
|
68
86
|
>
|
|
69
87
|
<article class="prose answers-appendix">
|
|
70
88
|
<h1>Answers & rationales</h1>
|
|
@@ -87,7 +105,8 @@ function chapterHref(chapter: number | string): string | null {
|
|
|
87
105
|
)}
|
|
88
106
|
|
|
89
107
|
{[...byChapter.entries()].map(([chapterKey, qs]) => {
|
|
90
|
-
const
|
|
108
|
+
const firstQuestion = qs[0];
|
|
109
|
+
const href = firstQuestion ? chapterHref(firstQuestion.data.chapter) : null;
|
|
91
110
|
return (
|
|
92
111
|
<section class="answers-chapter" id={`chapter-${chapterKey}`}>
|
|
93
112
|
<h2 class="answers-chapter-title">
|
package/pages/book.astro
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** v5.0.0 (#80): manifest-backed per-book landing page. */
|
|
3
|
+
import Base from '../layouts/Base.astro';
|
|
4
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
5
|
+
import { apparatusHref, normalizeBase } from '../src/lib/nav-href';
|
|
6
|
+
|
|
7
|
+
export function getStaticPaths() {
|
|
8
|
+
if (!bookConfig.corpus) return [];
|
|
9
|
+
return bookConfig.corpus.books.map((book) => ({
|
|
10
|
+
params: { book: book.id },
|
|
11
|
+
props: { book },
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { book } = Astro.props;
|
|
16
|
+
const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
17
|
+
const globalApparatus = bookConfig.apparatusRoutes ?? [];
|
|
18
|
+
const routes = book.apparatus ?? globalApparatus;
|
|
19
|
+
const routeLabels: Record<string, string> = {
|
|
20
|
+
references: 'References',
|
|
21
|
+
print: 'Print view',
|
|
22
|
+
convergence: 'Convergence',
|
|
23
|
+
tips: 'Tips',
|
|
24
|
+
exercises: 'Exercises',
|
|
25
|
+
'practice-exam': 'Practice exam',
|
|
26
|
+
glossary: 'Glossary',
|
|
27
|
+
flashcards: 'Flashcards',
|
|
28
|
+
answers: 'Answers',
|
|
29
|
+
};
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
<Base
|
|
33
|
+
title={book.title}
|
|
34
|
+
description={book.description}
|
|
35
|
+
ogImage={book.image}
|
|
36
|
+
bookId={book.id}
|
|
37
|
+
showChrome={false}
|
|
38
|
+
>
|
|
39
|
+
<article class="prose book-landing">
|
|
40
|
+
<h1>{book.title}</h1>
|
|
41
|
+
{book.subtitle && <p class="book-subtitle">{book.subtitle}</p>}
|
|
42
|
+
{book.description && <p class="lead">{book.description}</p>}
|
|
43
|
+
|
|
44
|
+
<h2>Read</h2>
|
|
45
|
+
<ul>
|
|
46
|
+
<li><a href={`${baseUrl}chapters/${book.id}/`}>Chapters</a></li>
|
|
47
|
+
{routes.map((route: string) => (
|
|
48
|
+
<li>
|
|
49
|
+
<a href={apparatusHref(route, book.id, '/:book/:route/', baseUrl)}>
|
|
50
|
+
{routeLabels[route] ?? route.replaceAll('-', ' ')}
|
|
51
|
+
</a>
|
|
52
|
+
</li>
|
|
53
|
+
))}
|
|
54
|
+
</ul>
|
|
55
|
+
|
|
56
|
+
{bookConfig.corpus && bookConfig.corpus.books.length > 1 && (
|
|
57
|
+
<p><a href={baseUrl}>← All books</a></p>
|
|
58
|
+
)}
|
|
59
|
+
</article>
|
|
60
|
+
</Base>
|
|
61
|
+
|
|
62
|
+
<style>
|
|
63
|
+
.book-landing {
|
|
64
|
+
max-width: 65ch;
|
|
65
|
+
margin: 2rem auto;
|
|
66
|
+
}
|
|
67
|
+
.book-subtitle {
|
|
68
|
+
color: var(--color-text-muted);
|
|
69
|
+
font-family: var(--font-sans);
|
|
70
|
+
font-size: var(--text-lg);
|
|
71
|
+
}
|
|
72
|
+
.lead {
|
|
73
|
+
font-size: var(--text-lg);
|
|
74
|
+
}
|
|
75
|
+
</style>
|
|
@@ -21,21 +21,53 @@ import type { Provenance } from '../../src/schemas.js';
|
|
|
21
21
|
import Chapter from '../../layouts/Chapter.astro';
|
|
22
22
|
import Base from '../../layouts/Base.astro';
|
|
23
23
|
import ProvenanceBlock from '../../components/Provenance.astro';
|
|
24
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
25
|
+
import { corpusBookIdOf, resolveCorpusBook } from '../../src/lib/corpus';
|
|
24
26
|
|
|
25
|
-
const BOOK_PRESET = import.meta.env.BOOK_PRESET
|
|
27
|
+
const BOOK_PRESET = import.meta.env.BOOK_PRESET;
|
|
28
|
+
if (!BOOK_PRESET) {
|
|
29
|
+
throw new Error('book-scaffold runtime is missing the integration-defined BOOK_PRESET.');
|
|
30
|
+
}
|
|
26
31
|
const USE_CHAPTER_LAYOUT = ['academic', 'research-portfolio'].includes(BOOK_PRESET);
|
|
27
32
|
|
|
28
33
|
export async function getStaticPaths() {
|
|
29
34
|
const chapters = await getCollection('chapters', (entry) => !entry.data.draft);
|
|
30
|
-
return chapters.map((entry) =>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
return chapters.map((entry) => {
|
|
36
|
+
if (!bookConfig.corpus) {
|
|
37
|
+
return {
|
|
38
|
+
params: { slug: entry.id },
|
|
39
|
+
props: { entry, book: null },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const bookId = corpusBookIdOf(bookConfig.corpus, entry.id);
|
|
43
|
+
if (!bookId) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Chapter entry ${JSON.stringify(entry.id)} does not start with a registered corpus book id.`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const slug = entry.id.slice(bookId.length + 1);
|
|
49
|
+
if (!slug) {
|
|
50
|
+
throw new Error(`Chapter entry ${JSON.stringify(entry.id)} has no book-local slug.`);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
params: { book: bookId, slug },
|
|
54
|
+
props: { entry, book: resolveCorpusBook(bookConfig.corpus, bookId) },
|
|
55
|
+
};
|
|
56
|
+
});
|
|
34
57
|
}
|
|
35
58
|
|
|
36
|
-
const { entry } = Astro.props;
|
|
59
|
+
const { entry, book } = Astro.props;
|
|
37
60
|
const { Content, headings } = await render(entry);
|
|
38
61
|
const Layout = USE_CHAPTER_LAYOUT ? Chapter : Base;
|
|
62
|
+
const layoutProps = USE_CHAPTER_LAYOUT
|
|
63
|
+
? { entry, headings, bookId: book?.id }
|
|
64
|
+
: {
|
|
65
|
+
title: entry.data.title,
|
|
66
|
+
description: entry.data.description,
|
|
67
|
+
ogImage: (entry.data as { image?: string }).image,
|
|
68
|
+
ogType: 'article',
|
|
69
|
+
bookId: book?.id,
|
|
70
|
+
};
|
|
39
71
|
|
|
40
72
|
// v4.8.0: per-chapter provenance audit trail. Rendered here (not in a single
|
|
41
73
|
// layout) so it reaches BOTH layout paths — Chapter.astro (academic/
|
|
@@ -43,7 +75,7 @@ const Layout = USE_CHAPTER_LAYOUT ? Chapter : Base;
|
|
|
43
75
|
// always rendered; absent `provenance` → the component's fallback.
|
|
44
76
|
const provenance = (entry.data as { provenance?: Provenance }).provenance ?? null;
|
|
45
77
|
---
|
|
46
|
-
<Layout
|
|
78
|
+
<Layout {...layoutProps}>
|
|
47
79
|
<Content />
|
|
48
80
|
<ProvenanceBlock data={provenance} />
|
|
49
81
|
</Layout>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** v5.0.0 (#80): one rich chapter index per registered corpus book. */
|
|
3
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
4
|
+
import ChaptersIndex from './chapters.astro';
|
|
5
|
+
|
|
6
|
+
export function getStaticPaths() {
|
|
7
|
+
if (!bookConfig.corpus) return [];
|
|
8
|
+
return bookConfig.corpus.books.map((book) => ({
|
|
9
|
+
params: { book: book.id },
|
|
10
|
+
props: { bookId: book.id },
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { bookId } = Astro.props;
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<ChaptersIndex bookId={bookId} />
|
package/pages/chapters.astro
CHANGED
|
@@ -27,17 +27,39 @@ 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
29
|
import { normalizeBase } from '../src/lib/nav-href';
|
|
30
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
31
|
+
import { resolveCorpusBook } from '../src/lib/corpus';
|
|
32
|
+
|
|
33
|
+
interface Props {
|
|
34
|
+
/** v5.0.0 (#80): supplied by the injected /chapters/[book] wrapper. */
|
|
35
|
+
bookId?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { bookId } = Astro.props;
|
|
39
|
+
const currentBook = bookId && bookConfig.corpus
|
|
40
|
+
? resolveCorpusBook(bookConfig.corpus, bookId)
|
|
41
|
+
: null;
|
|
42
|
+
const isCorpusRoot = Boolean(bookConfig.corpus && !currentBook);
|
|
30
43
|
|
|
31
44
|
// #142: prefix BASE_URL so chapter-card links stay inside a non-root deploy base.
|
|
32
45
|
const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
33
46
|
|
|
34
|
-
const
|
|
47
|
+
const configuredProfile = import.meta.env.BOOK_PROFILE;
|
|
48
|
+
if (!configuredProfile) {
|
|
49
|
+
throw new Error('book-scaffold runtime is missing the integration-defined BOOK_PROFILE.');
|
|
50
|
+
}
|
|
51
|
+
const profileName = configuredProfile as keyof typeof PROFILES;
|
|
35
52
|
const profileDef = PROFILES[profileName];
|
|
36
53
|
const renderer: ChaptersRenderer =
|
|
37
54
|
(profileDef as { chaptersRenderer?: ChaptersRenderer } | undefined)?.chaptersRenderer
|
|
38
55
|
?? fallbackChaptersRenderer;
|
|
39
56
|
|
|
40
|
-
const
|
|
57
|
+
const allChapters = await getAllChapters();
|
|
58
|
+
const chapters = currentBook
|
|
59
|
+
? allChapters.filter((chapter) => chapter.id.startsWith(`${currentBook.id}/`))
|
|
60
|
+
: isCorpusRoot
|
|
61
|
+
? []
|
|
62
|
+
: allChapters;
|
|
41
63
|
|
|
42
64
|
// Precompute each card's render-ready fields in the frontmatter block.
|
|
43
65
|
// This keeps TypeScript generics (Record<string, unknown> casts, the
|
|
@@ -94,21 +116,45 @@ for (const c of chapters) {
|
|
|
94
116
|
}
|
|
95
117
|
---
|
|
96
118
|
<Base
|
|
97
|
-
title=
|
|
98
|
-
description=
|
|
119
|
+
title={currentBook ? `${currentBook.title} · Chapters` : 'Chapters'}
|
|
120
|
+
description={
|
|
121
|
+
currentBook?.description ??
|
|
122
|
+
(isCorpusRoot ? 'All books in this corpus.' : 'All chapters grouped by Part.')
|
|
123
|
+
}
|
|
124
|
+
bookId={currentBook?.id}
|
|
125
|
+
showSidebar={!isCorpusRoot}
|
|
126
|
+
pagefindSurface={isCorpusRoot ? 'corpus' : undefined}
|
|
99
127
|
>
|
|
100
128
|
<article class="prose chapters-index">
|
|
101
129
|
<header class="chapters-index-header">
|
|
102
|
-
<h1>Chapters</h1>
|
|
130
|
+
<h1>{currentBook ? `${currentBook.title} chapters` : 'Chapters'}</h1>
|
|
103
131
|
<p class="chapters-index-lede">
|
|
104
|
-
|
|
105
|
-
|
|
132
|
+
{isCorpusRoot
|
|
133
|
+
? 'Choose a book. Each book keeps its own chapter order, references, and readiness state.'
|
|
134
|
+
: 'Every chapter, grouped by Part. Use the card metadata to calibrate how much trust to place in a chapter’s specific claims.'}
|
|
106
135
|
</p>
|
|
107
136
|
</header>
|
|
108
137
|
|
|
109
|
-
|
|
138
|
+
{isCorpusRoot && bookConfig.corpus ? (
|
|
139
|
+
<ol class="corpus-book-list">
|
|
140
|
+
{bookConfig.corpus.books.map((book) => {
|
|
141
|
+
const count = allChapters.filter((chapter) => chapter.id.startsWith(`${book.id}/`)).length;
|
|
142
|
+
return (
|
|
143
|
+
<li class="corpus-book-card">
|
|
144
|
+
<a href={`${baseUrl}chapters/${book.id}/`}>
|
|
145
|
+
<h2>{book.title}</h2>
|
|
146
|
+
{book.description && <p>{book.description}</p>}
|
|
147
|
+
<span>{count} {count === 1 ? 'chapter' : 'chapters'}</span>
|
|
148
|
+
</a>
|
|
149
|
+
</li>
|
|
150
|
+
);
|
|
151
|
+
})}
|
|
152
|
+
</ol>
|
|
153
|
+
) : (
|
|
154
|
+
<p class="chapters-filter-hint" id="filter-hint" aria-live="polite"></p>
|
|
155
|
+
)}
|
|
110
156
|
|
|
111
|
-
{Array.from(byPart.entries()).map(([part, cards]) => (
|
|
157
|
+
{!isCorpusRoot && Array.from(byPart.entries()).map(([part, cards]) => (
|
|
112
158
|
<section class="part-group">
|
|
113
159
|
<h2 class="part-heading">
|
|
114
160
|
<span class="part-label">{renderer.formatPartLabel(part)}</span>
|
|
@@ -248,6 +294,38 @@ for (const c of chapters) {
|
|
|
248
294
|
</script>
|
|
249
295
|
|
|
250
296
|
<style>
|
|
297
|
+
.corpus-book-list {
|
|
298
|
+
display: grid;
|
|
299
|
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
|
|
300
|
+
gap: var(--space-5);
|
|
301
|
+
list-style: none;
|
|
302
|
+
padding: 0;
|
|
303
|
+
}
|
|
304
|
+
.corpus-book-card {
|
|
305
|
+
margin: 0;
|
|
306
|
+
}
|
|
307
|
+
.corpus-book-card > a {
|
|
308
|
+
display: block;
|
|
309
|
+
height: 100%;
|
|
310
|
+
padding: var(--space-5);
|
|
311
|
+
border: 1px solid var(--color-border);
|
|
312
|
+
border-radius: var(--radius-md);
|
|
313
|
+
color: inherit;
|
|
314
|
+
text-decoration: none;
|
|
315
|
+
background: var(--color-bg-subtle);
|
|
316
|
+
}
|
|
317
|
+
.corpus-book-card > a:hover,
|
|
318
|
+
.corpus-book-card > a:focus-visible {
|
|
319
|
+
border-color: var(--color-link);
|
|
320
|
+
}
|
|
321
|
+
.corpus-book-card h2 {
|
|
322
|
+
margin-top: 0;
|
|
323
|
+
}
|
|
324
|
+
.corpus-book-card span {
|
|
325
|
+
color: var(--color-text-muted);
|
|
326
|
+
font-family: var(--font-code);
|
|
327
|
+
font-size: var(--text-sm);
|
|
328
|
+
}
|
|
251
329
|
.chapters-index-header {
|
|
252
330
|
margin-bottom: var(--space-8);
|
|
253
331
|
border-bottom: 1px solid var(--color-border);
|
package/pages/convergence.astro
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* placeholder so readers see the coverage gaps rather than inferring
|
|
8
8
|
* the registry is exhaustive.
|
|
9
9
|
*
|
|
10
|
-
* Driven
|
|
11
|
-
*
|
|
10
|
+
* Driven from changelog/patterns.yaml + changelog/tools/*.yaml in a
|
|
11
|
+
* single-book app, or changelog/<book>/ in corpus mode. No code change is
|
|
12
|
+
* needed to grow the dashboard — add manifest rows.
|
|
12
13
|
*/
|
|
13
14
|
import Base from '../layouts/Base.astro';
|
|
14
15
|
import PatternTimeline from '../components/PatternTimeline.astro';
|
|
@@ -19,6 +20,9 @@ import {
|
|
|
19
20
|
} from '../src/lib/patterns';
|
|
20
21
|
import { patternCategories } from '@brandon_m_behring/book-scaffold-astro';
|
|
21
22
|
|
|
23
|
+
interface Props { bookId?: string; }
|
|
24
|
+
const { bookId } = Astro.props;
|
|
25
|
+
|
|
22
26
|
// A tools-profile book may use the inline <Convergence> component yet define
|
|
23
27
|
// no `changelog/patterns.yaml` — then the `patterns` collection is never
|
|
24
28
|
// registered and getCollection('patterns') errors with "The collection
|
|
@@ -27,14 +31,37 @@ import { patternCategories } from '@brandon_m_behring/book-scaffold-astro';
|
|
|
27
31
|
// presence-gate references.astro uses for the optional `sources` collection.
|
|
28
32
|
// When absent, render the dashboard's honest empty-state from a pure
|
|
29
33
|
// all-empty map (no content-collection access).
|
|
30
|
-
const
|
|
34
|
+
const rootPatternsManifest = import.meta.glob('/changelog/patterns.yaml', {
|
|
35
|
+
query: '?raw',
|
|
36
|
+
import: 'default',
|
|
37
|
+
eager: true,
|
|
38
|
+
});
|
|
39
|
+
const corpusPatternManifests = import.meta.glob('/changelog/*/patterns.yaml', {
|
|
40
|
+
query: '?raw',
|
|
41
|
+
import: 'default',
|
|
42
|
+
eager: true,
|
|
43
|
+
});
|
|
44
|
+
const rootToolFiles = import.meta.glob('/changelog/tools/*.yaml', {
|
|
45
|
+
query: '?raw',
|
|
46
|
+
import: 'default',
|
|
47
|
+
eager: true,
|
|
48
|
+
});
|
|
49
|
+
const corpusToolFiles = import.meta.glob('/changelog/*/tools/*.yaml', {
|
|
31
50
|
query: '?raw',
|
|
32
51
|
import: 'default',
|
|
33
52
|
eager: true,
|
|
34
53
|
});
|
|
35
|
-
const
|
|
54
|
+
const patternsPath = bookId
|
|
55
|
+
? `/changelog/${bookId}/patterns.yaml`
|
|
56
|
+
: '/changelog/patterns.yaml';
|
|
57
|
+
const hasPatterns = bookId
|
|
58
|
+
? patternsPath in corpusPatternManifests
|
|
59
|
+
: patternsPath in rootPatternsManifest;
|
|
60
|
+
const hasChangelog = bookId
|
|
61
|
+
? Object.keys(corpusToolFiles).some((path) => path.startsWith(`/changelog/${bookId}/tools/`))
|
|
62
|
+
: Object.keys(rootToolFiles).length > 0;
|
|
36
63
|
const grouped = hasPatterns
|
|
37
|
-
? await getPatternsByCategory()
|
|
64
|
+
? await getPatternsByCategory(bookId)
|
|
38
65
|
: emptyPatternsByCategory();
|
|
39
66
|
const totalPatterns = Object.values(grouped).reduce(
|
|
40
67
|
(n, arr) => n + arr.length,
|
|
@@ -46,11 +73,12 @@ const totalPatterns = Object.values(grouped).reduce(
|
|
|
46
73
|
// Surface an actionable hint instead — matching tips.astro / references.astro.
|
|
47
74
|
const noManifestHint = hasPatterns
|
|
48
75
|
? null
|
|
49
|
-
:
|
|
76
|
+
: `No ${patternsPath.slice(1)} found — create it to populate this dashboard, or remove convergence from this book's apparatus.`;
|
|
50
77
|
---
|
|
51
78
|
<Base
|
|
52
79
|
title="Convergence — Agentic Coding"
|
|
53
80
|
description="Which agentic-coding patterns have converged across Claude Code, Gemini CLI, and Codex CLI — and when? A live timeline driven from the changelog manifest."
|
|
81
|
+
bookId={bookId}
|
|
54
82
|
>
|
|
55
83
|
<article class="prose convergence-dashboard">
|
|
56
84
|
<header class="convergence-header">
|
|
@@ -91,7 +119,9 @@ const noManifestHint = hasPatterns
|
|
|
91
119
|
</p>
|
|
92
120
|
) : (
|
|
93
121
|
<div class="convergence-card-list">
|
|
94
|
-
{patterns.map((p) =>
|
|
122
|
+
{patterns.map((p) => (
|
|
123
|
+
<PatternTimeline pattern={p} bookId={bookId} hasChangelog={hasChangelog} />
|
|
124
|
+
))}
|
|
95
125
|
</div>
|
|
96
126
|
)}
|
|
97
127
|
</section>
|
|
@@ -101,9 +131,9 @@ const noManifestHint = hasPatterns
|
|
|
101
131
|
<section class="convergence-method">
|
|
102
132
|
<h2>How to read this</h2>
|
|
103
133
|
<p>
|
|
104
|
-
The registry lives at <code>
|
|
105
|
-
Each tool's adoption timeline lives
|
|
106
|
-
<code>changelog/tools
|
|
134
|
+
The registry lives at <code>{patternsPath.slice(1)}</code>.
|
|
135
|
+
Each tool's adoption timeline lives beside it under
|
|
136
|
+
<code>{bookId ? `changelog/${bookId}/tools/` : 'changelog/tools/'}</code>. The dashboard
|
|
107
137
|
joins them at build time — no code edits needed to add new
|
|
108
138
|
patterns or new adoption events.
|
|
109
139
|
</p>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
3
|
+
import { corpusBookHasApparatusRoute } from '../../src/lib/corpus';
|
|
4
|
+
import AnswersPage from '../answers.astro';
|
|
5
|
+
|
|
6
|
+
export function getStaticPaths() {
|
|
7
|
+
if (!bookConfig.corpus) return [];
|
|
8
|
+
return bookConfig.corpus.books
|
|
9
|
+
.filter((book) => corpusBookHasApparatusRoute(bookConfig.corpus!, book.id, 'answers', bookConfig.apparatusRoutes))
|
|
10
|
+
.map((book) => ({ params: { book: book.id }, props: { bookId: book.id } }));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { bookId } = Astro.props;
|
|
14
|
+
---
|
|
15
|
+
<AnswersPage bookId={bookId} />
|