@brandon_m_behring/book-scaffold-astro 4.26.3 → 4.27.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.
Files changed (49) hide show
  1. package/AGENTS.md +6 -0
  2. package/CLAUDE.md +35 -10
  3. package/LATEX_TO_MDX_MAPPING.md +1 -1
  4. package/LICENSE +27 -0
  5. package/LICENSE-CONTENT +19 -0
  6. package/README.md +33 -0
  7. package/components/AssessmentTest.astro +2 -1
  8. package/components/ChapterNav.astro +2 -2
  9. package/components/Cite.astro +2 -1
  10. package/components/NavContent.astro +2 -2
  11. package/components/PartReview.astro +2 -1
  12. package/components/Rationale.astro +3 -2
  13. package/components/Sidebar.astro +2 -1
  14. package/components/Term.astro +2 -1
  15. package/components/TipsCard.astro +2 -1
  16. package/components/VersionSelector.tsx +39 -36
  17. package/components/WeekRef.astro +2 -1
  18. package/components/XRef.astro +2 -1
  19. package/dist/components/VersionSelector.d.ts +16 -2
  20. package/dist/components/VersionSelector.mjs +18 -17
  21. package/dist/index.d.ts +24 -6
  22. package/dist/index.mjs +105 -5
  23. package/dist/schemas.d.ts +1 -1
  24. package/dist/schemas.mjs +8 -1
  25. package/dist/{types-CWXP1S4b.d.ts → types-CZrkqzpC.d.ts} +58 -26
  26. package/layouts/Base.astro +30 -19
  27. package/package.json +7 -4
  28. package/pages/answers.astro +2 -1
  29. package/pages/chapters.astro +2 -1
  30. package/pages/exercises.astro +2 -1
  31. package/pages/index.astro +4 -3
  32. package/pages/search.astro +2 -1
  33. package/pages/tips.astro +2 -1
  34. package/recipes/01-add-math.md +40 -28
  35. package/recipes/04-component-library.md +14 -4
  36. package/recipes/05-deploy-cloudflare.md +44 -0
  37. package/recipes/06-mobile-first-layout.md +25 -2
  38. package/recipes/09-validation.md +18 -9
  39. package/recipes/15-defining-styles.md +5 -2
  40. package/recipes/19-prevalidate-hook.md +29 -83
  41. package/scripts/build-bib.mjs +7 -3
  42. package/scripts/build-labels.mjs +33 -16
  43. package/scripts/read-env.mjs +25 -0
  44. package/scripts/resolve-book-config.mjs +96 -0
  45. package/scripts/validate.mjs +125 -84
  46. package/src/lib/define-style.ts +15 -6
  47. package/src/lib/nav-href.ts +24 -2
  48. package/styles/layout.css +14 -8
  49. package/styles/tokens.css +8 -10
package/dist/index.mjs CHANGED
@@ -912,6 +912,7 @@ var BOOK_PROFILES = Object.keys(PROFILES);
912
912
  // src/types.ts
913
913
  import { existsSync, readFileSync } from "fs";
914
914
  var BOOK_PRESETS = BOOK_PROFILES;
915
+ var NUMBER_STYLES = ["shared", "per-kind"];
915
916
  var BookConfigError = class extends Error {
916
917
  constructor(message) {
917
918
  super(message);
@@ -936,6 +937,7 @@ function readEnvFile(path = ".env") {
936
937
  return {};
937
938
  }
938
939
  }
940
+ var PRESET_FALLBACK_WARNING = /* @__PURE__ */ Symbol.for("book-scaffold-astro:preset-fallback-warning");
939
941
  function resolvePreset(explicitPreset, explicitProfile) {
940
942
  let candidate = explicitPreset ?? explicitProfile ?? process.env.BOOK_PRESET ?? process.env.BOOK_PROFILE;
941
943
  let source = "default";
@@ -956,7 +958,13 @@ function resolvePreset(explicitPreset, explicitProfile) {
956
958
  );
957
959
  }
958
960
  if (source === "default") {
959
- console.warn("book-scaffold-astro: BOOK_PRESET not set; falling back to 'minimal'.");
961
+ const warningState = globalThis;
962
+ if (!warningState[PRESET_FALLBACK_WARNING]) {
963
+ warningState[PRESET_FALLBACK_WARNING] = true;
964
+ console.warn(
965
+ "book-scaffold-astro: no preset resolved; falling back to 'minimal' for v4 compatibility. This fallback will be removed in v5. Add a built-in style to defineBookConfig and pass the same preset to defineBookSchemas, or set BOOK_PRESET."
966
+ );
967
+ }
960
968
  }
961
969
  return candidate;
962
970
  }
@@ -966,7 +974,7 @@ function resolveProfile(explicit) {
966
974
 
967
975
  // src/integration.ts
968
976
  import { fileURLToPath } from "url";
969
- import { readFileSync as readFileSync2 } from "fs";
977
+ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
970
978
  import { join } from "path";
971
979
 
972
980
  // src/lib/define-style.ts
@@ -981,6 +989,7 @@ function composeStyles(styles) {
981
989
  for (const style of styles) {
982
990
  if (style.name !== void 0) merged.name = style.name;
983
991
  if (style.preset !== void 0) merged.preset = style.preset;
992
+ if (style.numberStyle !== void 0) merged.numberStyle = style.numberStyle;
984
993
  if (style.site !== void 0) merged.site = style.site;
985
994
  if (style.deploy !== void 0) merged.deploy = style.deploy;
986
995
  if (style.releaseStatus !== void 0) {
@@ -1121,6 +1130,21 @@ function defineMdxComponents(components) {
1121
1130
  // src/integration.ts
1122
1131
  var BOOK_CONFIG_VIRTUAL_ID = "virtual:book-scaffold/book-config";
1123
1132
  var BOOK_CONFIG_RESOLVED_ID = "\0" + BOOK_CONFIG_VIRTUAL_ID;
1133
+ function makeRobotoFontDisplayVitePlugin() {
1134
+ return {
1135
+ name: "book-scaffold:roboto-font-display",
1136
+ enforce: "pre",
1137
+ transform(code, id) {
1138
+ const [path = id] = id.split("?");
1139
+ const normalizedPath = path.replaceAll("\\", "/");
1140
+ if (!normalizedPath.endsWith("/@fontsource-variable/roboto/index.css")) {
1141
+ return null;
1142
+ }
1143
+ const transformed = code.replace(/font-display:\s*swap/g, "font-display: optional");
1144
+ return transformed === code ? null : { code: transformed, map: null };
1145
+ }
1146
+ };
1147
+ }
1124
1148
  function makeBookConfigVitePlugin(config) {
1125
1149
  const serialized = `export default ${JSON.stringify(config)};`;
1126
1150
  return {
@@ -1197,6 +1221,17 @@ var ROUTE_REGISTRY = {
1197
1221
  // empty string → '/[slug]'; arbitrary string → '/<prefix>/[slug]').
1198
1222
  frontmatter: { pattern: "/frontmatter/[slug]", file: "frontmatter/[...slug].astro" }
1199
1223
  };
1224
+ var DEFAULT_CONTENT_SECURITY_POLICY = "default-src 'self'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://cloudflareinsights.com; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'";
1225
+ function renderSecurityHeaders(contentSecurityPolicy) {
1226
+ const csp = contentSecurityPolicy ?? DEFAULT_CONTENT_SECURITY_POLICY;
1227
+ return `/*
1228
+ Strict-Transport-Security: max-age=31536000; includeSubDomains
1229
+ X-Content-Type-Options: nosniff
1230
+ Referrer-Policy: strict-origin-when-cross-origin
1231
+ Permissions-Policy: camera=(), microphone=(), geolocation=()
1232
+ Content-Security-Policy: ${csp}
1233
+ `;
1234
+ }
1200
1235
  function frontmatterPatternFromPrefix(prefix) {
1201
1236
  if (prefix === void 0) return ROUTE_REGISTRY.frontmatter.pattern;
1202
1237
  if (prefix === "") return "/[slug]";
@@ -1208,6 +1243,7 @@ function resolvePage(file) {
1208
1243
  function bookScaffoldIntegration(opts) {
1209
1244
  const {
1210
1245
  profile,
1246
+ numberStyle = "shared",
1211
1247
  routes: userOverrides = {},
1212
1248
  extraStyles = [],
1213
1249
  mdxComponentsModule,
@@ -1215,6 +1251,7 @@ function bookScaffoldIntegration(opts) {
1215
1251
  title,
1216
1252
  subtitle,
1217
1253
  releaseStatus,
1254
+ securityHeaders,
1218
1255
  description,
1219
1256
  portfolio,
1220
1257
  // v4.6.0: book-level author + SEO config, propagated through the
@@ -1245,7 +1282,7 @@ function bookScaffoldIntegration(opts) {
1245
1282
  ),
1246
1283
  frontmatter: fmEnabled
1247
1284
  };
1248
- return {
1285
+ const integration = {
1249
1286
  name: "book-scaffold-astro",
1250
1287
  hooks: {
1251
1288
  "astro:config:setup": ({ injectScript, injectRoute, updateConfig, config }) => {
@@ -1280,6 +1317,7 @@ function bookScaffoldIntegration(opts) {
1280
1317
  updateConfig({
1281
1318
  vite: {
1282
1319
  plugins: [
1320
+ makeRobotoFontDisplayVitePlugin(),
1283
1321
  makeMdxComponentsVitePlugin(resolvedMdxPath),
1284
1322
  makeBookConfigVitePlugin({
1285
1323
  title: title ?? null,
@@ -1315,9 +1353,38 @@ function bookScaffoldIntegration(opts) {
1315
1353
  }
1316
1354
  }
1317
1355
  });
1356
+ },
1357
+ // v4.27.0 (#188): emit defaults for every build, not only newly
1358
+ // scaffolded projects, so existing consumers become protected on their
1359
+ // next package upgrade. Astro copies public/_headers into the output
1360
+ // before build:done; an existing target therefore means the consumer
1361
+ // owns the complete file and must win byte-for-byte.
1362
+ "astro:build:done": ({ dir, logger }) => {
1363
+ if (securityHeaders === false) {
1364
+ logger.info("security-header emission disabled by defineBookConfig (#188)");
1365
+ return;
1366
+ }
1367
+ const target = join(fileURLToPath(dir), "_headers");
1368
+ if (existsSync3(target)) {
1369
+ logger.info("consumer public/_headers present; scaffold defaults skipped (#188)");
1370
+ return;
1371
+ }
1372
+ writeFileSync(
1373
+ target,
1374
+ renderSecurityHeaders(securityHeaders?.contentSecurityPolicy),
1375
+ "utf8"
1376
+ );
1377
+ logger.info("emitted default security headers; public/_headers overrides them (#188)");
1318
1378
  }
1319
1379
  }
1320
1380
  };
1381
+ Object.defineProperty(integration, "__bookScaffoldResolvedConfig", {
1382
+ value: Object.freeze({ preset: profile, numberStyle }),
1383
+ enumerable: false,
1384
+ configurable: false,
1385
+ writable: false
1386
+ });
1387
+ return integration;
1321
1388
  }
1322
1389
 
1323
1390
  // src/config.ts
@@ -1354,8 +1421,19 @@ async function defineBookConfig(opts) {
1354
1421
  if ("preset" in opts || "profile" in opts) {
1355
1422
  throw v3MigrationError(opts);
1356
1423
  }
1424
+ if (Object.prototype.hasOwnProperty.call(opts, "deploy")) {
1425
+ console.warn(
1426
+ "book-scaffold-astro: defineBookConfig({ deploy }) is inert and deprecated; create-book chooses wrangler.toml from its CLI preset. Remove this field before upgrading to v5 (#180)."
1427
+ );
1428
+ }
1357
1429
  const composed = composeStyles(opts.styles ?? []);
1358
- const profile = opts.styles === void 0 && composed.preset === void 0 ? "minimal" : composed.preset ?? "minimal";
1430
+ const profile = resolvePreset(composed.preset);
1431
+ const numberStyle = opts.numberStyle ?? composed.numberStyle ?? "shared";
1432
+ if (!NUMBER_STYLES.includes(numberStyle)) {
1433
+ throw new BookConfigError(
1434
+ `numberStyle must be one of ${NUMBER_STYLES.join(" | ")} (got ${JSON.stringify(numberStyle)})`
1435
+ );
1436
+ }
1359
1437
  const site = opts.site ?? composed.site;
1360
1438
  if (!site) {
1361
1439
  throw new BookConfigError(
@@ -1423,6 +1501,7 @@ async function defineBookConfig(opts) {
1423
1501
  sitemap(sitemapOptions),
1424
1502
  bookScaffoldIntegration({
1425
1503
  profile,
1504
+ numberStyle,
1426
1505
  routes: mergedRoutes,
1427
1506
  mdxComponentsModule,
1428
1507
  extraStyles: mergedExtraStyles,
@@ -1433,6 +1512,11 @@ async function defineBookConfig(opts) {
1433
1512
  subtitle: opts.subtitle,
1434
1513
  // v4.26.2 (#149; style inheritance + opt-out fixed in v4.26.3).
1435
1514
  releaseStatus: resolvedReleaseStatus === false ? void 0 : resolvedReleaseStatus,
1515
+ // v4.27.0 (#188): undefined enables the audited defaults, false opts
1516
+ // out, and an object can replace only the CSP. The integration owns
1517
+ // the build:done emission because that is where Astro's output dir is
1518
+ // known and a copied consumer public/_headers can be detected.
1519
+ securityHeaders: opts.securityHeaders,
1436
1520
  description: opts.description,
1437
1521
  portfolio: resolvedPortfolio,
1438
1522
  // v4.6.0: book-level author + SEO config (ogImage, twitterHandle),
@@ -1479,6 +1563,7 @@ async function defineBookConfig(opts) {
1479
1563
  };
1480
1564
  const {
1481
1565
  styles: _styles,
1566
+ numberStyle: _numberStyle,
1482
1567
  site: _site,
1483
1568
  routes: _routes,
1484
1569
  deploy: _deploy,
@@ -1492,6 +1577,8 @@ async function defineBookConfig(opts) {
1492
1577
  subtitle: _subtitle,
1493
1578
  // v4.26.2 (#149): strip the release-state banner config.
1494
1579
  releaseStatus: _releaseStatus,
1580
+ // v4.27.0 (#188): consumed by the scaffold integration at build:done.
1581
+ securityHeaders: _securityHeaders,
1495
1582
  description: _description,
1496
1583
  portfolio: _portfolio,
1497
1584
  // v4.6.0: strip new book-level SEO opts (author + seo block).
@@ -1512,6 +1599,7 @@ async function defineBookConfig(opts) {
1512
1599
  ...rest
1513
1600
  } = opts;
1514
1601
  void _styles;
1602
+ void _numberStyle;
1515
1603
  void _site;
1516
1604
  void _routes;
1517
1605
  void _deploy;
@@ -1523,6 +1611,7 @@ async function defineBookConfig(opts) {
1523
1611
  void _title;
1524
1612
  void _subtitle;
1525
1613
  void _releaseStatus;
1614
+ void _securityHeaders;
1526
1615
  void _description;
1527
1616
  void _portfolio;
1528
1617
  void _author;
@@ -1628,6 +1717,9 @@ function assertEnumProp(value, allowed, ctx) {
1628
1717
  );
1629
1718
  }
1630
1719
 
1720
+ // src/index.ts
1721
+ init_katex_macros();
1722
+
1631
1723
  // src/lib/book-link.ts
1632
1724
  function resolveBookHref(siblingBooks, book, to) {
1633
1725
  const base = siblingBooks?.[book];
@@ -1641,9 +1733,13 @@ function resolveBookHref(siblingBooks, book, to) {
1641
1733
  }
1642
1734
 
1643
1735
  // src/lib/nav-href.ts
1644
- function normBase(baseUrl) {
1736
+ function normalizeBase(baseUrl) {
1645
1737
  return (baseUrl || "/").replace(/\/*$/, "/");
1646
1738
  }
1739
+ function baseNoSlash(baseUrl) {
1740
+ return (baseUrl || "/").replace(/\/+$/, "");
1741
+ }
1742
+ var normBase = normalizeBase;
1647
1743
  function fillTokens(pattern, tokens) {
1648
1744
  return pattern.replace(/:(book|slug|route|id)\b/g, (_m, k) => tokens[k] ?? "");
1649
1745
  }
@@ -1916,6 +2012,7 @@ export {
1916
2012
  BookConfigError,
1917
2013
  DEFAULT_GITHUB_BRANCH,
1918
2014
  KIND_LABEL,
2015
+ NUMBER_STYLES,
1919
2016
  THEOREM_KINDS,
1920
2017
  UNKNOWN_PART_ORDINAL,
1921
2018
  academicChapterSchema,
@@ -1928,6 +2025,7 @@ export {
1928
2025
  apparatusHref,
1929
2026
  assertEnumProp,
1930
2027
  assertKnownDomain,
2028
+ baseNoSlash,
1931
2029
  bloomLevels,
1932
2030
  bookOf,
1933
2031
  bookScaffoldIntegration,
@@ -1962,6 +2060,7 @@ export {
1962
2060
  layoutModes,
1963
2061
  minimalChapterSchema,
1964
2062
  minimalStyle,
2063
+ normalizeBase,
1965
2064
  normalizeFrontmatterConfig,
1966
2065
  originUrlFromGitConfig,
1967
2066
  parseRepoSlug,
@@ -1993,6 +2092,7 @@ export {
1993
2092
  sourceTiersResearch,
1994
2093
  sourcesSchema,
1995
2094
  spreadBlueprint,
2095
+ ssmMacros,
1996
2096
  theoremLabel,
1997
2097
  tocHeadings,
1998
2098
  toolSlugs,
package/dist/schemas.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { defineCollection } from 'astro:content';
2
- import { g as BookSchemasOptions } from './types-CWXP1S4b.js';
2
+ import { g as BookSchemasOptions } from './types-CZrkqzpC.js';
3
3
  import 'astro';
4
4
  import './schemas-CKipJ5Ie.js';
5
5
  import 'astro/zod';
package/dist/schemas.mjs CHANGED
@@ -814,6 +814,7 @@ function readEnvFile(path = ".env") {
814
814
  return {};
815
815
  }
816
816
  }
817
+ var PRESET_FALLBACK_WARNING = /* @__PURE__ */ Symbol.for("book-scaffold-astro:preset-fallback-warning");
817
818
  function resolvePreset(explicitPreset, explicitProfile) {
818
819
  let candidate = explicitPreset ?? explicitProfile ?? process.env.BOOK_PRESET ?? process.env.BOOK_PROFILE;
819
820
  let source = "default";
@@ -834,7 +835,13 @@ function resolvePreset(explicitPreset, explicitProfile) {
834
835
  );
835
836
  }
836
837
  if (source === "default") {
837
- console.warn("book-scaffold-astro: BOOK_PRESET not set; falling back to 'minimal'.");
838
+ const warningState = globalThis;
839
+ if (!warningState[PRESET_FALLBACK_WARNING]) {
840
+ warningState[PRESET_FALLBACK_WARNING] = true;
841
+ console.warn(
842
+ "book-scaffold-astro: no preset resolved; falling back to 'minimal' for v4 compatibility. This fallback will be removed in v5. Add a built-in style to defineBookConfig and pass the same preset to defineBookSchemas, or set BOOK_PRESET."
843
+ );
844
+ }
838
845
  }
839
846
  return candidate;
840
847
  }
@@ -353,6 +353,8 @@ interface Style {
353
353
  readonly name?: string;
354
354
  /** Profile that backs this style — determines schema + default routes + styles + KaTeX wiring. */
355
355
  readonly preset?: BookPreset;
356
+ /** Theorem-family numbering strategy; shallow override (last wins). */
357
+ readonly numberStyle?: NumberStyle;
356
358
  /** Book's deployed origin (sitemap, canonical, Pagefind). Required at composition end;
357
359
  * optional inside a Style (so styles can omit it and consumers can provide per-book). */
358
360
  readonly site?: string;
@@ -374,10 +376,10 @@ interface Style {
374
376
  * `remarkPlugins` and `rehypePlugins` arrays concat across the chain;
375
377
  * scalar fields override. */
376
378
  readonly markdown?: AstroUserConfig['markdown'];
377
- /** Deploy target drives create-book's wrangler.toml shape.
378
- * - `'workers'`: Cloudflare Workers + Static Assets (default for academic/tools/minimal)
379
- * - `'pages'`: Cloudflare Pages (default for research-portfolio/course-notes)
380
- * Closes #50. */
379
+ /** Reserved legacy metadata. It is composed but has no runtime or
380
+ * create-book effect: create-book chooses wrangler.toml from its CLI
381
+ * preset before a consumer Style exists (#180).
382
+ * @deprecated Inert in v4; scheduled for removal in v5. */
381
383
  readonly deploy?: 'pages' | 'workers';
382
384
  /**
383
385
  * v4.26.2 (#149; style inheritance fixed in v4.26.3): release-state
@@ -445,7 +447,7 @@ declare function defineStyle(opts: StyleInput): Style;
445
447
  * - Top-level `defineBookConfig` fields beat any style (handled in config.ts)
446
448
  *
447
449
  * Per-key merge strategy:
448
- * - `preset`, `site`, `deploy`, `mdxComponentsModule`, `name`, `releaseStatus`
450
+ * - `preset`, `numberStyle`, `site`, `deploy`, `mdxComponentsModule`, `name`, `releaseStatus`
449
451
  * → shallow override (last defined wins; `releaseStatus: false` suppresses)
450
452
  * - `routes` → per-route spread (each route key independently overridable)
451
453
  * - `katexMacros` → per-macro spread (each macro key independently overridable)
@@ -482,6 +484,30 @@ declare function normalizeFrontmatterConfig(v: FrontmatterRouteConfig | undefine
482
484
 
483
485
  type BookPreset = BookProfile;
484
486
  declare const BOOK_PRESETS: readonly ("academic" | "tools" | "minimal" | "course-notes" | "research-portfolio")[];
487
+ /** Theorem-family numbering strategy used by build-labels and validate. */
488
+ type NumberStyle = 'shared' | 'per-kind';
489
+ declare const NUMBER_STYLES: readonly ["shared", "per-kind"];
490
+ /**
491
+ * v4.27.0 (#188): build-time security-header customization.
492
+ *
493
+ * The integration emits a Cloudflare-compatible `dist/_headers` file by
494
+ * default. Consumers that need a different CSP can replace that one header
495
+ * while retaining the scaffold's other audited defaults.
496
+ */
497
+ interface SecurityHeadersConfig {
498
+ contentSecurityPolicy?: string;
499
+ }
500
+ /**
501
+ * v4.26.2 (#149): book-level release state rendered by
502
+ * `<PreReleaseBanner>` across every page.
503
+ *
504
+ * Style inheritance and explicit suppression were fixed in v4.26.3.
505
+ */
506
+ interface ReleaseStatusConfig {
507
+ state: 'alpha' | 'beta' | 'rc' | 'locked';
508
+ dismissAt?: string;
509
+ message?: string;
510
+ }
485
511
  /**
486
512
  * v4.26.2 (#149): book-level release state rendered by
487
513
  * `<PreReleaseBanner>` across every page.
@@ -531,6 +557,13 @@ interface BookConfigOptions {
531
557
  * });
532
558
  */
533
559
  styles?: readonly Style[];
560
+ /**
561
+ * v4.27.0 (#175): theorem-family counter strategy. `shared` preserves the
562
+ * amsthm-style sequence used through v4.26; `per-kind` gives theorem,
563
+ * proposition, lemma, and each other theorem kind an independent sequence.
564
+ * May also be supplied by a Style; top-level config wins.
565
+ */
566
+ numberStyle?: NumberStyle;
534
567
  /**
535
568
  * Optional per-route override of the composed profile's defaults. Use to
536
569
  * disable an auto-injected route (e.g. multi-book consumer that ships
@@ -545,10 +578,13 @@ interface BookConfigOptions {
545
578
  */
546
579
  routes?: PartialRouteToggles;
547
580
  /**
548
- * v4.0.0 (NEW): deploy target drives `create-book`'s wrangler.toml shape.
549
- * Defaults from the composed style chain (academic/tools/minimal
550
- * 'workers'; course-notes/research-portfolio 'pages'); per-book override
551
- * here beats any style. Closes #50.
581
+ * v4.0.0 (#50): RESERVEDaccepted and style-chain-merged, but currently
582
+ * has NO runtime effect (#180). The wrangler.toml shape is decided once, at
583
+ * scaffold time, by `create-book` from the profile name (academic/tools/
584
+ * minimal Workers; course-notes/research-portfolio Pages); setting this
585
+ * field changes nothing afterward. Wire-or-remove is a v5 decision.
586
+ * @deprecated Inert in v4; remove this option before v5. Deployment shape
587
+ * is chosen by create-book or by the consumer's own deployment files.
552
588
  */
553
589
  deploy?: 'pages' | 'workers';
554
590
  /**
@@ -625,6 +661,14 @@ interface BookConfigOptions {
625
661
  * composed style chain; set `false` to suppress an inherited banner.
626
662
  */
627
663
  releaseStatus?: ReleaseStatusConfig | false;
664
+ /**
665
+ * v4.27.0 (#188): security headers emitted to `dist/_headers` after an
666
+ * Astro build. Omit for the scaffold defaults; set `false` to emit no
667
+ * scaffold-owned file; provide `contentSecurityPolicy` to replace only the
668
+ * default CSP while retaining HSTS, XCTO, Referrer-Policy, and
669
+ * Permissions-Policy. A consumer-owned `public/_headers` always wins.
670
+ */
671
+ securityHeaders?: SecurityHeadersConfig | false;
628
672
  /**
629
673
  * v4.5.0: Book description. Read by the auto-injected `/` landing page (lead paragraph + <meta description>).
630
674
  * Optional; landing renders no description paragraph if unset.
@@ -757,6 +801,8 @@ interface BookSchemasOptions {
757
801
  /** Options for the internal `bookScaffoldIntegration`. See PACKAGE_DESIGN.md §6. */
758
802
  interface BookScaffoldIntegrationOptions {
759
803
  profile: BookProfile;
804
+ /** Resolved theorem-family counter strategy exposed to package CLI tooling. */
805
+ numberStyle?: NumberStyle;
760
806
  /**
761
807
  * Per-route override; merged into the profile's defaults.
762
808
  * v4.0.0: `routes.frontmatter` widened to `boolean | { enabled, prefix? }`
@@ -780,6 +826,8 @@ interface BookScaffoldIntegrationOptions {
780
826
  /** v4.26.2 (#149; style inheritance fixed in v4.26.3): resolved
781
827
  * release-state banner propagated via the book-config virtual module. */
782
828
  releaseStatus?: ReleaseStatusConfig;
829
+ /** v4.27.0 (#188): resolved security-header emission policy. */
830
+ securityHeaders?: SecurityHeadersConfig | false;
783
831
  /** v4.5.0: book description, propagated to `/` landing via vite.define. */
784
832
  description?: string;
785
833
  /**
@@ -831,22 +879,6 @@ interface BookScaffoldIntegrationOptions {
831
879
  declare class BookConfigError extends Error {
832
880
  constructor(message: string);
833
881
  }
834
- /**
835
- * Resolve preset from explicit args → env → .env → default. Throws on invalid.
836
- *
837
- * v3.4.0 (closes #9): canonical resolver. Accepts both `preset` and `profile`
838
- * (back-compat) explicit args; reads both `BOOK_PRESET` (preferred) and
839
- * `BOOK_PROFILE` (alias) env vars; same for .env file lookups.
840
- *
841
- * Resolution order:
842
- * 1. explicitPreset (from defineBookConfig({ preset: ... }))
843
- * 2. explicitProfile (from defineBookConfig({ profile: ... }))
844
- * 3. process.env.BOOK_PRESET
845
- * 4. process.env.BOOK_PROFILE
846
- * 5. .env BOOK_PRESET
847
- * 6. .env BOOK_PROFILE
848
- * 7. 'minimal' (with console.warn)
849
- */
850
882
  declare function resolvePreset(explicitPreset?: BookPreset, explicitProfile?: BookProfile): BookPreset;
851
883
  /**
852
884
  * Backward-compat alias. New code should use `resolvePreset()`.
@@ -855,4 +887,4 @@ declare function resolvePreset(explicitPreset?: BookPreset, explicitProfile?: Bo
855
887
  */
856
888
  declare function resolveProfile(explicit?: BookProfile): BookProfile;
857
889
 
858
- export { BOOK_PRESETS as B, type ChapterFor as C, type FreshnessAffordance as F, type PartKey as P, type ReleaseStatusConfig as R, type StatusBadge as S, type VolatilityBadge as V, BOOK_PROFILES as a, BookConfigError as b, type BookConfigOptions as c, type BookPreset as d, type BookProfile as e, type BookScaffoldIntegrationOptions as f, type BookSchemasOptions as g, type ChaptersRenderer as h, type FrontmatterRouteConfig as i, type PartialRouteToggles as j, type ProfileDefinition as k, type RouteToggles as l, type Style as m, type StyleInput as n, composeStyles as o, defineProfile as p, defineStyle as q, normalizeFrontmatterConfig as r, resolvePreset as s, resolveProfile as t };
890
+ export { BOOK_PRESETS as B, type ChapterFor as C, type FreshnessAffordance as F, NUMBER_STYLES as N, type PartKey as P, type ReleaseStatusConfig as R, type SecurityHeadersConfig as S, type VolatilityBadge as V, BOOK_PROFILES as a, BookConfigError as b, type BookConfigOptions as c, type BookPreset as d, type BookProfile as e, type BookScaffoldIntegrationOptions as f, type BookSchemasOptions as g, type ChaptersRenderer as h, type FrontmatterRouteConfig as i, type NumberStyle as j, type PartialRouteToggles as k, type ProfileDefinition as l, type RouteToggles as m, type StatusBadge as n, type Style as o, type StyleInput as p, composeStyles as q, defineProfile as r, defineStyle as s, normalizeFrontmatterConfig as t, resolvePreset as u, resolveProfile as v };
@@ -15,15 +15,15 @@
15
15
  * lang — html lang attribute (default: en)
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
- * Below 1024px the sidebar is auto-hidden via CSS regardless.
19
- * showChrome — render the tools chrome (ToolFilter + VersionSelector)
18
+ * Below 80rem (1280px) the sidebar is auto-hidden via CSS and
19
+ * the mobile nav drawer is the chapter nav (v4.26.0, #80).
20
+ * showChrome — render the automatic tools chrome (ToolFilter)
20
21
  * (default: true). Set false on a landing/hub page with no
21
- * chapters/tools/versions; search + theme toggle always render.
22
+ * chapter filtering; search + theme toggle always render.
22
23
  *
23
- * Tools chrome (ToolFilter, VersionSelector): only mounted when
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).
24
+ * Tools chrome (ToolFilter): only mounted when BOOK_PROFILE !== 'academic' AND
25
+ * showChrome !== false. VersionSelector is a prop-driven public component that
26
+ * consumers mount explicitly once they have real deployed-version data.
27
27
  *
28
28
  * Usage:
29
29
  * ---
@@ -39,6 +39,7 @@
39
39
  * — or the page ends up with duplicate `<main>` landmarks (a11y, #91).
40
40
  */
41
41
  import '@fontsource-variable/roboto';
42
+ import robotoLatinUrl from '@fontsource-variable/roboto/files/roboto-latin-wght-normal.woff2?url';
42
43
  import '@fontsource-variable/source-code-pro';
43
44
  // KaTeX CSS is injected by bookScaffoldIntegration for academic profile
44
45
  // only (since v3.0 alpha.8) — tools/minimal profiles don't install katex
@@ -51,12 +52,11 @@ import '../styles/chapter.css';
51
52
  import '../styles/tool-filter.css';
52
53
  import '../styles/convergence.css';
53
54
  import '../styles/print.css';
54
- // Use package-path imports for the .tsx islands so Vite resolves them
55
+ // Use the package-path import for the .tsx island so Vite resolves it
55
56
  // via the exports map (→ pre-compiled dist/components/*.mjs with proper
56
57
  // preact JSX). The raw .tsx files are also shipped so consumers can
57
58
  // import them directly, but relative imports from inside the package
58
59
  // must route through the exports map, not the raw source.
59
- import VersionSelector from '@brandon_m_behring/book-scaffold-astro/components/VersionSelector';
60
60
  import ToolFilter from '@brandon_m_behring/book-scaffold-astro/components/ToolFilter';
61
61
  import Sidebar from '../components/Sidebar.astro';
62
62
  // v4.26.2 (#149; style inheritance fixed in v4.26.3): site-wide
@@ -68,6 +68,7 @@ import NavContent from '../components/NavContent.astro';
68
68
  // description fallback, ogImage default, twitterHandle) from the
69
69
  // book-config virtual module (was landing-config in v4.5.1).
70
70
  import bookConfig from 'virtual:book-scaffold/book-config';
71
+ import { normalizeBase } from '../src/lib/nav-href';
71
72
 
72
73
  const profile = import.meta.env.BOOK_PROFILE ?? 'minimal';
73
74
 
@@ -76,10 +77,10 @@ interface Props {
76
77
  description?: string;
77
78
  lang?: string;
78
79
  showSidebar?: boolean;
79
- /** #163: render the optional "tools chrome" — the ToolFilter +
80
- * VersionSelector islands (default true). Set false on landing/hub pages
81
- * with no chapters/tools/versions; the search + theme-toggle cluster always
82
- * renders. (No-op for the academic profile, which never shows them.) */
80
+ /** #163: render the optional automatic tools chrome — the ToolFilter island
81
+ * (default true). Set false on landing/hub pages with no chapter filtering;
82
+ * the search + theme-toggle cluster always renders. (No-op for the academic
83
+ * profile, which never shows the ToolFilter.) */
83
84
  showChrome?: boolean;
84
85
  /** v4.6.0: Open Graph image URL (relative to site root, or absolute). */
85
86
  ogImage?: string;
@@ -97,9 +98,9 @@ const {
97
98
  ogType = 'website',
98
99
  } = Astro.props;
99
100
 
100
- // Tools chrome (ToolFilter + VersionSelector) renders only for non-academic
101
+ // Automatic tools chrome (ToolFilter) renders only for non-academic
101
102
  // profiles AND when the page opts in via showChrome (default true). A
102
- // landing/hub page (no chapters/tools/versions) passes showChrome={false} to
103
+ // landing/hub page (no chapter filtering) passes showChrome={false} to
103
104
  // get the search + theme-toggle cluster only — the chrome-free path that
104
105
  // previously forced borrowing the academic profile (and its katex deps). #163.
105
106
  const showToolsChrome = (profile !== 'academic') && showChrome;
@@ -120,7 +121,7 @@ const absoluteOgImage = resolvedOgImage
120
121
  const twitterHandle = bookConfig.seo?.twitterHandle ?? null;
121
122
  const ogSiteName = bookConfig.title ?? title;
122
123
  const ogDescription = description ?? bookConfig.description ?? '';
123
- const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/*$/, '/');
124
+ const baseUrl = normalizeBase(import.meta.env.BASE_URL);
124
125
  ---
125
126
 
126
127
  <!doctype html>
@@ -128,6 +129,15 @@ const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/*$/, '/');
128
129
  <head>
129
130
  <meta charset="utf-8" />
130
131
  <link rel="icon" type="image/svg+xml" href={`${baseUrl}favicon.svg`} />
132
+ {/* #187: start the common Latin body face early. Its Fontsource rules use
133
+ font-display: optional, so a slow response never swaps after paint. */}
134
+ <link
135
+ rel="preload"
136
+ href={robotoLatinUrl}
137
+ as="font"
138
+ type="font/woff2"
139
+ crossorigin="anonymous"
140
+ />
131
141
  <meta name="viewport" content="width=device-width, initial-scale=1" />
132
142
  <meta name="color-scheme" content="light dark" />
133
143
  <meta name="generator" content={Astro.generator} />
@@ -196,7 +206,6 @@ const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/*$/, '/');
196
206
  </a>
197
207
  )}
198
208
  {showToolsChrome && <ToolFilter client:idle />}
199
- {showToolsChrome && <VersionSelector client:idle />}
200
209
  <a
201
210
  href={`${baseUrl}search/`}
202
211
  class="chrome-button"
@@ -217,7 +226,8 @@ const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/*$/, '/');
217
226
  </button>
218
227
  </div>
219
228
  {/* v4.26.0 (#80): mobile/tablet nav drawer (sibling of <main>, NOT a <main>).
220
- CSS-hidden ≥64rem where the Sidebar is the nav. role=dialog + the inline
229
+ CSS-hidden ≥80rem where the Sidebar is the nav (layout.css and the
230
+ controller's desktop auto-close agree on 80rem). role=dialog + the inline
221
231
  controller below provide focus-trap/ESC; `:target` is the no-JS fallback. */}
222
232
  {showSidebar && (
223
233
  <div id="nav-drawer" class="nav-drawer" data-nav-drawer>
@@ -390,7 +400,8 @@ const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/*$/, '/');
390
400
  text-decoration: none;
391
401
  }
392
402
 
393
- /* Version selector dropdown */
403
+ /* Opt-in VersionSelector dropdown. Base provides its visual language but
404
+ * deliberately does not mount it without a consumer-owned version manifest. */
394
405
  .version-selector {
395
406
  position: relative;
396
407
  display: inline-block;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@brandon_m_behring/book-scaffold-astro",
3
- "description": "Astro 6 + MDX toolkit for long-form technical books. 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.26.3",
3
+ "description": "Astro 6 + MDX toolkit for long-form technical books with five typed presets, Tufte typography, citations, search, PDF, and Cloudflare deployment.",
4
+ "version": "4.27.1",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Brandon Behring",
@@ -161,9 +161,11 @@
161
161
  "pedagogy",
162
162
  "examples",
163
163
  "CLAUDE.md",
164
+ "AGENTS.md",
164
165
  "README.md",
165
- "LATEX_TO_MDX_MAPPING.md",
166
- "examples"
166
+ "LICENSE",
167
+ "LICENSE-CONTENT",
168
+ "LATEX_TO_MDX_MAPPING.md"
167
169
  ],
168
170
  "scripts": {
169
171
  "build": "tsup",
@@ -195,6 +197,7 @@
195
197
  "@fontsource-variable/roboto": "^5.2.10",
196
198
  "@fontsource-variable/source-code-pro": "^5.2.7",
197
199
  "pagefind": "^1.5.2",
200
+ "vite": "^7.3.2",
198
201
  "yaml": "^2.9.0"
199
202
  },
200
203
  "devDependencies": {
@@ -26,6 +26,7 @@ import { render } from 'astro:content';
26
26
  import bookConfig from 'virtual:book-scaffold/book-config';
27
27
  import { getAllQuestions, groupByChapter } from '../src/lib/questions';
28
28
  import { assertKnownDomain } from '../src/lib/exam-domains';
29
+ import { normalizeBase } from '../src/lib/nav-href';
29
30
 
30
31
  // Presence-gate: only touch the collection when the directory holds files.
31
32
  const questionModules = import.meta.glob('/src/content/questions/**/*.{md,mdx}', {
@@ -51,7 +52,7 @@ const rendered = await Promise.all(
51
52
  const byChapter = groupByChapter(rendered);
52
53
  const total = rendered.length;
53
54
  // #142: practice-exam + chapter links must prefix the deploy base.
54
- const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
55
+ const baseUrl = normalizeBase(import.meta.env.BASE_URL);
55
56
  const practiceHref = (bookConfig.enabledRoutes ?? []).includes('practiceExam')
56
57
  ? `${baseUrl}practice-exam`
57
58
  : null;
@@ -26,9 +26,10 @@ import { getAllChapters, type Chapter } from '../src/lib/chapters';
26
26
  import { PROFILES } from '../src/profiles/index';
27
27
  import { fallbackChaptersRenderer } from '../src/profiles/renderers/fallback-chapters';
28
28
  import type { ChaptersRenderer, PartKey } from '../src/lib/chapters-renderer';
29
+ import { normalizeBase } from '../src/lib/nav-href';
29
30
 
30
31
  // #142: prefix BASE_URL so chapter-card links stay inside a non-root deploy base.
31
- const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
32
+ const baseUrl = normalizeBase(import.meta.env.BASE_URL);
32
33
 
33
34
  const profileName = (import.meta.env.BOOK_PROFILE ?? 'minimal') as keyof typeof PROFILES;
34
35
  const profileDef = PROFILES[profileName];