@brandon_m_behring/book-scaffold-astro 4.25.0 → 4.25.2
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/components/AssessmentTest.astro +6 -2
- package/components/ChapterNav.astro +1 -1
- package/components/Cite.astro +1 -1
- package/components/PartReview.astro +1 -1
- package/components/Sidebar.astro +1 -1
- package/components/Term.astro +1 -1
- package/components/TipsCard.astro +1 -1
- package/components/WeekRef.astro +3 -1
- package/components/XRef.astro +5 -1
- package/dist/components/ExamRunner.d.ts +1 -1
- package/dist/{exam-manifest-DnqyzZ9I.d.ts → exam-manifest-X9IrX1G3.d.ts} +7 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +3 -2
- package/layouts/Base.astro +20 -4
- package/package.json +1 -1
- package/pages/answers.astro +4 -2
- package/pages/chapters.astro +4 -1
- package/pages/exercises.astro +5 -2
- package/pages/index.astro +11 -7
- package/pages/search.astro +6 -2
- package/pages/tips.astro +4 -1
- package/recipes/04-component-library.md +1 -1
- package/recipes/20-anki-export.md +1 -1
- package/recipes/21-multi-guide-single-app.md +4 -4
- package/recipes/README.md +1 -1
- package/scripts/build-labels.mjs +3 -1
- package/src/lib/exam-manifest.ts +11 -2
- package/styles/chapter.css +15 -0
|
@@ -80,10 +80,14 @@ for (const e of entries) {
|
|
|
80
80
|
const scoreable = entries.filter(
|
|
81
81
|
(e) => e.data.type === 'mcq' && (e.data.options?.length ?? 0) > 0,
|
|
82
82
|
);
|
|
83
|
+
// #142: thread the deploy base through the routing map + practice-exam link.
|
|
84
|
+
// deriveDomainRouting takes baseUrl as a param (not import.meta.env) because it
|
|
85
|
+
// ships pre-compiled in dist/ where Vite's env replacement does not reach.
|
|
86
|
+
const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
|
|
83
87
|
const manifest = buildExamManifest(entries);
|
|
84
|
-
const domainRouting = deriveDomainRouting(entries);
|
|
88
|
+
const domainRouting = deriveDomainRouting(entries, baseUrl);
|
|
85
89
|
const practiceExamHref = (bookConfig.enabledRoutes ?? []).includes('practiceExam')
|
|
86
|
-
?
|
|
90
|
+
? `${baseUrl}practice-exam`
|
|
87
91
|
: null;
|
|
88
92
|
|
|
89
93
|
const rendered = await Promise.all(
|
|
@@ -10,7 +10,7 @@ interface Props {
|
|
|
10
10
|
}
|
|
11
11
|
const { currentId } = Astro.props;
|
|
12
12
|
const { prev, next } = await getNeighbors(currentId);
|
|
13
|
-
const baseUrl = import.meta.env.BASE_URL ?? '/';
|
|
13
|
+
const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
|
|
14
14
|
---
|
|
15
15
|
{(prev || next) && (
|
|
16
16
|
<nav class="chapter-nav" aria-label="Chapter navigation">
|
package/components/Cite.astro
CHANGED
|
@@ -27,7 +27,7 @@ interface Props {
|
|
|
27
27
|
title?: string;
|
|
28
28
|
}
|
|
29
29
|
const { part, title } = Astro.props;
|
|
30
|
-
const baseUrl = import.meta.env.BASE_URL ?? '/';
|
|
30
|
+
const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
|
|
31
31
|
|
|
32
32
|
// Reuse the build-exercises index (keyed by chapter slug). Project-root-relative
|
|
33
33
|
// glob (the tips.astro / exercises.astro lesson) so it resolves across consumers.
|
package/components/Sidebar.astro
CHANGED
|
@@ -28,7 +28,7 @@ import { academicPartHeading } from '../src/lib/academic-parts';
|
|
|
28
28
|
const profile = import.meta.env.BOOK_PROFILE ?? 'minimal';
|
|
29
29
|
const siteTitle = bookConfig.title ?? 'Book';
|
|
30
30
|
const siteSubtitle = bookConfig.subtitle ?? 'A scaffold-astro book';
|
|
31
|
-
const baseUrl = import.meta.env.BASE_URL ?? '/';
|
|
31
|
+
const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
|
|
32
32
|
|
|
33
33
|
// Academic profile: part is a string enum. `academicParts` (schemas.ts) is
|
|
34
34
|
// the canonical order, shared with the renderer and ChapterHeader (#95); the
|
package/components/Term.astro
CHANGED
|
@@ -17,7 +17,7 @@ interface Props {
|
|
|
17
17
|
id: string;
|
|
18
18
|
}
|
|
19
19
|
const { id } = Astro.props;
|
|
20
|
-
const baseUrl = import.meta.env.BASE_URL ?? '/';
|
|
20
|
+
const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
|
|
21
21
|
---
|
|
22
22
|
<a href={`${baseUrl}glossary#term-${id}`} class="term-link"><slot /></a>
|
|
23
23
|
|
|
@@ -20,7 +20,7 @@ const tipsModules = import.meta.glob<{ default: Array<{ n: number; title: string
|
|
|
20
20
|
{ eager: true },
|
|
21
21
|
);
|
|
22
22
|
const tips = tipsModules['/src/data/tips.json']?.default ?? [];
|
|
23
|
-
const baseUrl = import.meta.env.BASE_URL ?? '/';
|
|
23
|
+
const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
|
|
24
24
|
---
|
|
25
25
|
<aside class="tips-card" role="contentinfo">
|
|
26
26
|
<h2 class="tips-card-title">Tips</h2>
|
package/components/WeekRef.astro
CHANGED
|
@@ -20,7 +20,9 @@ interface Props {
|
|
|
20
20
|
}
|
|
21
21
|
const { week } = Astro.props;
|
|
22
22
|
const slug = `week${String(week).padStart(2, '0')}`;
|
|
23
|
-
|
|
23
|
+
// #142: prefix BASE_URL so the chapter link stays inside a non-root deploy base.
|
|
24
|
+
const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
|
|
25
|
+
const href = `${baseUrl}chapters/${slug}/`;
|
|
24
26
|
---
|
|
25
27
|
<aside class="week-ref">
|
|
26
28
|
<span class="week-ref-arrow" aria-hidden="true">→</span>
|
package/components/XRef.astro
CHANGED
|
@@ -44,9 +44,13 @@ interface Props {
|
|
|
44
44
|
|
|
45
45
|
const { id } = Astro.props;
|
|
46
46
|
const entry = map[id];
|
|
47
|
+
// #142: labels.json stores a base-less `chapters/<slug>#<id>` ref; prefix
|
|
48
|
+
// BASE_URL at render. The leading-slash strip tolerates a labels.json built
|
|
49
|
+
// by an older (pre-#142) build-labels that still emits a root-absolute href.
|
|
50
|
+
const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
|
|
47
51
|
---
|
|
48
52
|
{entry ? (
|
|
49
|
-
<a href={entry.href} class="xref">{entry.display}</a>
|
|
53
|
+
<a href={`${baseUrl}${entry.href.replace(/^\//, '')}`} class="xref">{entry.display}</a>
|
|
50
54
|
) : (
|
|
51
55
|
<span class="xref xref-unknown" title={`Unknown label: ${id}`}>[?{id}]</span>
|
|
52
56
|
)}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as preact from 'preact';
|
|
2
|
-
import { a as ExamQuestion, R as RoutingChapter } from '../exam-manifest-
|
|
2
|
+
import { a as ExamQuestion, R as RoutingChapter } from '../exam-manifest-X9IrX1G3.js';
|
|
3
3
|
import '../schemas-CKipJ5Ie.js';
|
|
4
4
|
import 'astro/zod';
|
|
5
5
|
|
|
@@ -115,11 +115,16 @@ interface RoutingChapter {
|
|
|
115
115
|
* Derive the weak-domain → chapters routing map for `<AssessmentTest>` (#113):
|
|
116
116
|
* for each domain, the distinct chapters (in book order) that carry ≥1 question
|
|
117
117
|
* in it. String chapters are slugs (the schema's kebab-case branch) and link to
|
|
118
|
-
*
|
|
118
|
+
* `${baseUrl}chapters/<slug>/`; numeric chapters render as plain labels.
|
|
119
|
+
*
|
|
120
|
+
* `baseUrl` (default '/') is the deploy base, injected by the .astro caller from
|
|
121
|
+
* import.meta.env.BASE_URL — NOT read here, because this lib ships pre-compiled
|
|
122
|
+
* in dist/ where Vite's env replacement does not reach, so a self-read would
|
|
123
|
+
* silently drop the base under a non-root deploy (#142).
|
|
119
124
|
*/
|
|
120
125
|
declare function deriveDomainRouting(entries: readonly (QuestionLike & {
|
|
121
126
|
data: Pick<Question, 'id' | 'chapter' | 'domain'>;
|
|
122
|
-
})[]): Record<string, RoutingChapter[]>;
|
|
127
|
+
})[], baseUrl?: string): Record<string, RoutingChapter[]>;
|
|
123
128
|
/**
|
|
124
129
|
* Build the cross-domain blueprint for assessment mode: spread `count` evenly
|
|
125
130
|
* across the domains present in the pool (floor, minimum 1 per domain), letting
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { B as BOOK_PRESETS, a as BOOK_PROFILES, b as BookConfigError, d as BookP
|
|
|
4
4
|
import { E as volatilityLevels, c as academicParts, Q as Question } from './schemas-CKipJ5Ie.js';
|
|
5
5
|
export { A as AcademicChapter, B as BloomLevel, C as CourseNotesChapter, G as GlossaryTerm, M as MinimalChapter, P as Provenance, a as QuestionType, R as ResearchPortfolioChapter, T as ToolsChapter, b as academicChapterSchema, d as bloomLevels, e as changeKinds, f as changelogSchema, g as chapterStatus, h as citationBackstops, i as courseNotesChapterSchema, j as glossarySchema, l as layoutModes, m as minimalChapterSchema, p as patternCategories, k as patternsSchema, n as provenanceObject, o as provenanceSchema, q as questionDifficulties, r as questionSchema, s as questionTypes, t as refineQuestion, u as refinedQuestionSchema, v as researchPortfolioChapterSchema, w as sourceTiers, x as sourceTiersResearch, y as sourcesSchema, z as toolSlugs, D as toolsChapterSchema } from './schemas-CKipJ5Ie.js';
|
|
6
6
|
export { KIND_LABEL, ResolvedTheoremLabel, THEOREM_KINDS, TheoremKind, TheoremLabelProps, resolveTheoremNumber, theoremLabel } from './lib/theorem-label.js';
|
|
7
|
-
export { D as DomainScore, E as ExamBlueprint, a as ExamQuestion, b as ExamResult, R as RoutingChapter, c as buildExamManifest, d as deriveDomainRouting, s as sampleExam, e as scoreExam, f as shuffle, g as spreadBlueprint } from './exam-manifest-
|
|
7
|
+
export { D as DomainScore, E as ExamBlueprint, a as ExamQuestion, b as ExamResult, R as RoutingChapter, c as buildExamManifest, d as deriveDomainRouting, s as sampleExam, e as scoreExam, f as shuffle, g as spreadBlueprint } from './exam-manifest-X9IrX1G3.js';
|
|
8
8
|
export { F as FlashcardRef, b as buildFlashcardDeck } from './flashcards-okekZcl8.js';
|
|
9
9
|
import 'astro/zod';
|
|
10
10
|
|
package/dist/index.mjs
CHANGED
|
@@ -1729,7 +1729,8 @@ function buildExamManifest(entries) {
|
|
|
1729
1729
|
options: (e.data.options ?? []).map((o) => ({ id: o.id, correct: o.correct === true }))
|
|
1730
1730
|
}));
|
|
1731
1731
|
}
|
|
1732
|
-
function deriveDomainRouting(entries) {
|
|
1732
|
+
function deriveDomainRouting(entries, baseUrl = "/") {
|
|
1733
|
+
const base = baseUrl.replace(/\/?$/, "/");
|
|
1733
1734
|
const out = {};
|
|
1734
1735
|
const seen = /* @__PURE__ */ new Set();
|
|
1735
1736
|
for (const e of sortQuestions(entries)) {
|
|
@@ -1739,7 +1740,7 @@ function deriveDomainRouting(entries) {
|
|
|
1739
1740
|
seen.add(key);
|
|
1740
1741
|
(out[e.data.domain] ??= []).push({
|
|
1741
1742
|
label,
|
|
1742
|
-
href: typeof e.data.chapter === "string" ?
|
|
1743
|
+
href: typeof e.data.chapter === "string" ? `${base}chapters/${e.data.chapter}/` : null
|
|
1743
1744
|
});
|
|
1744
1745
|
}
|
|
1745
1746
|
return out;
|
package/layouts/Base.astro
CHANGED
|
@@ -16,10 +16,14 @@
|
|
|
16
16
|
* showSidebar — render the left chapter-navigation sidebar (default: true).
|
|
17
17
|
* Set false for the landing page or any full-bleed surface.
|
|
18
18
|
* Below 1024px the sidebar is auto-hidden via CSS regardless.
|
|
19
|
+
* showChrome — render the tools chrome (ToolFilter + VersionSelector)
|
|
20
|
+
* (default: true). Set false on a landing/hub page with no
|
|
21
|
+
* chapters/tools/versions; search + theme toggle always render.
|
|
19
22
|
*
|
|
20
23
|
* Tools chrome (ToolFilter, VersionSelector): only mounted when
|
|
21
|
-
* BOOK_PROFILE !== 'academic'. Academic-profile books
|
|
22
|
-
*
|
|
24
|
+
* BOOK_PROFILE !== 'academic' AND showChrome !== false. Academic-profile books
|
|
25
|
+
* (and any page with showChrome={false}) get no JS islands in the chrome row
|
|
26
|
+
* (search + theme toggle only).
|
|
23
27
|
*
|
|
24
28
|
* Usage:
|
|
25
29
|
* ---
|
|
@@ -61,13 +65,17 @@ import Sidebar from '../components/Sidebar.astro';
|
|
|
61
65
|
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
62
66
|
|
|
63
67
|
const profile = import.meta.env.BOOK_PROFILE ?? 'minimal';
|
|
64
|
-
const showToolsChrome = profile !== 'academic';
|
|
65
68
|
|
|
66
69
|
interface Props {
|
|
67
70
|
title: string;
|
|
68
71
|
description?: string;
|
|
69
72
|
lang?: string;
|
|
70
73
|
showSidebar?: boolean;
|
|
74
|
+
/** #163: render the optional "tools chrome" — the ToolFilter +
|
|
75
|
+
* VersionSelector islands (default true). Set false on landing/hub pages
|
|
76
|
+
* with no chapters/tools/versions; the search + theme-toggle cluster always
|
|
77
|
+
* renders. (No-op for the academic profile, which never shows them.) */
|
|
78
|
+
showChrome?: boolean;
|
|
71
79
|
/** v4.6.0: Open Graph image URL (relative to site root, or absolute). */
|
|
72
80
|
ogImage?: string;
|
|
73
81
|
/** v4.6.0: Open Graph type. Defaults to 'website'; Chapter.astro passes 'article'. */
|
|
@@ -79,10 +87,18 @@ const {
|
|
|
79
87
|
description,
|
|
80
88
|
lang = 'en',
|
|
81
89
|
showSidebar = true,
|
|
90
|
+
showChrome = true,
|
|
82
91
|
ogImage,
|
|
83
92
|
ogType = 'website',
|
|
84
93
|
} = Astro.props;
|
|
85
94
|
|
|
95
|
+
// Tools chrome (ToolFilter + VersionSelector) renders only for non-academic
|
|
96
|
+
// profiles AND when the page opts in via showChrome (default true). A
|
|
97
|
+
// landing/hub page (no chapters/tools/versions) passes showChrome={false} to
|
|
98
|
+
// get the search + theme-toggle cluster only — the chrome-free path that
|
|
99
|
+
// previously forced borrowing the academic profile (and its katex deps). #163.
|
|
100
|
+
const showToolsChrome = (profile !== 'academic') && showChrome;
|
|
101
|
+
|
|
86
102
|
// v4.6.0: canonical + og:url need Astro.site. defineBookConfig throws at
|
|
87
103
|
// config-load if `site` is missing, so Astro.site is guaranteed populated
|
|
88
104
|
// here. `new URL` resolves the relative pathname against it.
|
|
@@ -99,7 +115,7 @@ const absoluteOgImage = resolvedOgImage
|
|
|
99
115
|
const twitterHandle = bookConfig.seo?.twitterHandle ?? null;
|
|
100
116
|
const ogSiteName = bookConfig.title ?? title;
|
|
101
117
|
const ogDescription = description ?? bookConfig.description ?? '';
|
|
102
|
-
const baseUrl = import.meta.env.BASE_URL ?? '/';
|
|
118
|
+
const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
|
|
103
119
|
---
|
|
104
120
|
|
|
105
121
|
<!doctype html>
|
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. Profile-aware (academic / tools / minimal); ships Tufte typography, KaTeX, BibTeX citations, Pagefind, Cloudflare Workers deploy. See PACKAGE_DESIGN.md for the API contract.",
|
|
4
|
-
"version": "4.25.
|
|
4
|
+
"version": "4.25.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Brandon Behring",
|
package/pages/answers.astro
CHANGED
|
@@ -50,13 +50,15 @@ const rendered = await Promise.all(
|
|
|
50
50
|
|
|
51
51
|
const byChapter = groupByChapter(rendered);
|
|
52
52
|
const total = rendered.length;
|
|
53
|
+
// #142: practice-exam + chapter links must prefix the deploy base.
|
|
54
|
+
const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
|
|
53
55
|
const practiceHref = (bookConfig.enabledRoutes ?? []).includes('practiceExam')
|
|
54
|
-
?
|
|
56
|
+
? `${baseUrl}practice-exam`
|
|
55
57
|
: null;
|
|
56
58
|
|
|
57
59
|
/** A string chapter ref is a slug (schema kebab-case branch) → linkable. */
|
|
58
60
|
function chapterHref(chapter: number | string): string | null {
|
|
59
|
-
return typeof chapter === 'string' ?
|
|
61
|
+
return typeof chapter === 'string' ? `${baseUrl}chapters/${chapter}/` : null;
|
|
60
62
|
}
|
|
61
63
|
---
|
|
62
64
|
<Base
|
package/pages/chapters.astro
CHANGED
|
@@ -27,6 +27,9 @@ 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
|
|
|
30
|
+
// #142: prefix BASE_URL so chapter-card links stay inside a non-root deploy base.
|
|
31
|
+
const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
|
|
32
|
+
|
|
30
33
|
const profileName = (import.meta.env.BOOK_PROFILE ?? 'minimal') as keyof typeof PROFILES;
|
|
31
34
|
const profileDef = PROFILES[profileName];
|
|
32
35
|
const renderer: ChaptersRenderer =
|
|
@@ -112,7 +115,7 @@ for (const c of chapters) {
|
|
|
112
115
|
<ol class="chapter-list">
|
|
113
116
|
{cards.map((card) => (
|
|
114
117
|
<li class="chapter-card" data-tools={card.toolsAttr}>
|
|
115
|
-
<a href={
|
|
118
|
+
<a href={`${baseUrl}chapters/${card.c.id}/`} class="chapter-card-link">
|
|
116
119
|
<div class="chapter-card-meta">
|
|
117
120
|
<span class="chapter-card-number">{card.number}</span>
|
|
118
121
|
{card.volatility && (
|
package/pages/exercises.astro
CHANGED
|
@@ -26,6 +26,9 @@ const loadError = exEntry
|
|
|
26
26
|
|
|
27
27
|
const chapters = Object.keys(byChapter).sort();
|
|
28
28
|
const total = chapters.reduce((sum, c) => sum + byChapter[c].length, 0);
|
|
29
|
+
|
|
30
|
+
// #142: chapter deep-links must prefix the deploy base.
|
|
31
|
+
const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
|
|
29
32
|
---
|
|
30
33
|
<Base title="Exercises" description="All exercises in this book, grouped by chapter with deep links.">
|
|
31
34
|
<article class="prose">
|
|
@@ -47,12 +50,12 @@ const total = chapters.reduce((sum, c) => sum + byChapter[c].length, 0);
|
|
|
47
50
|
{chapters.map((chapter) => (
|
|
48
51
|
<section class="exercises-chapter" id={`chapter-${chapter}`}>
|
|
49
52
|
<h2 class="exercises-chapter-title">
|
|
50
|
-
Chapter: <a href={
|
|
53
|
+
Chapter: <a href={`${baseUrl}chapters/${chapter}/`}>{chapter}</a>
|
|
51
54
|
</h2>
|
|
52
55
|
<ol class="exercises-list">
|
|
53
56
|
{byChapter[chapter].map((ex) => (
|
|
54
57
|
<li class="exercises-item">
|
|
55
|
-
<a href={
|
|
58
|
+
<a href={`${baseUrl}chapters/${chapter}/#exercise-${ex.id}`} class="exercises-link">
|
|
56
59
|
<strong class="exercises-id">Exercise {ex.id}</strong>
|
|
57
60
|
<span class="exercises-preview">{ex.problem.slice(0, 120)}{ex.problem.length > 120 ? '…' : ''}</span>
|
|
58
61
|
</a>
|
package/pages/index.astro
CHANGED
|
@@ -40,17 +40,21 @@ const description = bookConfig.description;
|
|
|
40
40
|
const portfolio = bookConfig.portfolio;
|
|
41
41
|
const enabledRoutes = bookConfig.enabledRoutes;
|
|
42
42
|
|
|
43
|
+
// Astro guarantees BASE_URL carries a trailing slash ('/' or '/foo/'), so
|
|
44
|
+
// `${baseUrl}chapters/` resolves correctly under any deploy base (#142).
|
|
45
|
+
const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
|
|
46
|
+
|
|
43
47
|
// Map from internal route name → display label + URL. Only routes that
|
|
44
48
|
// produce a single landing-list entry are listed here (frontmatter is a
|
|
45
49
|
// slug pattern; chaptersSlug is dynamic; landing IS this page).
|
|
46
50
|
const ROUTE_LABELS: Record<string, { label: string; href: string }> = {
|
|
47
|
-
chapters: { label: 'Chapters', href:
|
|
48
|
-
search: { label: 'Search', href:
|
|
49
|
-
references: { label: 'References', href:
|
|
50
|
-
print: { label: 'Print view', href:
|
|
51
|
-
convergence: { label: 'Convergence', href:
|
|
52
|
-
tips: { label: 'Tips', href:
|
|
53
|
-
exercises: { label: 'Exercises', href:
|
|
51
|
+
chapters: { label: 'Chapters', href: `${baseUrl}chapters/` },
|
|
52
|
+
search: { label: 'Search', href: `${baseUrl}search/` },
|
|
53
|
+
references: { label: 'References', href: `${baseUrl}references/` },
|
|
54
|
+
print: { label: 'Print view', href: `${baseUrl}print/` },
|
|
55
|
+
convergence: { label: 'Convergence', href: `${baseUrl}convergence/` },
|
|
56
|
+
tips: { label: 'Tips', href: `${baseUrl}tips/` },
|
|
57
|
+
exercises: { label: 'Exercises', href: `${baseUrl}exercises/` },
|
|
54
58
|
};
|
|
55
59
|
|
|
56
60
|
const visibleRoutes = enabledRoutes
|
package/pages/search.astro
CHANGED
|
@@ -11,13 +11,17 @@
|
|
|
11
11
|
* `astro dev`. See README for dev-search workflow.
|
|
12
12
|
*/
|
|
13
13
|
import Base from '../layouts/Base.astro';
|
|
14
|
+
|
|
15
|
+
// #142: Pagefind writes its bundle to dist/pagefind/, served under the deploy
|
|
16
|
+
// base — so the asset URLs must prefix BASE_URL or they 404 under a non-root base.
|
|
17
|
+
const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
|
|
14
18
|
---
|
|
15
19
|
|
|
16
20
|
<Base
|
|
17
21
|
title="Search · book-template-astro"
|
|
18
22
|
description="Full-text search across the book. Indexed via Pagefind at build time; all queries run in the browser."
|
|
19
23
|
>
|
|
20
|
-
<link rel="stylesheet" href=
|
|
24
|
+
<link rel="stylesheet" href={`${baseUrl}pagefind/pagefind-ui.css`} slot="head" />
|
|
21
25
|
|
|
22
26
|
<div class="prose search-page">
|
|
23
27
|
<h1>Search</h1>
|
|
@@ -29,7 +33,7 @@ import Base from '../layouts/Base.astro';
|
|
|
29
33
|
<div id="search"></div>
|
|
30
34
|
</div>
|
|
31
35
|
|
|
32
|
-
<script src=
|
|
36
|
+
<script src={`${baseUrl}pagefind/pagefind-ui.js`} is:inline></script>
|
|
33
37
|
<script is:inline>
|
|
34
38
|
// Initialize after the Pagefind bundle has loaded.
|
|
35
39
|
window.addEventListener('DOMContentLoaded', () => {
|
package/pages/tips.astro
CHANGED
|
@@ -25,6 +25,9 @@ const tips = tipsEntry?.default ?? [];
|
|
|
25
25
|
const loadError = tipsEntry
|
|
26
26
|
? null
|
|
27
27
|
: 'src/data/tips.json not found — run `npx book-scaffold build-tips` to generate.';
|
|
28
|
+
|
|
29
|
+
// #142: chapter back-links must prefix the deploy base.
|
|
30
|
+
const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
|
|
28
31
|
---
|
|
29
32
|
<Base title="Tips" description="Numbered tips from this book, drawn from <Tip> instances in chapters.">
|
|
30
33
|
<article class="prose">
|
|
@@ -47,7 +50,7 @@ const loadError = tipsEntry
|
|
|
47
50
|
<p class="tips-index-preview">{tip.preview}</p>
|
|
48
51
|
)}
|
|
49
52
|
<p class="tips-index-meta">
|
|
50
|
-
From <a href={
|
|
53
|
+
From <a href={`${baseUrl}chapters/${tip.chapter}/`}>{tip.chapter}</a>
|
|
51
54
|
</p>
|
|
52
55
|
</li>
|
|
53
56
|
))}
|
|
@@ -153,7 +153,7 @@ TLS provides confidentiality and integrity via the negotiated cipher suite… <C
|
|
|
153
153
|
|
|
154
154
|
- **`/flashcards` route** (v4.22.0, #116; `routes: { flashcards: true }`) — Sybex-style electronic flashcards generated from the glossary collection (front = term, back = the rendered MDX definition). The island shuffles a deck, shows one card at a time (recall first — the back hides until Flip), sorts cards into knew-it/still-learning buckets persisted to localStorage (`book:flashcards:known`), and offers a "review unknown only" pass. Appendix-style surface, deliberately not inline (Bjork: recall practice, not re-reading). No JS → the full front+back list stays readable. Question/objective-derived cards are a later increment.
|
|
155
155
|
|
|
156
|
-
The `/practice-exam` route renders the bank grouped by domain with each answer behind a "Show answer" reveal — and (v4.21.0, #112-UI) mounts the **ExamRunner island** (`client:idle`): Start samples a form client-side (pure `sampleExam`), hides the rest of the bank, hides answer reveals while the exam is active, scores checked radio options on submit (`scoreExam`), and renders a per-domain readout with weak-domain anchors. No JS → the static bank with radios + reveals is the fallback. The `/answers` route (v4.21.0, #114; `routes: { answers: true }`) is the back-appendix: every question grouped by chapter with options, the correct answer, and rationales pre-expanded. `cloze` questions are reserved (schema-accepted, render-deferred). (`<Diagnostic>` #110, `<PartReview>` #111, and `/glossary` #115 shipped in v4.19.0; the interactive layer #112-UI/#113/#114 in v4.21.0; flashcards #116 in v4.22.0 — completing epic #122. History: `docs/plans/
|
|
156
|
+
The `/practice-exam` route renders the bank grouped by domain with each answer behind a "Show answer" reveal — and (v4.21.0, #112-UI) mounts the **ExamRunner island** (`client:idle`): Start samples a form client-side (pure `sampleExam`), hides the rest of the bank, hides answer reveals while the exam is active, scores checked radio options on submit (`scoreExam`), and renders a per-domain readout with weak-domain anchors. No JS → the static bank with radios + reveals is the fallback. The `/answers` route (v4.21.0, #114; `routes: { answers: true }`) is the back-appendix: every question grouped by chapter with options, the correct answer, and rationales pre-expanded. `cloze` questions are reserved (schema-accepted, render-deferred). (`<Diagnostic>` #110, `<PartReview>` #111, and `/glossary` #115 shipped in v4.19.0; the interactive layer #112-UI/#113/#114 in v4.21.0; flashcards #116 in v4.22.0 — completing epic #122. History: `docs/plans/implemented/study-guide-epic_*.md`.)
|
|
157
157
|
|
|
158
158
|
## Mixing families
|
|
159
159
|
|
|
@@ -152,7 +152,7 @@ if (!byBook.has(book)) byBook.set(book, []);
|
|
|
152
152
|
byBook.get(book).push({ ... });
|
|
153
153
|
```
|
|
154
154
|
|
|
155
|
-
Then write one file per book key. Multi-book corpus routing is itself out of scope at v4.x ([deferred, see
|
|
155
|
+
Then write one file per book key. Multi-book corpus routing is itself out of scope at v4.x ([deferred, see §15a](../../PACKAGE_DESIGN.md#15a-deferred-scope-post-v4x); tracked on #80) — if you need it, the same consumer-side pattern applies.
|
|
156
156
|
|
|
157
157
|
## Common gotchas
|
|
158
158
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Profile**: any (proven on research-portfolio).
|
|
4
4
|
|
|
5
|
-
**TL;DR**: To host more than one guide/book in a single Astro app, keep ONE `chapters` collection rooted at `src/content/` and namespace every entry id by its guide folder via a `generateId` in the glob loader. The scaffold's existing rest-param route (`/chapters/[...slug]/`) then serves `/chapters/<guide>/<slug>/` for every guide with **no scaffold changes**. This is the blessed interim pattern until multibook (#
|
|
5
|
+
**TL;DR**: To host more than one guide/book in a single Astro app, keep ONE `chapters` collection rooted at `src/content/` and namespace every entry id by its guide folder via a `generateId` in the glob loader. The scaffold's existing rest-param route (`/chapters/[...slug]/`) then serves `/chapters/<guide>/<slug>/` for every guide with **no scaffold changes**. This is the blessed interim pattern until multibook (#80) ships first-class support.
|
|
6
6
|
|
|
7
7
|
## The pattern
|
|
8
8
|
|
|
@@ -52,15 +52,15 @@ The scaffold's auto-injected `pages/chapters/[...slug].astro` routes on `params:
|
|
|
52
52
|
|
|
53
53
|
## Gotcha 2 — `validate` counts everything under the base
|
|
54
54
|
|
|
55
|
-
With `base: './src/content'`, `book-scaffold validate` walks the whole base — shared folders like `frontmatter/` are counted as "chapters" (e.g. `16 chapter(s)` for 13 + 2 real chapters + an authors page). The checks still run correctly per file; only the count is inflated. Excluding non-guide folders from the *loader* pattern does not affect the validator's walk. Guide-aware validation is part of the first-class multibook design (#
|
|
55
|
+
With `base: './src/content'`, `book-scaffold validate` walks the whole base — shared folders like `frontmatter/` are counted as "chapters" (e.g. `16 chapter(s)` for 13 + 2 real chapters + an authors page). The checks still run correctly per file; only the count is inflated. Excluding non-guide folders from the *loader* pattern does not affect the validator's walk. Guide-aware validation is part of the first-class multibook design (#80).
|
|
56
56
|
|
|
57
57
|
## Gotcha 3 — the `/chapters/` index mixes guides
|
|
58
58
|
|
|
59
|
-
The auto-injected `/chapters/` index lists every entry in the collection — i.e. all guides interleaved. If you want per-guide landing pages, add consumer-owned pages (e.g. `src/pages/evaluation/index.astro`) that `getCollection('chapters', (e) => e.id.startsWith('evaluation/'))`. A grouped-by-guide index is also on the #
|
|
59
|
+
The auto-injected `/chapters/` index lists every entry in the collection — i.e. all guides interleaved. If you want per-guide landing pages, add consumer-owned pages (e.g. `src/pages/evaluation/index.astro`) that `getCollection('chapters', (e) => e.id.startsWith('evaluation/'))`. A grouped-by-guide index is also on the #80 wishlist.
|
|
60
60
|
|
|
61
61
|
## When to expect first-class support
|
|
62
62
|
|
|
63
|
-
This recipe is the interim, zero-scaffold-change path. Issue [#
|
|
63
|
+
This recipe is the interim, zero-scaffold-change path. Issue [#80 (multibook)](https://github.com/brandon-behring/book-scaffold-astro/issues/80) tracks a `books`/`guides` registry that would emit per-guide indexes, a guides landing, and guide-scoped `validate` (the original #15 is closed; #80 is the live v5.x tracker). It was deferred pending 2–3 independent consumers; `guides-ai-engineering` (#132) is the second — file your use case on #80 to add weight.
|
|
64
64
|
|
|
65
65
|
## Canonical files
|
|
66
66
|
|
package/recipes/README.md
CHANGED
|
@@ -26,7 +26,7 @@ Terse pointers into canonical code for the most common book-authoring workflows.
|
|
|
26
26
|
| 18 | [Chapter route ownership](18-chapter-route-ownership.md) | any | When to override the auto-injected `/chapters/[...slug]/` route |
|
|
27
27
|
| 19 | [Prevalidate hook](19-prevalidate-hook.md) | any | Wire `prevalidate` to run `build:bib` + `build:labels` before `validate` |
|
|
28
28
|
| 20 | [Anki deck export (consumer-side)](20-anki-export.md) | any (esp. course-notes, research-portfolio) | Roll-your-own `<AnkiCard>` + extractor; scaffold deliberately doesn't ship this |
|
|
29
|
-
| 21 | [Multiple guides in one app](21-multi-guide-single-app.md) | any | `generateId` namespacing over one collection; flat-slug footgun; interim until multibook (#
|
|
29
|
+
| 21 | [Multiple guides in one app](21-multi-guide-single-app.md) | any | `generateId` namespacing over one collection; flat-slug footgun; interim until multibook (#80) |
|
|
30
30
|
|
|
31
31
|
## How to read recipes
|
|
32
32
|
|
package/scripts/build-labels.mjs
CHANGED
|
@@ -259,7 +259,9 @@ async function main() {
|
|
|
259
259
|
);
|
|
260
260
|
}
|
|
261
261
|
labels[id] = {
|
|
262
|
-
|
|
262
|
+
// #142: base-less ref — XRef.astro prefixes BASE_URL at render so one
|
|
263
|
+
// labels.json serves any deploy base (root or path-proxied series).
|
|
264
|
+
href: `chapters/${slug}#${id}`,
|
|
263
265
|
display,
|
|
264
266
|
number: labelOverride ? null : number,
|
|
265
267
|
};
|
package/src/lib/exam-manifest.ts
CHANGED
|
@@ -49,11 +49,20 @@ export interface RoutingChapter {
|
|
|
49
49
|
* Derive the weak-domain → chapters routing map for `<AssessmentTest>` (#113):
|
|
50
50
|
* for each domain, the distinct chapters (in book order) that carry ≥1 question
|
|
51
51
|
* in it. String chapters are slugs (the schema's kebab-case branch) and link to
|
|
52
|
-
*
|
|
52
|
+
* `${baseUrl}chapters/<slug>/`; numeric chapters render as plain labels.
|
|
53
|
+
*
|
|
54
|
+
* `baseUrl` (default '/') is the deploy base, injected by the .astro caller from
|
|
55
|
+
* import.meta.env.BASE_URL — NOT read here, because this lib ships pre-compiled
|
|
56
|
+
* in dist/ where Vite's env replacement does not reach, so a self-read would
|
|
57
|
+
* silently drop the base under a non-root deploy (#142).
|
|
53
58
|
*/
|
|
54
59
|
export function deriveDomainRouting(
|
|
55
60
|
entries: readonly (QuestionLike & { data: Pick<Question, 'id' | 'chapter' | 'domain'> })[],
|
|
61
|
+
baseUrl = '/',
|
|
56
62
|
): Record<string, RoutingChapter[]> {
|
|
63
|
+
// #142: normalize so a no-trailing-slash base ('/foo') still yields '/foo/chapters/'
|
|
64
|
+
// rather than '/foochapters/' (Astro does NOT guarantee a trailing slash on base).
|
|
65
|
+
const base = baseUrl.replace(/\/?$/, '/');
|
|
57
66
|
const out: Record<string, RoutingChapter[]> = {};
|
|
58
67
|
const seen = new Set<string>(); // domain\u0000label pairs already routed
|
|
59
68
|
for (const e of sortQuestions(entries)) {
|
|
@@ -63,7 +72,7 @@ export function deriveDomainRouting(
|
|
|
63
72
|
seen.add(key);
|
|
64
73
|
(out[e.data.domain] ??= []).push({
|
|
65
74
|
label,
|
|
66
|
-
href: typeof e.data.chapter === 'string' ?
|
|
75
|
+
href: typeof e.data.chapter === 'string' ? `${base}chapters/${e.data.chapter}/` : null,
|
|
67
76
|
});
|
|
68
77
|
}
|
|
69
78
|
return out;
|
package/styles/chapter.css
CHANGED
|
@@ -230,3 +230,18 @@
|
|
|
230
230
|
}
|
|
231
231
|
.chapter-nav .next { grid-column: 1; text-align: left; }
|
|
232
232
|
}
|
|
233
|
+
|
|
234
|
+
/* Display math (KaTeX) — scroll a too-wide equation WITHIN its own block instead
|
|
235
|
+
of forcing a horizontal scroll on the whole page. KaTeX sets no `overflow` on
|
|
236
|
+
`.katex-display`, so it defaults to `visible`; a wide equation (long
|
|
237
|
+
derivations, big matrices) then overflows the viewport and the page scrolls
|
|
238
|
+
sideways on mobile. WCAG 1.4.10 Reflow. See book-scaffold-astro#162.
|
|
239
|
+
overflow-y is deliberately NOT set: once overflow-x is non-visible the CSS
|
|
240
|
+
overflow rule already computes overflow-y to `auto`, so there is no spurious
|
|
241
|
+
vertical scrollbar — and an explicit `hidden` would silently clip tall
|
|
242
|
+
equations if a consumer ever constrains .katex-display's height. */
|
|
243
|
+
.katex-display {
|
|
244
|
+
overflow-x: auto;
|
|
245
|
+
/* a little room so the horizontal scrollbar doesn't clip the equation's descenders */
|
|
246
|
+
padding-block-end: 0.25rem;
|
|
247
|
+
}
|