@brandon_m_behring/book-scaffold-astro 4.5.1 → 4.6.1
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 +29 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +89 -25
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.mjs +45 -13
- package/examples/chapter-template-research-portfolio.mdx +18 -10
- package/layouts/Base.astro +54 -1
- package/layouts/Chapter.astro +35 -1
- package/package.json +2 -1
- package/pages/index.astro +5 -3
- package/recipes/07-chapter-shapes.md +2 -0
- package/recipes/13-research-portfolio-getting-started.md +49 -11
- package/recipes/18-chapter-route-ownership.md +87 -0
- package/recipes/19-prevalidate-hook.md +113 -0
- package/recipes/20-anki-export.md +175 -0
- package/recipes/README.md +6 -0
- package/scripts/validate.mjs +78 -0
- package/src/profile-kit.ts +17 -0
- package/src/profiles/academic.ts +4 -1
- package/src/profiles/course-notes.ts +4 -1
- package/src/profiles/minimal.ts +1 -1
- package/src/profiles/research-portfolio.ts +1 -1
- package/src/schemas.ts +27 -0
package/CLAUDE.md
CHANGED
|
@@ -58,6 +58,35 @@ sources: [] # array of source-manifest keys
|
|
|
58
58
|
---
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
### Research-portfolio profile (`src/schemas.ts:researchPortfolioChapterSchema`)
|
|
62
|
+
|
|
63
|
+
Hybrid of academic + tools provenance with research-paper-style inline sources. Only `title` + `last_verified` are required; all hierarchy and classification fields are optional.
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
---
|
|
67
|
+
title: "..." # required
|
|
68
|
+
last_verified: 2026-05-19 # date, required
|
|
69
|
+
# optional — hierarchy (use whichever fits; all may be omitted)
|
|
70
|
+
slug: ch01-introduction # defaults to filename
|
|
71
|
+
chapter: 1
|
|
72
|
+
part: 1 # number OR academic-style string enum
|
|
73
|
+
week: 1
|
|
74
|
+
# optional — status (AUTHORING state) vs freshness (EPISTEMIC type) are ORTHOGONAL
|
|
75
|
+
status: prose_only # 'scaffolded'|'prose_only'|'code_only'|'chapter_only'|'reading_only'|'implemented'|'planned'
|
|
76
|
+
freshness: experimental-result # 'experimental-result'|'literature-survey'|'theoretical'|'reference'
|
|
77
|
+
# optional — provenance + inline sources (T1-T4 tiers)
|
|
78
|
+
volatility: feature-surface # 'stable-principle'|'architectural-pattern'|'feature-surface'
|
|
79
|
+
tags: [prompt-injection, ...] # freeform string array
|
|
80
|
+
sources:
|
|
81
|
+
- tier: T1
|
|
82
|
+
url: https://...
|
|
83
|
+
label: Primary source
|
|
84
|
+
# optional: description, draft, updated, author, published, image (SEO/og:*)
|
|
85
|
+
---
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**`status` vs `freshness` is the #1 author gotcha.** `status` = authoring state (have I written it?). `freshness` = epistemic type (what kind of evidence?). A chapter can be `status: scaffolded` (not written yet) AND `freshness: theoretical` (will be a math argument). See Recipe 13 for the full table.
|
|
89
|
+
|
|
61
90
|
## Component reference
|
|
62
91
|
|
|
63
92
|
Two callout families coexist. Authors import what they need.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AstroUserConfig, AstroIntegration } from 'astro';
|
|
2
|
-
import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, Q as volatilityLevels, h as ChaptersRenderer, n as Style } from './types-
|
|
3
|
-
export { A as AcademicChapter, B as BOOK_PRESETS, a as BOOK_PROFILES, b as BookConfigError, d as BookPreset, e as BookProfile, g as BookSchemasOptions, C as ChapterFor, i as CourseNotesChapter, F as FreshnessAffordance, j as FrontmatterRouteConfig, M as MinimalChapter, P as PartKey, k as PartialRouteToggles, l as ProfileDefinition, R as ResearchPortfolioChapter, m as RouteToggles, S as StatusBadge, o as StyleInput, T as ToolsChapter, V as VolatilityBadge, p as academicChapterSchema, q as academicParts, r as changeKinds, s as changelogSchema, t as chapterStatus, u as composeStyles, v as courseNotesChapterSchema, w as defineProfile, x as defineStyle, y as minimalChapterSchema, z as normalizeFrontmatterConfig, D as patternCategories, E as patternsSchema, G as researchPortfolioChapterSchema, H as resolvePreset, I as resolveProfile, J as sourceTiers, K as sourceTiersResearch, L as sourcesSchema, N as toolSlugs, O as toolsChapterSchema } from './types-
|
|
2
|
+
import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, Q as volatilityLevels, h as ChaptersRenderer, n as Style } from './types-B2bO9Nga.js';
|
|
3
|
+
export { A as AcademicChapter, B as BOOK_PRESETS, a as BOOK_PROFILES, b as BookConfigError, d as BookPreset, e as BookProfile, g as BookSchemasOptions, C as ChapterFor, i as CourseNotesChapter, F as FreshnessAffordance, j as FrontmatterRouteConfig, M as MinimalChapter, P as PartKey, k as PartialRouteToggles, l as ProfileDefinition, R as ResearchPortfolioChapter, m as RouteToggles, S as StatusBadge, o as StyleInput, T as ToolsChapter, V as VolatilityBadge, p as academicChapterSchema, q as academicParts, r as changeKinds, s as changelogSchema, t as chapterStatus, u as composeStyles, v as courseNotesChapterSchema, w as defineProfile, x as defineStyle, y as minimalChapterSchema, z as normalizeFrontmatterConfig, D as patternCategories, E as patternsSchema, G as researchPortfolioChapterSchema, H as resolvePreset, I as resolveProfile, J as sourceTiers, K as sourceTiersResearch, L as sourcesSchema, N as toolSlugs, O as toolsChapterSchema } from './types-B2bO9Nga.js';
|
|
4
4
|
import 'astro/zod';
|
|
5
5
|
|
|
6
6
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -117,6 +117,7 @@ var init_katex_macros = __esm({
|
|
|
117
117
|
// src/config.ts
|
|
118
118
|
import mdx from "@astrojs/mdx";
|
|
119
119
|
import preact from "@astrojs/preact";
|
|
120
|
+
import sitemap from "@astrojs/sitemap";
|
|
120
121
|
|
|
121
122
|
// src/profile-kit.ts
|
|
122
123
|
function defineProfile(p) {
|
|
@@ -177,7 +178,14 @@ var academicChapterSchema = z.object({
|
|
|
177
178
|
tests_path: z.string().optional(),
|
|
178
179
|
notebook_path: z.string().optional(),
|
|
179
180
|
description: z.string().optional(),
|
|
180
|
-
draft: z.boolean().default(false)
|
|
181
|
+
draft: z.boolean().default(false),
|
|
182
|
+
// v4.6.0: optional SEO / article:* fields consumed by Chapter.astro.
|
|
183
|
+
// All optional; existing chapters without these continue to work.
|
|
184
|
+
author: z.string().optional(),
|
|
185
|
+
published: z.date().optional(),
|
|
186
|
+
updated: z.date().optional(),
|
|
187
|
+
tags: z.array(z.string()).default([]),
|
|
188
|
+
image: z.string().optional()
|
|
181
189
|
});
|
|
182
190
|
var toolsChapterSchema = z.object({
|
|
183
191
|
title: z.string().min(1),
|
|
@@ -189,7 +197,13 @@ var toolsChapterSchema = z.object({
|
|
|
189
197
|
sources: z.array(z.string()).default([]),
|
|
190
198
|
description: z.string().optional(),
|
|
191
199
|
draft: z.boolean().default(false),
|
|
192
|
-
updated: z.date().optional()
|
|
200
|
+
updated: z.date().optional(),
|
|
201
|
+
// v4.6.0: optional SEO / article:* fields consumed by Chapter.astro.
|
|
202
|
+
// `updated` already existed; the rest are new.
|
|
203
|
+
author: z.string().optional(),
|
|
204
|
+
published: z.date().optional(),
|
|
205
|
+
tags: z.array(z.string()).default([]),
|
|
206
|
+
image: z.string().optional()
|
|
193
207
|
});
|
|
194
208
|
var minimalChapterSchema = toolsChapterSchema;
|
|
195
209
|
var sourceTiersResearch = ["T1", "T2", "T3", "T4"];
|
|
@@ -216,7 +230,14 @@ var courseNotesChapterSchema = z.object({
|
|
|
216
230
|
last_verified: z.date(),
|
|
217
231
|
volatility: z.enum(volatilityLevels).default("architectural-pattern"),
|
|
218
232
|
sources: z.array(z.string()).default([]),
|
|
219
|
-
draft: z.boolean().default(false)
|
|
233
|
+
draft: z.boolean().default(false),
|
|
234
|
+
// v4.6.0: optional SEO / article:* fields consumed by Chapter.astro.
|
|
235
|
+
// `tags` already existed; the rest are new. `instructor` (line 130) is
|
|
236
|
+
// attribution metadata — distinct from `author` (the note-writer/curator).
|
|
237
|
+
author: z.string().optional(),
|
|
238
|
+
published: z.date().optional(),
|
|
239
|
+
updated: z.date().optional(),
|
|
240
|
+
image: z.string().optional()
|
|
220
241
|
});
|
|
221
242
|
var researchPortfolioChapterSchema = z.object({
|
|
222
243
|
// Identity
|
|
@@ -269,7 +290,12 @@ var researchPortfolioChapterSchema = z.object({
|
|
|
269
290
|
// Status + dates.
|
|
270
291
|
last_verified: z.date(),
|
|
271
292
|
updated: z.date().optional(),
|
|
272
|
-
draft: z.boolean().default(false)
|
|
293
|
+
draft: z.boolean().default(false),
|
|
294
|
+
// v4.6.0: optional SEO / article:* fields consumed by Chapter.astro.
|
|
295
|
+
// `tags` + `updated` already existed; `author` + `published` + `image` are new.
|
|
296
|
+
author: z.string().optional(),
|
|
297
|
+
published: z.date().optional(),
|
|
298
|
+
image: z.string().optional()
|
|
273
299
|
});
|
|
274
300
|
var sourcesSchema = z.object({
|
|
275
301
|
url: z.string().url(),
|
|
@@ -372,8 +398,8 @@ var academicProfile = defineProfile({
|
|
|
372
398
|
references: true,
|
|
373
399
|
search: true,
|
|
374
400
|
print: true,
|
|
375
|
-
chapters:
|
|
376
|
-
// academic
|
|
401
|
+
chapters: true,
|
|
402
|
+
// v4.6.1 (#75 follow-up): auto-injected /chapters/[...slug]/ + /chapters/ index. Pre-v4.3.0 academic books shipped their own listing; v4.6.0 (#76 Layer 3c) removed the consumer template assuming auto-injection. Default flipped here to close the gap. Consumers wanting their own listing override via `routes: { chapters: false }` + their own src/pages/chapters/* — see recipe 18.
|
|
377
403
|
convergence: false,
|
|
378
404
|
// tools-profile-specific
|
|
379
405
|
frontmatter: false,
|
|
@@ -387,8 +413,11 @@ var academicProfile = defineProfile({
|
|
|
387
413
|
},
|
|
388
414
|
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
|
|
389
415
|
katex: true,
|
|
390
|
-
chaptersRenderer: academicChaptersRenderer
|
|
416
|
+
chaptersRenderer: academicChaptersRenderer,
|
|
391
417
|
// v3.7.0 (#35) — owns /chapters semantics if consumer opts in via routes.chapters
|
|
418
|
+
// v4.6.0 (#76 Secondary): exclude /print/ from sitemap — print-friendly
|
|
419
|
+
// view, crawl-redundant. Academic-profile default.
|
|
420
|
+
sitemapFilter: (page) => !page.includes("/print/")
|
|
392
421
|
});
|
|
393
422
|
|
|
394
423
|
// src/lib/freshness.ts
|
|
@@ -578,7 +607,8 @@ var minimalProfile = defineProfile({
|
|
|
578
607
|
references: true,
|
|
579
608
|
search: true,
|
|
580
609
|
print: true,
|
|
581
|
-
chapters:
|
|
610
|
+
chapters: true,
|
|
611
|
+
// v4.6.1 (#75 follow-up): default-on across all profiles. Consumer override via routes: { chapters: false }.
|
|
582
612
|
convergence: false,
|
|
583
613
|
frontmatter: false,
|
|
584
614
|
// opt-in per book; see #7
|
|
@@ -602,8 +632,8 @@ var courseNotesProfile = defineProfile({
|
|
|
602
632
|
references: true,
|
|
603
633
|
search: true,
|
|
604
634
|
print: true,
|
|
605
|
-
chapters:
|
|
606
|
-
//
|
|
635
|
+
chapters: true,
|
|
636
|
+
// v4.6.1 (#75 follow-up): default-on. Multi-book consumers (DLAI-style) override via routes: { chapters: false } + own [book]/[slug] routes — see #15 deferred.
|
|
607
637
|
convergence: false,
|
|
608
638
|
frontmatter: false,
|
|
609
639
|
// opt-in per book; see #7
|
|
@@ -616,7 +646,10 @@ var courseNotesProfile = defineProfile({
|
|
|
616
646
|
},
|
|
617
647
|
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
|
|
618
648
|
// v3.7.0 (#35): course-notes schema has tools-style fields (chapter, volatility, sources) — fallback renderer dispatches via tools renderer
|
|
619
|
-
chaptersRenderer: fallbackChaptersRenderer
|
|
649
|
+
chaptersRenderer: fallbackChaptersRenderer,
|
|
650
|
+
// v4.6.0 (#76 Secondary): exclude /print/ from sitemap — print-friendly
|
|
651
|
+
// view, crawl-redundant. Course-notes-profile default.
|
|
652
|
+
sitemapFilter: (page) => !page.includes("/print/")
|
|
620
653
|
});
|
|
621
654
|
|
|
622
655
|
// src/profiles/research-portfolio.ts
|
|
@@ -627,8 +660,8 @@ var researchPortfolioProfile = defineProfile({
|
|
|
627
660
|
references: true,
|
|
628
661
|
search: true,
|
|
629
662
|
print: true,
|
|
630
|
-
chapters:
|
|
631
|
-
//
|
|
663
|
+
chapters: true,
|
|
664
|
+
// v4.6.1 (#75 follow-up): default-on. Portfolios still ship their own /frontmatter/* + landing; /chapters/* renders the underlying chapter list.
|
|
632
665
|
convergence: false,
|
|
633
666
|
// tools-profile-specific
|
|
634
667
|
frontmatter: true,
|
|
@@ -824,19 +857,19 @@ function defineMdxComponents(components) {
|
|
|
824
857
|
}
|
|
825
858
|
|
|
826
859
|
// src/integration.ts
|
|
827
|
-
var
|
|
828
|
-
var
|
|
829
|
-
function
|
|
860
|
+
var BOOK_CONFIG_VIRTUAL_ID = "virtual:book-scaffold/book-config";
|
|
861
|
+
var BOOK_CONFIG_RESOLVED_ID = "\0" + BOOK_CONFIG_VIRTUAL_ID;
|
|
862
|
+
function makeBookConfigVitePlugin(config) {
|
|
830
863
|
const serialized = `export default ${JSON.stringify(config)};`;
|
|
831
864
|
return {
|
|
832
|
-
name: "book-scaffold:
|
|
865
|
+
name: "book-scaffold:book-config",
|
|
833
866
|
enforce: "pre",
|
|
834
867
|
resolveId(id) {
|
|
835
|
-
if (id ===
|
|
868
|
+
if (id === BOOK_CONFIG_VIRTUAL_ID) return BOOK_CONFIG_RESOLVED_ID;
|
|
836
869
|
return null;
|
|
837
870
|
},
|
|
838
871
|
load(id) {
|
|
839
|
-
if (id !==
|
|
872
|
+
if (id !== BOOK_CONFIG_RESOLVED_ID) return null;
|
|
840
873
|
return serialized;
|
|
841
874
|
}
|
|
842
875
|
};
|
|
@@ -887,10 +920,14 @@ function bookScaffoldIntegration(opts) {
|
|
|
887
920
|
routes: userOverrides = {},
|
|
888
921
|
extraStyles = [],
|
|
889
922
|
mdxComponentsModule,
|
|
890
|
-
// v4.5.0: landing-page data, propagated via
|
|
923
|
+
// v4.5.0: landing-page data, propagated via virtual module to /index.astro.
|
|
891
924
|
title,
|
|
892
925
|
description,
|
|
893
|
-
portfolio
|
|
926
|
+
portfolio,
|
|
927
|
+
// v4.6.0: book-level author + SEO config, propagated through the
|
|
928
|
+
// (renamed) book-config virtual module to Base.astro + Chapter.astro.
|
|
929
|
+
author,
|
|
930
|
+
seo
|
|
894
931
|
} = opts;
|
|
895
932
|
const def = PROFILES[profile];
|
|
896
933
|
const fmNormalized = normalizeFrontmatterConfig(userOverrides.frontmatter);
|
|
@@ -937,11 +974,16 @@ function bookScaffoldIntegration(opts) {
|
|
|
937
974
|
vite: {
|
|
938
975
|
plugins: [
|
|
939
976
|
makeMdxComponentsVitePlugin(resolvedMdxPath),
|
|
940
|
-
|
|
977
|
+
makeBookConfigVitePlugin({
|
|
941
978
|
title: title ?? null,
|
|
942
979
|
description: description ?? null,
|
|
943
980
|
portfolio: portfolio ?? false,
|
|
944
|
-
enabledRoutes: enabledRouteNames
|
|
981
|
+
enabledRoutes: enabledRouteNames,
|
|
982
|
+
author: author ?? null,
|
|
983
|
+
seo: {
|
|
984
|
+
ogImage: seo?.ogImage ?? null,
|
|
985
|
+
twitterHandle: seo?.twitterHandle ?? null
|
|
986
|
+
}
|
|
945
987
|
})
|
|
946
988
|
],
|
|
947
989
|
define: {
|
|
@@ -1045,19 +1087,36 @@ async function defineBookConfig(opts) {
|
|
|
1045
1087
|
]);
|
|
1046
1088
|
}
|
|
1047
1089
|
const resolvedPortfolio = opts.portfolio === false ? false : opts.portfolio ?? BRANDON_PORTFOLIO_DEFAULT;
|
|
1090
|
+
const profileSitemapFilter = PROFILES[profile]?.sitemapFilter;
|
|
1091
|
+
const sitemapFilter = opts.seo?.sitemap?.filter ?? profileSitemapFilter;
|
|
1092
|
+
const sitemapCustomPages = opts.seo?.sitemap?.customPages;
|
|
1093
|
+
const sitemapOptions = {};
|
|
1094
|
+
if (sitemapFilter) sitemapOptions.filter = sitemapFilter;
|
|
1095
|
+
if (sitemapCustomPages) sitemapOptions.customPages = sitemapCustomPages;
|
|
1048
1096
|
const integrations = [
|
|
1049
1097
|
mdx(),
|
|
1050
1098
|
preact(),
|
|
1099
|
+
// v4.6.0: @astrojs/sitemap default integration. Emits
|
|
1100
|
+
// /sitemap-index.xml + per-route sitemaps from the resolved `site:`
|
|
1101
|
+
// (defineBookConfig throws above if site is missing, so the URL is
|
|
1102
|
+
// always available to the sitemap integration here).
|
|
1103
|
+
sitemap(sitemapOptions),
|
|
1051
1104
|
bookScaffoldIntegration({
|
|
1052
1105
|
profile,
|
|
1053
1106
|
routes: mergedRoutes,
|
|
1054
1107
|
mdxComponentsModule,
|
|
1055
1108
|
extraStyles: mergedExtraStyles,
|
|
1056
1109
|
// v4.5.0: pass landing-page data through to the integration so it can
|
|
1057
|
-
// be exposed to the auto-injected /index.astro via
|
|
1110
|
+
// be exposed to the auto-injected /index.astro via the virtual module.
|
|
1058
1111
|
title: opts.title,
|
|
1059
1112
|
description: opts.description,
|
|
1060
|
-
portfolio: resolvedPortfolio
|
|
1113
|
+
portfolio: resolvedPortfolio,
|
|
1114
|
+
// v4.6.0: book-level author + SEO config (ogImage, twitterHandle),
|
|
1115
|
+
// propagated through the renamed `book-config` virtual module to
|
|
1116
|
+
// Base.astro + Chapter.astro. `seo.sitemap` is NOT passed through —
|
|
1117
|
+
// it's consumed below at config-time by the @astrojs/sitemap call.
|
|
1118
|
+
author: opts.author,
|
|
1119
|
+
seo: opts.seo ? { ogImage: opts.seo.ogImage, twitterHandle: opts.seo.twitterHandle } : void 0
|
|
1061
1120
|
}),
|
|
1062
1121
|
...mergedExtraIntegrations
|
|
1063
1122
|
];
|
|
@@ -1096,6 +1155,9 @@ async function defineBookConfig(opts) {
|
|
|
1096
1155
|
title: _title,
|
|
1097
1156
|
description: _description,
|
|
1098
1157
|
portfolio: _portfolio,
|
|
1158
|
+
// v4.6.0: strip new book-level SEO opts (author + seo block).
|
|
1159
|
+
author: _author,
|
|
1160
|
+
seo: _seo,
|
|
1099
1161
|
...rest
|
|
1100
1162
|
} = opts;
|
|
1101
1163
|
void _styles;
|
|
@@ -1110,6 +1172,8 @@ async function defineBookConfig(opts) {
|
|
|
1110
1172
|
void _title;
|
|
1111
1173
|
void _description;
|
|
1112
1174
|
void _portfolio;
|
|
1175
|
+
void _author;
|
|
1176
|
+
void _seo;
|
|
1113
1177
|
const katexExternals = wantsKatex ? [] : ["remark-math", "rehype-katex", "katex"];
|
|
1114
1178
|
const config = {
|
|
1115
1179
|
site,
|
package/dist/schemas.d.ts
CHANGED
package/dist/schemas.mjs
CHANGED
|
@@ -62,7 +62,14 @@ var academicChapterSchema = z.object({
|
|
|
62
62
|
tests_path: z.string().optional(),
|
|
63
63
|
notebook_path: z.string().optional(),
|
|
64
64
|
description: z.string().optional(),
|
|
65
|
-
draft: z.boolean().default(false)
|
|
65
|
+
draft: z.boolean().default(false),
|
|
66
|
+
// v4.6.0: optional SEO / article:* fields consumed by Chapter.astro.
|
|
67
|
+
// All optional; existing chapters without these continue to work.
|
|
68
|
+
author: z.string().optional(),
|
|
69
|
+
published: z.date().optional(),
|
|
70
|
+
updated: z.date().optional(),
|
|
71
|
+
tags: z.array(z.string()).default([]),
|
|
72
|
+
image: z.string().optional()
|
|
66
73
|
});
|
|
67
74
|
var toolsChapterSchema = z.object({
|
|
68
75
|
title: z.string().min(1),
|
|
@@ -74,7 +81,13 @@ var toolsChapterSchema = z.object({
|
|
|
74
81
|
sources: z.array(z.string()).default([]),
|
|
75
82
|
description: z.string().optional(),
|
|
76
83
|
draft: z.boolean().default(false),
|
|
77
|
-
updated: z.date().optional()
|
|
84
|
+
updated: z.date().optional(),
|
|
85
|
+
// v4.6.0: optional SEO / article:* fields consumed by Chapter.astro.
|
|
86
|
+
// `updated` already existed; the rest are new.
|
|
87
|
+
author: z.string().optional(),
|
|
88
|
+
published: z.date().optional(),
|
|
89
|
+
tags: z.array(z.string()).default([]),
|
|
90
|
+
image: z.string().optional()
|
|
78
91
|
});
|
|
79
92
|
var minimalChapterSchema = toolsChapterSchema;
|
|
80
93
|
var sourceTiersResearch = ["T1", "T2", "T3", "T4"];
|
|
@@ -101,7 +114,14 @@ var courseNotesChapterSchema = z.object({
|
|
|
101
114
|
last_verified: z.date(),
|
|
102
115
|
volatility: z.enum(volatilityLevels).default("architectural-pattern"),
|
|
103
116
|
sources: z.array(z.string()).default([]),
|
|
104
|
-
draft: z.boolean().default(false)
|
|
117
|
+
draft: z.boolean().default(false),
|
|
118
|
+
// v4.6.0: optional SEO / article:* fields consumed by Chapter.astro.
|
|
119
|
+
// `tags` already existed; the rest are new. `instructor` (line 130) is
|
|
120
|
+
// attribution metadata — distinct from `author` (the note-writer/curator).
|
|
121
|
+
author: z.string().optional(),
|
|
122
|
+
published: z.date().optional(),
|
|
123
|
+
updated: z.date().optional(),
|
|
124
|
+
image: z.string().optional()
|
|
105
125
|
});
|
|
106
126
|
var researchPortfolioChapterSchema = z.object({
|
|
107
127
|
// Identity
|
|
@@ -154,7 +174,12 @@ var researchPortfolioChapterSchema = z.object({
|
|
|
154
174
|
// Status + dates.
|
|
155
175
|
last_verified: z.date(),
|
|
156
176
|
updated: z.date().optional(),
|
|
157
|
-
draft: z.boolean().default(false)
|
|
177
|
+
draft: z.boolean().default(false),
|
|
178
|
+
// v4.6.0: optional SEO / article:* fields consumed by Chapter.astro.
|
|
179
|
+
// `tags` + `updated` already existed; `author` + `published` + `image` are new.
|
|
180
|
+
author: z.string().optional(),
|
|
181
|
+
published: z.date().optional(),
|
|
182
|
+
image: z.string().optional()
|
|
158
183
|
});
|
|
159
184
|
var sourcesSchema = z.object({
|
|
160
185
|
url: z.string().url(),
|
|
@@ -257,8 +282,8 @@ var academicProfile = defineProfile({
|
|
|
257
282
|
references: true,
|
|
258
283
|
search: true,
|
|
259
284
|
print: true,
|
|
260
|
-
chapters:
|
|
261
|
-
// academic
|
|
285
|
+
chapters: true,
|
|
286
|
+
// v4.6.1 (#75 follow-up): auto-injected /chapters/[...slug]/ + /chapters/ index. Pre-v4.3.0 academic books shipped their own listing; v4.6.0 (#76 Layer 3c) removed the consumer template assuming auto-injection. Default flipped here to close the gap. Consumers wanting their own listing override via `routes: { chapters: false }` + their own src/pages/chapters/* — see recipe 18.
|
|
262
287
|
convergence: false,
|
|
263
288
|
// tools-profile-specific
|
|
264
289
|
frontmatter: false,
|
|
@@ -272,8 +297,11 @@ var academicProfile = defineProfile({
|
|
|
272
297
|
},
|
|
273
298
|
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
|
|
274
299
|
katex: true,
|
|
275
|
-
chaptersRenderer: academicChaptersRenderer
|
|
300
|
+
chaptersRenderer: academicChaptersRenderer,
|
|
276
301
|
// v3.7.0 (#35) — owns /chapters semantics if consumer opts in via routes.chapters
|
|
302
|
+
// v4.6.0 (#76 Secondary): exclude /print/ from sitemap — print-friendly
|
|
303
|
+
// view, crawl-redundant. Academic-profile default.
|
|
304
|
+
sitemapFilter: (page) => !page.includes("/print/")
|
|
277
305
|
});
|
|
278
306
|
|
|
279
307
|
// src/lib/freshness.ts
|
|
@@ -463,7 +491,8 @@ var minimalProfile = defineProfile({
|
|
|
463
491
|
references: true,
|
|
464
492
|
search: true,
|
|
465
493
|
print: true,
|
|
466
|
-
chapters:
|
|
494
|
+
chapters: true,
|
|
495
|
+
// v4.6.1 (#75 follow-up): default-on across all profiles. Consumer override via routes: { chapters: false }.
|
|
467
496
|
convergence: false,
|
|
468
497
|
frontmatter: false,
|
|
469
498
|
// opt-in per book; see #7
|
|
@@ -487,8 +516,8 @@ var courseNotesProfile = defineProfile({
|
|
|
487
516
|
references: true,
|
|
488
517
|
search: true,
|
|
489
518
|
print: true,
|
|
490
|
-
chapters:
|
|
491
|
-
//
|
|
519
|
+
chapters: true,
|
|
520
|
+
// v4.6.1 (#75 follow-up): default-on. Multi-book consumers (DLAI-style) override via routes: { chapters: false } + own [book]/[slug] routes — see #15 deferred.
|
|
492
521
|
convergence: false,
|
|
493
522
|
frontmatter: false,
|
|
494
523
|
// opt-in per book; see #7
|
|
@@ -501,7 +530,10 @@ var courseNotesProfile = defineProfile({
|
|
|
501
530
|
},
|
|
502
531
|
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
|
|
503
532
|
// v3.7.0 (#35): course-notes schema has tools-style fields (chapter, volatility, sources) — fallback renderer dispatches via tools renderer
|
|
504
|
-
chaptersRenderer: fallbackChaptersRenderer
|
|
533
|
+
chaptersRenderer: fallbackChaptersRenderer,
|
|
534
|
+
// v4.6.0 (#76 Secondary): exclude /print/ from sitemap — print-friendly
|
|
535
|
+
// view, crawl-redundant. Course-notes-profile default.
|
|
536
|
+
sitemapFilter: (page) => !page.includes("/print/")
|
|
505
537
|
});
|
|
506
538
|
|
|
507
539
|
// src/profiles/research-portfolio.ts
|
|
@@ -512,8 +544,8 @@ var researchPortfolioProfile = defineProfile({
|
|
|
512
544
|
references: true,
|
|
513
545
|
search: true,
|
|
514
546
|
print: true,
|
|
515
|
-
chapters:
|
|
516
|
-
//
|
|
547
|
+
chapters: true,
|
|
548
|
+
// v4.6.1 (#75 follow-up): default-on. Portfolios still ship their own /frontmatter/* + landing; /chapters/* renders the underlying chapter list.
|
|
517
549
|
convergence: false,
|
|
518
550
|
// tools-profile-specific
|
|
519
551
|
frontmatter: true,
|
|
@@ -1,25 +1,33 @@
|
|
|
1
1
|
---
|
|
2
|
+
# required fields — schema will reject the chapter if missing
|
|
2
3
|
title: "Chapter N — Title goes here"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
last_verified: 2026-05-19 # required; YAML date (no quotes)
|
|
5
|
+
|
|
6
|
+
# optional — hierarchy (use whichever fits; all may be omitted)
|
|
7
|
+
slug: chN-short-slug # optional; defaults to filename
|
|
8
|
+
chapter: 1 # optional; tools-style numeric
|
|
9
|
+
part: 1 # optional; number OR academic-style string enum
|
|
6
10
|
week: 1 # optional; omit if not on a weekly cadence
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
|
|
12
|
+
# optional — status (AUTHORING state) vs freshness (EPISTEMIC type); see Recipe 13
|
|
13
|
+
status: prose_only # 'scaffolded'|'prose_only'|'code_only'|'chapter_only'|'reading_only'|'implemented'|'planned'
|
|
14
|
+
freshness: experimental-result # 'experimental-result'|'literature-survey'|'theoretical'|'reference'
|
|
15
|
+
|
|
16
|
+
# optional — provenance
|
|
17
|
+
volatility: feature-surface # 'stable-principle'|'architectural-pattern'|'feature-surface'
|
|
18
|
+
tags: # optional; freeform string array
|
|
11
19
|
- replace-me
|
|
12
20
|
- with
|
|
13
21
|
- real-tags
|
|
14
|
-
sources:
|
|
22
|
+
sources: # optional; structured inline T1-T4
|
|
15
23
|
- tier: T1
|
|
16
24
|
url: https://example.invalid/primary-source
|
|
17
25
|
label: Primary source (e.g., NVD CVE / arXiv paper / official spec)
|
|
18
26
|
- tier: T2
|
|
19
27
|
url: https://example.invalid/secondary
|
|
20
28
|
label: Secondary corroboration
|
|
21
|
-
|
|
22
|
-
draft: true
|
|
29
|
+
|
|
30
|
+
draft: true # optional; defaults to false
|
|
23
31
|
---
|
|
24
32
|
|
|
25
33
|
import PreReleaseBanner from '@brandon_m_behring/book-scaffold-astro/components/PreReleaseBanner.astro';
|
package/layouts/Base.astro
CHANGED
|
@@ -50,6 +50,10 @@ import '../styles/print.css';
|
|
|
50
50
|
import VersionSelector from '@brandon_m_behring/book-scaffold-astro/components/VersionSelector';
|
|
51
51
|
import ToolFilter from '@brandon_m_behring/book-scaffold-astro/components/ToolFilter';
|
|
52
52
|
import Sidebar from '../components/Sidebar.astro';
|
|
53
|
+
// v4.6.0: SEO meta tags read book-level identity (title fallback,
|
|
54
|
+
// description fallback, ogImage default, twitterHandle) from the
|
|
55
|
+
// book-config virtual module (was landing-config in v4.5.1).
|
|
56
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
53
57
|
|
|
54
58
|
const profile = import.meta.env.BOOK_PROFILE ?? 'minimal';
|
|
55
59
|
const showToolsChrome = profile !== 'academic';
|
|
@@ -59,9 +63,37 @@ interface Props {
|
|
|
59
63
|
description?: string;
|
|
60
64
|
lang?: string;
|
|
61
65
|
showSidebar?: boolean;
|
|
66
|
+
/** v4.6.0: Open Graph image URL (relative to site root, or absolute). */
|
|
67
|
+
ogImage?: string;
|
|
68
|
+
/** v4.6.0: Open Graph type. Defaults to 'website'; Chapter.astro passes 'article'. */
|
|
69
|
+
ogType?: string;
|
|
62
70
|
}
|
|
63
71
|
|
|
64
|
-
const {
|
|
72
|
+
const {
|
|
73
|
+
title,
|
|
74
|
+
description,
|
|
75
|
+
lang = 'en',
|
|
76
|
+
showSidebar = true,
|
|
77
|
+
ogImage,
|
|
78
|
+
ogType = 'website',
|
|
79
|
+
} = Astro.props;
|
|
80
|
+
|
|
81
|
+
// v4.6.0: canonical + og:url need Astro.site. defineBookConfig throws at
|
|
82
|
+
// config-load if `site` is missing, so Astro.site is guaranteed populated
|
|
83
|
+
// here. `new URL` resolves the relative pathname against it.
|
|
84
|
+
const canonicalURL = new URL(Astro.url.pathname, Astro.site).toString();
|
|
85
|
+
|
|
86
|
+
// Per D3: og:image emits only when an image is explicitly set — either via
|
|
87
|
+
// per-page Astro.props.ogImage or book-level bookConfig.seo.ogImage. No
|
|
88
|
+
// '/og-default.png' fallback to avoid broken-link meta tags on consumers
|
|
89
|
+
// without an OG image authored.
|
|
90
|
+
const resolvedOgImage = ogImage ?? bookConfig.seo?.ogImage ?? null;
|
|
91
|
+
const absoluteOgImage = resolvedOgImage
|
|
92
|
+
? new URL(resolvedOgImage, Astro.site).toString()
|
|
93
|
+
: null;
|
|
94
|
+
const twitterHandle = bookConfig.seo?.twitterHandle ?? null;
|
|
95
|
+
const ogSiteName = bookConfig.title ?? title;
|
|
96
|
+
const ogDescription = description ?? bookConfig.description ?? '';
|
|
65
97
|
---
|
|
66
98
|
|
|
67
99
|
<!doctype html>
|
|
@@ -74,6 +106,27 @@ const { title, description, lang = 'en', showSidebar = true } = Astro.props;
|
|
|
74
106
|
<meta name="generator" content={Astro.generator} />
|
|
75
107
|
<title>{title}</title>
|
|
76
108
|
{description && <meta name="description" content={description} />}
|
|
109
|
+
|
|
110
|
+
{/* v4.6.0: SEO meta-tag block (issue #76 Primary). canonical + og:* +
|
|
111
|
+
twitter:* on every page; article:* lives in Chapter.astro via the
|
|
112
|
+
Base `<slot name="head" />` (below). og:image + twitter:image emit
|
|
113
|
+
only when an image is explicitly set (per D3, avoids broken-link
|
|
114
|
+
OG tags). sitemap link points at @astrojs/sitemap's auto-emitted
|
|
115
|
+
index. */}
|
|
116
|
+
<link rel="canonical" href={canonicalURL} />
|
|
117
|
+
<link rel="sitemap" type="application/xml" href="/sitemap-index.xml" />
|
|
118
|
+
<meta property="og:title" content={title} />
|
|
119
|
+
{ogDescription && <meta property="og:description" content={ogDescription} />}
|
|
120
|
+
<meta property="og:url" content={canonicalURL} />
|
|
121
|
+
<meta property="og:type" content={ogType} />
|
|
122
|
+
<meta property="og:site_name" content={ogSiteName} />
|
|
123
|
+
{absoluteOgImage && <meta property="og:image" content={absoluteOgImage} />}
|
|
124
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
125
|
+
<meta name="twitter:title" content={title} />
|
|
126
|
+
{ogDescription && <meta name="twitter:description" content={ogDescription} />}
|
|
127
|
+
{absoluteOgImage && <meta name="twitter:image" content={absoluteOgImage} />}
|
|
128
|
+
{twitterHandle && <meta name="twitter:site" content={twitterHandle} />}
|
|
129
|
+
|
|
77
130
|
{/* FOUC prevention — apply saved theme before any paint. */}
|
|
78
131
|
<script is:inline>
|
|
79
132
|
(function () {
|
package/layouts/Chapter.astro
CHANGED
|
@@ -12,10 +12,17 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Driven by a CollectionEntry<'chapters'> passed in Astro.props.entry
|
|
14
14
|
* plus the `headings` array Astro's MDX renderer provides.
|
|
15
|
+
*
|
|
16
|
+
* v4.6.0: passes ogType="article" to Base + emits article:* meta tags via
|
|
17
|
+
* Base's `<slot name="head" />`. Article tags read from chapter frontmatter
|
|
18
|
+
* (`author`, `published`, `updated`, `tags`) with `author` falling back to
|
|
19
|
+
* book-level `bookConfig.author`. `entry.data.image` (chapter-specific OG
|
|
20
|
+
* image) is passed as `ogImage` for richer per-chapter social cards.
|
|
15
21
|
*/
|
|
16
22
|
import type { CollectionEntry } from 'astro:content';
|
|
17
23
|
import type { MarkdownHeading } from 'astro';
|
|
18
24
|
import Base from './Base.astro';
|
|
25
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
19
26
|
import ChapterHeader from '../components/ChapterHeader.astro';
|
|
20
27
|
import ChapterTOC from '../components/ChapterTOC.astro';
|
|
21
28
|
import ChapterNav from '../components/ChapterNav.astro';
|
|
@@ -25,9 +32,36 @@ interface Props {
|
|
|
25
32
|
headings: MarkdownHeading[];
|
|
26
33
|
}
|
|
27
34
|
const { entry, headings } = Astro.props;
|
|
35
|
+
|
|
36
|
+
// v4.6.0: article:* meta-tag data. All fields are Zod-optional in the
|
|
37
|
+
// chapter schemas (academic / tools / course-notes / research-portfolio),
|
|
38
|
+
// so any of these may be undefined. `author` falls back to bookConfig.author
|
|
39
|
+
// (the top-level defineBookConfig field) so single-author books don't
|
|
40
|
+
// repeat themselves per chapter.
|
|
41
|
+
const articleAuthor =
|
|
42
|
+
(entry.data as { author?: string }).author ?? bookConfig.author ?? null;
|
|
43
|
+
const articlePublished = (entry.data as { published?: Date }).published;
|
|
44
|
+
const articleUpdated = (entry.data as { updated?: Date }).updated;
|
|
45
|
+
const articleTags = ((entry.data as { tags?: string[] }).tags ?? []) as string[];
|
|
46
|
+
const chapterImage = (entry.data as { image?: string }).image;
|
|
28
47
|
---
|
|
29
48
|
|
|
30
|
-
<Base
|
|
49
|
+
<Base
|
|
50
|
+
title={entry.data.title}
|
|
51
|
+
description={entry.data.description}
|
|
52
|
+
ogType="article"
|
|
53
|
+
ogImage={chapterImage}
|
|
54
|
+
>
|
|
55
|
+
<Fragment slot="head">
|
|
56
|
+
{articleAuthor && <meta property="article:author" content={articleAuthor} />}
|
|
57
|
+
{articlePublished && (
|
|
58
|
+
<meta property="article:published_time" content={articlePublished.toISOString()} />
|
|
59
|
+
)}
|
|
60
|
+
{articleUpdated && (
|
|
61
|
+
<meta property="article:modified_time" content={articleUpdated.toISOString()} />
|
|
62
|
+
)}
|
|
63
|
+
{articleTags.map((t) => <meta property="article:tag" content={t} />)}
|
|
64
|
+
</Fragment>
|
|
31
65
|
<article class="prose">
|
|
32
66
|
<ChapterHeader data={entry.data} />
|
|
33
67
|
<ChapterTOC headings={headings} />
|
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.
|
|
4
|
+
"version": "4.6.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Brandon Behring",
|
|
@@ -152,6 +152,7 @@
|
|
|
152
152
|
"remark-math": { "optional": true }
|
|
153
153
|
},
|
|
154
154
|
"dependencies": {
|
|
155
|
+
"@astrojs/sitemap": "^3.6.1",
|
|
155
156
|
"@citation-js/core": "^0.7.21",
|
|
156
157
|
"@citation-js/plugin-bibtex": "^0.7.21",
|
|
157
158
|
"@fontsource-variable/roboto": "^5.2.10",
|
package/pages/index.astro
CHANGED
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
* config needed.
|
|
10
10
|
*
|
|
11
11
|
* Reads book identity + portfolio + enabled-routes from the
|
|
12
|
-
* `virtual:book-scaffold/
|
|
13
|
-
*
|
|
12
|
+
* `virtual:book-scaffold/book-config` virtual module exposed by
|
|
13
|
+
* makeBookConfigVitePlugin (renamed from makeLandingConfigVitePlugin in
|
|
14
|
+
* v4.6.0 since Base.astro now also imports the module on every page).
|
|
15
|
+
* v4.5.0 used
|
|
14
16
|
* import.meta.env.BOOK_* env vars; that pattern was vulnerable to silent
|
|
15
17
|
* override by consumer .env files (caught during DML deploy when a stale
|
|
16
18
|
* `BOOK_TITLE=web` in web/.env overrode defineBookConfig({title})). The
|
|
@@ -31,7 +33,7 @@
|
|
|
31
33
|
* or defineBookConfig({ portfolio: false })
|
|
32
34
|
*/
|
|
33
35
|
import Base from '../layouts/Base.astro';
|
|
34
|
-
import bookConfig from 'virtual:book-scaffold/
|
|
36
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
35
37
|
|
|
36
38
|
const title = bookConfig.title ?? 'book-scaffold-astro';
|
|
37
39
|
const description = bookConfig.description;
|