@brandon_m_behring/book-scaffold-astro 4.5.1 → 4.6.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/dist/index.d.ts +2 -2
- package/dist/index.mjs +81 -18
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.mjs +37 -6
- 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/18-chapter-route-ownership.md +87 -0
- package/recipes/19-prevalidate-hook.md +113 -0
- package/scripts/validate.mjs +78 -0
- package/src/profile-kit.ts +17 -0
- package/src/profiles/academic.ts +3 -0
- package/src/profiles/course-notes.ts +3 -0
- package/src/schemas.ts +27 -0
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(),
|
|
@@ -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
|
|
@@ -616,7 +645,10 @@ var courseNotesProfile = defineProfile({
|
|
|
616
645
|
},
|
|
617
646
|
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
|
|
618
647
|
// v3.7.0 (#35): course-notes schema has tools-style fields (chapter, volatility, sources) — fallback renderer dispatches via tools renderer
|
|
619
|
-
chaptersRenderer: fallbackChaptersRenderer
|
|
648
|
+
chaptersRenderer: fallbackChaptersRenderer,
|
|
649
|
+
// v4.6.0 (#76 Secondary): exclude /print/ from sitemap — print-friendly
|
|
650
|
+
// view, crawl-redundant. Course-notes-profile default.
|
|
651
|
+
sitemapFilter: (page) => !page.includes("/print/")
|
|
620
652
|
});
|
|
621
653
|
|
|
622
654
|
// src/profiles/research-portfolio.ts
|
|
@@ -824,19 +856,19 @@ function defineMdxComponents(components) {
|
|
|
824
856
|
}
|
|
825
857
|
|
|
826
858
|
// src/integration.ts
|
|
827
|
-
var
|
|
828
|
-
var
|
|
829
|
-
function
|
|
859
|
+
var BOOK_CONFIG_VIRTUAL_ID = "virtual:book-scaffold/book-config";
|
|
860
|
+
var BOOK_CONFIG_RESOLVED_ID = "\0" + BOOK_CONFIG_VIRTUAL_ID;
|
|
861
|
+
function makeBookConfigVitePlugin(config) {
|
|
830
862
|
const serialized = `export default ${JSON.stringify(config)};`;
|
|
831
863
|
return {
|
|
832
|
-
name: "book-scaffold:
|
|
864
|
+
name: "book-scaffold:book-config",
|
|
833
865
|
enforce: "pre",
|
|
834
866
|
resolveId(id) {
|
|
835
|
-
if (id ===
|
|
867
|
+
if (id === BOOK_CONFIG_VIRTUAL_ID) return BOOK_CONFIG_RESOLVED_ID;
|
|
836
868
|
return null;
|
|
837
869
|
},
|
|
838
870
|
load(id) {
|
|
839
|
-
if (id !==
|
|
871
|
+
if (id !== BOOK_CONFIG_RESOLVED_ID) return null;
|
|
840
872
|
return serialized;
|
|
841
873
|
}
|
|
842
874
|
};
|
|
@@ -887,10 +919,14 @@ function bookScaffoldIntegration(opts) {
|
|
|
887
919
|
routes: userOverrides = {},
|
|
888
920
|
extraStyles = [],
|
|
889
921
|
mdxComponentsModule,
|
|
890
|
-
// v4.5.0: landing-page data, propagated via
|
|
922
|
+
// v4.5.0: landing-page data, propagated via virtual module to /index.astro.
|
|
891
923
|
title,
|
|
892
924
|
description,
|
|
893
|
-
portfolio
|
|
925
|
+
portfolio,
|
|
926
|
+
// v4.6.0: book-level author + SEO config, propagated through the
|
|
927
|
+
// (renamed) book-config virtual module to Base.astro + Chapter.astro.
|
|
928
|
+
author,
|
|
929
|
+
seo
|
|
894
930
|
} = opts;
|
|
895
931
|
const def = PROFILES[profile];
|
|
896
932
|
const fmNormalized = normalizeFrontmatterConfig(userOverrides.frontmatter);
|
|
@@ -937,11 +973,16 @@ function bookScaffoldIntegration(opts) {
|
|
|
937
973
|
vite: {
|
|
938
974
|
plugins: [
|
|
939
975
|
makeMdxComponentsVitePlugin(resolvedMdxPath),
|
|
940
|
-
|
|
976
|
+
makeBookConfigVitePlugin({
|
|
941
977
|
title: title ?? null,
|
|
942
978
|
description: description ?? null,
|
|
943
979
|
portfolio: portfolio ?? false,
|
|
944
|
-
enabledRoutes: enabledRouteNames
|
|
980
|
+
enabledRoutes: enabledRouteNames,
|
|
981
|
+
author: author ?? null,
|
|
982
|
+
seo: {
|
|
983
|
+
ogImage: seo?.ogImage ?? null,
|
|
984
|
+
twitterHandle: seo?.twitterHandle ?? null
|
|
985
|
+
}
|
|
945
986
|
})
|
|
946
987
|
],
|
|
947
988
|
define: {
|
|
@@ -1045,19 +1086,36 @@ async function defineBookConfig(opts) {
|
|
|
1045
1086
|
]);
|
|
1046
1087
|
}
|
|
1047
1088
|
const resolvedPortfolio = opts.portfolio === false ? false : opts.portfolio ?? BRANDON_PORTFOLIO_DEFAULT;
|
|
1089
|
+
const profileSitemapFilter = PROFILES[profile]?.sitemapFilter;
|
|
1090
|
+
const sitemapFilter = opts.seo?.sitemap?.filter ?? profileSitemapFilter;
|
|
1091
|
+
const sitemapCustomPages = opts.seo?.sitemap?.customPages;
|
|
1092
|
+
const sitemapOptions = {};
|
|
1093
|
+
if (sitemapFilter) sitemapOptions.filter = sitemapFilter;
|
|
1094
|
+
if (sitemapCustomPages) sitemapOptions.customPages = sitemapCustomPages;
|
|
1048
1095
|
const integrations = [
|
|
1049
1096
|
mdx(),
|
|
1050
1097
|
preact(),
|
|
1098
|
+
// v4.6.0: @astrojs/sitemap default integration. Emits
|
|
1099
|
+
// /sitemap-index.xml + per-route sitemaps from the resolved `site:`
|
|
1100
|
+
// (defineBookConfig throws above if site is missing, so the URL is
|
|
1101
|
+
// always available to the sitemap integration here).
|
|
1102
|
+
sitemap(sitemapOptions),
|
|
1051
1103
|
bookScaffoldIntegration({
|
|
1052
1104
|
profile,
|
|
1053
1105
|
routes: mergedRoutes,
|
|
1054
1106
|
mdxComponentsModule,
|
|
1055
1107
|
extraStyles: mergedExtraStyles,
|
|
1056
1108
|
// v4.5.0: pass landing-page data through to the integration so it can
|
|
1057
|
-
// be exposed to the auto-injected /index.astro via
|
|
1109
|
+
// be exposed to the auto-injected /index.astro via the virtual module.
|
|
1058
1110
|
title: opts.title,
|
|
1059
1111
|
description: opts.description,
|
|
1060
|
-
portfolio: resolvedPortfolio
|
|
1112
|
+
portfolio: resolvedPortfolio,
|
|
1113
|
+
// v4.6.0: book-level author + SEO config (ogImage, twitterHandle),
|
|
1114
|
+
// propagated through the renamed `book-config` virtual module to
|
|
1115
|
+
// Base.astro + Chapter.astro. `seo.sitemap` is NOT passed through —
|
|
1116
|
+
// it's consumed below at config-time by the @astrojs/sitemap call.
|
|
1117
|
+
author: opts.author,
|
|
1118
|
+
seo: opts.seo ? { ogImage: opts.seo.ogImage, twitterHandle: opts.seo.twitterHandle } : void 0
|
|
1061
1119
|
}),
|
|
1062
1120
|
...mergedExtraIntegrations
|
|
1063
1121
|
];
|
|
@@ -1096,6 +1154,9 @@ async function defineBookConfig(opts) {
|
|
|
1096
1154
|
title: _title,
|
|
1097
1155
|
description: _description,
|
|
1098
1156
|
portfolio: _portfolio,
|
|
1157
|
+
// v4.6.0: strip new book-level SEO opts (author + seo block).
|
|
1158
|
+
author: _author,
|
|
1159
|
+
seo: _seo,
|
|
1099
1160
|
...rest
|
|
1100
1161
|
} = opts;
|
|
1101
1162
|
void _styles;
|
|
@@ -1110,6 +1171,8 @@ async function defineBookConfig(opts) {
|
|
|
1110
1171
|
void _title;
|
|
1111
1172
|
void _description;
|
|
1112
1173
|
void _portfolio;
|
|
1174
|
+
void _author;
|
|
1175
|
+
void _seo;
|
|
1113
1176
|
const katexExternals = wantsKatex ? [] : ["remark-math", "rehype-katex", "katex"];
|
|
1114
1177
|
const config = {
|
|
1115
1178
|
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(),
|
|
@@ -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
|
|
@@ -501,7 +529,10 @@ var courseNotesProfile = defineProfile({
|
|
|
501
529
|
},
|
|
502
530
|
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
|
|
503
531
|
// v3.7.0 (#35): course-notes schema has tools-style fields (chapter, volatility, sources) — fallback renderer dispatches via tools renderer
|
|
504
|
-
chaptersRenderer: fallbackChaptersRenderer
|
|
532
|
+
chaptersRenderer: fallbackChaptersRenderer,
|
|
533
|
+
// v4.6.0 (#76 Secondary): exclude /print/ from sitemap — print-friendly
|
|
534
|
+
// view, crawl-redundant. Course-notes-profile default.
|
|
535
|
+
sitemapFilter: (page) => !page.includes("/print/")
|
|
505
536
|
});
|
|
506
537
|
|
|
507
538
|
// src/profiles/research-portfolio.ts
|
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.0",
|
|
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;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Recipe 18 — Chapter-route ownership (v4.3.0+)
|
|
2
|
+
|
|
3
|
+
Since v4.3.0, `book-scaffold-astro` **auto-injects** the per-chapter route `/chapters/[...slug]/` when `routes.chapters: true` is in `defineBookConfig`. Consumers that pre-date v4.3.0 (or were scaffolded from older templates) may carry their own `src/pages/chapters/[...slug].astro` that **shadows** the auto-injected one — Astro's filesystem routing picks the consumer file over `injectRoute`, with no error or warning until v4.6.0.
|
|
4
|
+
|
|
5
|
+
This recipe explains the three valid states and how to pick one.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## TL;DR
|
|
10
|
+
|
|
11
|
+
If your `src/pages/chapters/[...slug].astro` is a stock copy from a pre-v4.3.0 template (mechanical boilerplate, no custom layout work), **delete it**. The scaffold's auto-injected route handles every academic / tools / research-portfolio consumer's per-chapter rendering identically.
|
|
12
|
+
|
|
13
|
+
If you customized the file (e.g., book-specific chapter chrome, citation styling, sidebar variations), **keep it** AND set `routes: { chapters: false }` in your `defineBookConfig` to signal the override is intentional.
|
|
14
|
+
|
|
15
|
+
The `book-scaffold validate` CLI emits a warning (v4.6.0+) when it detects a consumer-owned file without the `chapters: false` override, prompting you to pick one of the two states.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Three states
|
|
20
|
+
|
|
21
|
+
### State 1 — Default (90% case, recommended)
|
|
22
|
+
|
|
23
|
+
No `src/pages/chapters/[...slug].astro` in your consumer repo. Scaffold's auto-injected route handles per-chapter rendering. `routes.chapters` defaults to whatever your profile sets (`true` for academic by default).
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
your-book/
|
|
27
|
+
└── src/
|
|
28
|
+
└── pages/
|
|
29
|
+
└── chapters.astro ← optional: the /chapters/ index page (also auto-injected if missing)
|
|
30
|
+
# no chapters/ directory; scaffold provides /chapters/[...slug]/
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Pros: zero per-consumer code. Future scaffold improvements (e.g., richer chapter sidebars, better TOC rendering) propagate automatically on the next scaffold bump.
|
|
34
|
+
|
|
35
|
+
### State 2 — Intentional override (custom chapter layout)
|
|
36
|
+
|
|
37
|
+
`src/pages/chapters/[...slug].astro` exists in your consumer repo AND `defineBookConfig({ routes: { chapters: false } })` is set. The consumer file owns the route; scaffold's auto-injected route is suppressed.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// astro.config.mjs
|
|
41
|
+
export default await defineBookConfig({
|
|
42
|
+
styles: [academicStyle],
|
|
43
|
+
site: 'https://your-book.example/',
|
|
44
|
+
routes: { chapters: false }, // ← signal: I own the chapter route
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
your-book/
|
|
50
|
+
└── src/
|
|
51
|
+
└── pages/
|
|
52
|
+
└── chapters/
|
|
53
|
+
└── [...slug].astro ← consumer-owned; scaffold defers
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Pros: full control over per-chapter chrome, sidebar, TOC layout, etc. Cons: must hand-maintain when scaffold ships chapter-level improvements (the consumer file is now your source of truth for chapter rendering).
|
|
57
|
+
|
|
58
|
+
### State 3 — Anti-pattern (shadow, fixed by v4.6 validator warning)
|
|
59
|
+
|
|
60
|
+
`src/pages/chapters/[...slug].astro` exists in your consumer repo BUT `routes.chapters` is undefined or `true`. Astro's filesystem routing wins, so the consumer file renders — but the scaffold is also trying to inject a route at the same pattern. Functionally works today (Astro picks the consumer file), but:
|
|
61
|
+
|
|
62
|
+
- Future scaffold improvements to chapter rendering silently no-op for this consumer.
|
|
63
|
+
- Dual-source ownership is confusing for future maintainers.
|
|
64
|
+
- `book-scaffold validate` emits a warning in v4.6.0+.
|
|
65
|
+
|
|
66
|
+
**Fix**: pick State 1 (delete the file) or State 2 (set `chapters: false`).
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Migration from pre-v4.3.0 templates
|
|
71
|
+
|
|
72
|
+
Reference consumers `double_ml_time_series` (Phase 1f, 2026-05-26) and `ssm-foundations` (Phase 1c, 2026-05-26) both migrated from State 3 → State 1 — the consumer-owned `src/pages/chapters/[...slug].astro` was a mechanical copy of the same boilerplate that scaffold v4.3.0+ auto-injects, so deletion was lossless.
|
|
73
|
+
|
|
74
|
+
Migration steps:
|
|
75
|
+
|
|
76
|
+
1. `diff` your consumer's `src/pages/chapters/[...slug].astro` against the scaffold's `package/pages/chapters/[...slug].astro` to confirm no customization. If they're equivalent (ignoring trivial formatting), proceed.
|
|
77
|
+
2. `rm src/pages/chapters/'[...slug].astro'` (note the shell-escaped brackets).
|
|
78
|
+
3. `git commit -m "chore: delete chapter-route override (scaffold v4.3.0+ auto-injects)"`.
|
|
79
|
+
4. Push + redeploy. The scaffold's auto-injected route takes over without behavior change.
|
|
80
|
+
|
|
81
|
+
If your file has real customization, prefer State 2: keep the file and add `routes: { chapters: false }` to `defineBookConfig`.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Why this matters
|
|
86
|
+
|
|
87
|
+
Layer-3 cleanup is part of [issue #76](https://github.com/brandon-behring/book-scaffold-astro/issues/76)'s v4.6 bundle. Related companion: [recipe 19 — prevalidate-hook](./19-prevalidate-hook.md), which fixes another silent-CI gap surfaced during the same first-deploy sessions.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Recipe 19 — `prevalidate` npm hook (v4.6.0+)
|
|
2
|
+
|
|
3
|
+
`book-scaffold validate` checks `<Cite key="...">` against `src/data/references.json` (academic profile) and `<XRef id="...">` against `src/data/labels.json`. Both JSON files are **derived artifacts** regenerated from `bibliography.bib` + chapter MDX by `book-scaffold build-bib` + `book-scaffold build-labels`. Both are gitignored.
|
|
4
|
+
|
|
5
|
+
When `npm run validate` runs **standalone** (e.g., a reusable deploy workflow runs only the validate command, without the full `npm run build` chain), the prereq scripts don't fire and the validator chokes on apparently-missing keys.
|
|
6
|
+
|
|
7
|
+
The `prevalidate` npm lifecycle hook is the canonical fix: it auto-runs the prereqs whenever `npm run validate` is invoked, regardless of how validate is called.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## TL;DR
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"scripts": {
|
|
16
|
+
"prevalidate": "npm run build:bib && npm run build:labels --if-present",
|
|
17
|
+
"validate": "book-scaffold validate"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`npm run validate` → automatically runs `prevalidate` first (build:bib + build:labels) → then runs `validate`. CI and local behave identically; no separate `ci:validate` wrapper script needed.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Why the workaround was needed
|
|
27
|
+
|
|
28
|
+
`brandon-behring/deploy-workflows@v1` (the reusable Cloudflare Workers deploy) runs `npm run <validate-command>` between `npm ci` and `npm run build`. Without the `prevalidate` hook OR an explicit wrapper script, the validate step ran against missing artifacts.
|
|
29
|
+
|
|
30
|
+
The Phase 1c first-deploy of `ssm-foundations` (2026-05-26) hit this — validate emitted 25+ "Unknown bibkey" errors that pointed at chapter content instead of the missing `references.json` artifact. The deploy-time fix shipped as a `ci:validate` wrapper:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"scripts": {
|
|
35
|
+
"ci:validate": "npm run build:bib && npm run build:labels --if-present && npm run validate"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
…with `validate-command: ci:validate` in `.github/workflows/deploy.yml`. This worked but introduced consumer-side ceremony for what is structurally a scaffold-level convention.
|
|
41
|
+
|
|
42
|
+
The cleaner long-term fix is the `prevalidate` npm-lifecycle hook (this recipe). v4.6.0's `create-book` automatically scaffolds it for academic + research-portfolio profiles.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Migration: from `ci:validate` to `prevalidate`
|
|
47
|
+
|
|
48
|
+
Existing consumers (DML, ssm, dlai when it ships) that adopted the `ci:validate` wrapper during 2026-05-26 deploys can migrate when they bump to scaffold `^4.6.0`. Three-file mechanical change per consumer:
|
|
49
|
+
|
|
50
|
+
### 1. `package.json` — rename `ci:validate` → `prevalidate`
|
|
51
|
+
|
|
52
|
+
```diff
|
|
53
|
+
{
|
|
54
|
+
"scripts": {
|
|
55
|
+
- "ci:validate": "npm run build:bib && npm run build:labels --if-present && npm run validate",
|
|
56
|
+
+ "prevalidate": "npm run build:bib && npm run build:labels --if-present",
|
|
57
|
+
"validate": "book-scaffold validate"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Note the renamed script no longer needs to call `validate` itself — npm's lifecycle invokes it automatically after `prevalidate` completes.
|
|
63
|
+
|
|
64
|
+
### 2. `.github/workflows/deploy.yml` — revert `validate-command`
|
|
65
|
+
|
|
66
|
+
```diff
|
|
67
|
+
jobs:
|
|
68
|
+
deploy:
|
|
69
|
+
uses: brandon-behring/deploy-workflows/.github/workflows/deploy-astro-worker.yml@v2
|
|
70
|
+
secrets: inherit
|
|
71
|
+
with:
|
|
72
|
+
working-directory: web
|
|
73
|
+
- validate-command: ci:validate
|
|
74
|
+
+ validate-command: validate
|
|
75
|
+
enable-pr-previews: true
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The reusable workflow's `validate-command` now points at the native `validate` script; the `prevalidate` hook handles its own prereqs transparently.
|
|
79
|
+
|
|
80
|
+
### 3. Simplify `prebuild` (optional)
|
|
81
|
+
|
|
82
|
+
If the consumer's `prebuild` still has the long chain:
|
|
83
|
+
|
|
84
|
+
```diff
|
|
85
|
+
{
|
|
86
|
+
"scripts": {
|
|
87
|
+
- "prebuild": "npm run build:bib --if-present && npm run build:labels --if-present && npm run validate --if-present",
|
|
88
|
+
+ "prebuild": "npm run validate --if-present",
|
|
89
|
+
"build": "astro build && pagefind --site dist"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Now `npm run build` → triggers `prebuild` (which runs `validate`) → triggers `prevalidate` (which runs the prereqs) → validate runs cleanly. Single source of truth for the prereq chain.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## When `prevalidate` is NOT needed
|
|
99
|
+
|
|
100
|
+
Profiles that don't run cite-key or XRef validation don't need `prevalidate`. Specifically:
|
|
101
|
+
|
|
102
|
+
- `tools`, `minimal`: no `<Cite>` resolution against `references.json`.
|
|
103
|
+
- `course-notes`: depends on whether the consumer uses `<Cite>` (some do for source-tier attribution).
|
|
104
|
+
|
|
105
|
+
For these profiles, `book-scaffold validate` operates on chapter structure + XRef IDs only; `prevalidate` would be a no-op. The v4.6.0 create-book template adds `prevalidate` ONLY for academic + research-portfolio.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Why this matters
|
|
110
|
+
|
|
111
|
+
Recipe 19 is part of [issue #76](https://github.com/brandon-behring/book-scaffold-astro/issues/76)'s v4.6 bundle. Companion: [recipe 18 — chapter-route ownership](./18-chapter-route-ownership.md), which fixes another silent-CI surface from the same Phase 1c first-deploys.
|
|
112
|
+
|
|
113
|
+
The `prevalidate` convention also closes [issue #77](https://github.com/brandon-behring/book-scaffold-astro/issues/77) — the v4.6 validator now emits a single re-framed error pointing at this recipe when `references.json` is missing, replacing the noisy 25-symptom output.
|
package/scripts/validate.mjs
CHANGED
|
@@ -121,6 +121,46 @@ const PRESET =
|
|
|
121
121
|
const PROFILE = PRESET;
|
|
122
122
|
const REPO_ROOT = process.env.BOOK_REPO_ROOT ?? null;
|
|
123
123
|
|
|
124
|
+
// v4.6.0 (issue #76 Layer 3b): chapter-route shadow warning. Detect a
|
|
125
|
+
// consumer-owned `src/pages/chapters/[...slug].astro` that shadows the
|
|
126
|
+
// scaffold v4.3.0+ auto-injected route. Non-blocking — emits to stderr,
|
|
127
|
+
// validate continues normally. Suppressed when the consumer explicitly
|
|
128
|
+
// disabled the scaffold's chapter route (intentional override).
|
|
129
|
+
//
|
|
130
|
+
// Edge cases per issue #76:
|
|
131
|
+
// file + routes.chapters undefined/true → WARN
|
|
132
|
+
// file + routes.chapters: false → silent (intentional override)
|
|
133
|
+
// no file (any routes config) → silent
|
|
134
|
+
//
|
|
135
|
+
// Heuristic for "routes.chapters: false": regex-grep astro.config.mjs for
|
|
136
|
+
// the literal `chapters: false`. Light-touch detection that matches the
|
|
137
|
+
// issue's warning-not-error intent; consumers wanting a stricter detector
|
|
138
|
+
// can run `astro check` separately.
|
|
139
|
+
{
|
|
140
|
+
const consumerChapterRoute = resolve(ROOT, 'src/pages/chapters/[...slug].astro');
|
|
141
|
+
if (existsSync(consumerChapterRoute)) {
|
|
142
|
+
const astroConfigPath = resolve(ROOT, 'astro.config.mjs');
|
|
143
|
+
let chaptersDisabled = false;
|
|
144
|
+
if (existsSync(astroConfigPath)) {
|
|
145
|
+
const astroConfig = readFileSync(astroConfigPath, 'utf8');
|
|
146
|
+
// Match `chapters: false` (with optional whitespace) inside a routes
|
|
147
|
+
// object. Slight false-positive risk on commented-out code; acceptable
|
|
148
|
+
// for a non-blocking warning.
|
|
149
|
+
chaptersDisabled = /\bchapters\s*:\s*false\b/.test(astroConfig);
|
|
150
|
+
}
|
|
151
|
+
if (!chaptersDisabled) {
|
|
152
|
+
console.warn(
|
|
153
|
+
`\n⚠ Consumer-owned chapter route at src/pages/chapters/[...slug].astro\n` +
|
|
154
|
+
` shadows the scaffold v4.3.0+ auto-injected route. Either:\n` +
|
|
155
|
+
` • Delete the consumer file to defer to the scaffold (recommended), OR\n` +
|
|
156
|
+
` • Set 'routes: { chapters: false }' in defineBookConfig to keep\n` +
|
|
157
|
+
` your override (intentional).\n` +
|
|
158
|
+
` See: package/recipes/18-chapter-route-ownership.md\n`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
124
164
|
const errors = [];
|
|
125
165
|
const warnings = [];
|
|
126
166
|
const fail = (file, line, msg) => errors.push({ file, line, msg });
|
|
@@ -231,6 +271,44 @@ for (const rel of chapterFiles) {
|
|
|
231
271
|
}
|
|
232
272
|
}
|
|
233
273
|
|
|
274
|
+
// ===== v4.6.0 (issue #77): missing-prereq re-framing =====
|
|
275
|
+
//
|
|
276
|
+
// When errors are downstream symptoms of a missing artifact (references.json
|
|
277
|
+
// or labels.json), abort with ONE leading error pointing at the prereq
|
|
278
|
+
// instead of printing 25 "Unknown bibkey" / "Unknown XRef" symptoms. Single
|
|
279
|
+
// clean signal: fix the prereq. Per D12 of the v4.6.0 plan.
|
|
280
|
+
{
|
|
281
|
+
if (PROFILE === 'academic') {
|
|
282
|
+
const refsPath = join(DATA_DIR, 'references.json');
|
|
283
|
+
const hasBibkeyErrors = errors.some((e) => /Unknown bibkey/.test(e.msg));
|
|
284
|
+
if (hasBibkeyErrors && !existsSync(refsPath)) {
|
|
285
|
+
console.error(
|
|
286
|
+
`\n✗ Validate cannot run: src/data/references.json is missing.\n\n` +
|
|
287
|
+
`This file is generated from bibliography.bib by 'npm run build:bib'.\n` +
|
|
288
|
+
`Run that first, OR adopt the prevalidate npm hook convention so\n` +
|
|
289
|
+
`'npm run validate' regenerates it automatically:\n\n` +
|
|
290
|
+
` "prevalidate": "npm run build:bib && npm run build:labels --if-present"\n` +
|
|
291
|
+
` "validate": "book-scaffold validate"\n\n` +
|
|
292
|
+
`See package/recipes/19-prevalidate-hook.md.\n`,
|
|
293
|
+
);
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const labelsPath = join(DATA_DIR, 'labels.json');
|
|
298
|
+
const hasXrefErrors = errors.some((e) => /Unknown XRef/.test(e.msg));
|
|
299
|
+
if (hasXrefErrors && !existsSync(labelsPath)) {
|
|
300
|
+
console.error(
|
|
301
|
+
`\n✗ Validate cannot run: src/data/labels.json is missing.\n\n` +
|
|
302
|
+
`This file is generated from <Theorem id="..."> and <Figure id="..."> markers\n` +
|
|
303
|
+
`in chapter MDX by 'npm run build:labels'. Run that first, OR adopt the\n` +
|
|
304
|
+
`prevalidate npm hook convention so 'npm run validate' regenerates it:\n\n` +
|
|
305
|
+
` "prevalidate": "npm run build:bib && npm run build:labels --if-present"\n\n` +
|
|
306
|
+
`See package/recipes/19-prevalidate-hook.md.\n`,
|
|
307
|
+
);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
234
312
|
// ===== Report =====
|
|
235
313
|
const format = ({ file, line, msg }) => ` ${file}:${line} ${msg}`;
|
|
236
314
|
if (warnings.length > 0) {
|
package/src/profile-kit.ts
CHANGED
|
@@ -105,6 +105,23 @@ export interface ProfileDefinition {
|
|
|
105
105
|
* fallbackChaptersRenderer (field-presence dispatch) at route render time.
|
|
106
106
|
*/
|
|
107
107
|
chaptersRenderer?: ChaptersRenderer;
|
|
108
|
+
/**
|
|
109
|
+
* v4.6.0 (issue #76 Secondary): per-profile default sitemap filter.
|
|
110
|
+
* Predicate run against every page URL by `@astrojs/sitemap` — return
|
|
111
|
+
* true to include in sitemap, false to exclude.
|
|
112
|
+
*
|
|
113
|
+
* Defaults:
|
|
114
|
+
* academic + course-notes → excludes `/print/` (print-friendly view,
|
|
115
|
+
* crawl-redundant)
|
|
116
|
+
* tools + minimal + research-portfolio → omitted (include everything)
|
|
117
|
+
*
|
|
118
|
+
* Per D7 of the v4.6.0 plan: when a consumer sets
|
|
119
|
+
* `defineBookConfig({ seo: { sitemap: { filter } } })`, the consumer's
|
|
120
|
+
* filter REPLACES this profile default (not composed). Consumers wanting
|
|
121
|
+
* to also exclude /print/ on top of additional exclusions copy the
|
|
122
|
+
* profile-default predicate's behavior into their own filter.
|
|
123
|
+
*/
|
|
124
|
+
sitemapFilter?: (page: string) => boolean;
|
|
108
125
|
}
|
|
109
126
|
|
|
110
127
|
/**
|
package/src/profiles/academic.ts
CHANGED
|
@@ -30,4 +30,7 @@ export const academicProfile = defineProfile({
|
|
|
30
30
|
styles: ['tokens.css', 'layout.css', 'callouts.css', 'chapter.css', 'typography.css', 'print.css'],
|
|
31
31
|
katex: true,
|
|
32
32
|
chaptersRenderer: academicChaptersRenderer, // v3.7.0 (#35) — owns /chapters semantics if consumer opts in via routes.chapters
|
|
33
|
+
// v4.6.0 (#76 Secondary): exclude /print/ from sitemap — print-friendly
|
|
34
|
+
// view, crawl-redundant. Academic-profile default.
|
|
35
|
+
sitemapFilter: (page: string) => !page.includes('/print/'),
|
|
33
36
|
});
|
|
@@ -32,4 +32,7 @@ export const courseNotesProfile = defineProfile({
|
|
|
32
32
|
styles: ['tokens.css', 'layout.css', 'callouts.css', 'chapter.css', 'typography.css', 'print.css'],
|
|
33
33
|
// v3.7.0 (#35): course-notes schema has tools-style fields (chapter, volatility, sources) — fallback renderer dispatches via tools renderer
|
|
34
34
|
chaptersRenderer: fallbackChaptersRenderer,
|
|
35
|
+
// v4.6.0 (#76 Secondary): exclude /print/ from sitemap — print-friendly
|
|
36
|
+
// view, crawl-redundant. Course-notes-profile default.
|
|
37
|
+
sitemapFilter: (page: string) => !page.includes('/print/'),
|
|
35
38
|
});
|
package/src/schemas.ts
CHANGED
|
@@ -83,6 +83,13 @@ export const academicChapterSchema = z.object({
|
|
|
83
83
|
notebook_path: z.string().optional(),
|
|
84
84
|
description: z.string().optional(),
|
|
85
85
|
draft: z.boolean().default(false),
|
|
86
|
+
// v4.6.0: optional SEO / article:* fields consumed by Chapter.astro.
|
|
87
|
+
// All optional; existing chapters without these continue to work.
|
|
88
|
+
author: z.string().optional(),
|
|
89
|
+
published: z.date().optional(),
|
|
90
|
+
updated: z.date().optional(),
|
|
91
|
+
tags: z.array(z.string()).default([]),
|
|
92
|
+
image: z.string().optional(),
|
|
86
93
|
});
|
|
87
94
|
|
|
88
95
|
export const toolsChapterSchema = z.object({
|
|
@@ -96,6 +103,12 @@ export const toolsChapterSchema = z.object({
|
|
|
96
103
|
description: z.string().optional(),
|
|
97
104
|
draft: z.boolean().default(false),
|
|
98
105
|
updated: z.date().optional(),
|
|
106
|
+
// v4.6.0: optional SEO / article:* fields consumed by Chapter.astro.
|
|
107
|
+
// `updated` already existed; the rest are new.
|
|
108
|
+
author: z.string().optional(),
|
|
109
|
+
published: z.date().optional(),
|
|
110
|
+
tags: z.array(z.string()).default([]),
|
|
111
|
+
image: z.string().optional(),
|
|
99
112
|
});
|
|
100
113
|
|
|
101
114
|
/** Minimal profile currently aliases the tools schema. */
|
|
@@ -147,6 +160,14 @@ export const courseNotesChapterSchema = z.object({
|
|
|
147
160
|
volatility: z.enum(volatilityLevels).default('architectural-pattern'),
|
|
148
161
|
sources: z.array(z.string()).default([]),
|
|
149
162
|
draft: z.boolean().default(false),
|
|
163
|
+
|
|
164
|
+
// v4.6.0: optional SEO / article:* fields consumed by Chapter.astro.
|
|
165
|
+
// `tags` already existed; the rest are new. `instructor` (line 130) is
|
|
166
|
+
// attribution metadata — distinct from `author` (the note-writer/curator).
|
|
167
|
+
author: z.string().optional(),
|
|
168
|
+
published: z.date().optional(),
|
|
169
|
+
updated: z.date().optional(),
|
|
170
|
+
image: z.string().optional(),
|
|
150
171
|
});
|
|
151
172
|
|
|
152
173
|
/**
|
|
@@ -228,6 +249,12 @@ export const researchPortfolioChapterSchema = z.object({
|
|
|
228
249
|
last_verified: z.date(),
|
|
229
250
|
updated: z.date().optional(),
|
|
230
251
|
draft: z.boolean().default(false),
|
|
252
|
+
|
|
253
|
+
// v4.6.0: optional SEO / article:* fields consumed by Chapter.astro.
|
|
254
|
+
// `tags` + `updated` already existed; `author` + `published` + `image` are new.
|
|
255
|
+
author: z.string().optional(),
|
|
256
|
+
published: z.date().optional(),
|
|
257
|
+
image: z.string().optional(),
|
|
231
258
|
});
|
|
232
259
|
|
|
233
260
|
// ===== Inferred chapter types — one per schema =====
|