@heroiclands/package-build 23.12.8 → 24.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +3 -2
  3. package/assets/preview/gitea.ini +12 -7
  4. package/assets/styles/content-base.css +293 -24
  5. package/bin/content-commands.mjs +370 -213
  6. package/bin/package-build-preview.mjs +2 -2
  7. package/bin/package-build.mjs +3 -2
  8. package/ci/check-banners.mjs +7 -7
  9. package/config.mjs +21 -6
  10. package/content-config.mjs +199 -80
  11. package/docs/api.md +110 -89
  12. package/docs/authoring/links-and-markup.md +469 -97
  13. package/docs/commands.md +259 -78
  14. package/docs/configuration.md +211 -103
  15. package/docs/content-format.md +4 -4
  16. package/docs/diagnostics.md +44 -2
  17. package/docs/getting-started.md +1 -1
  18. package/docs/guides/icons.md +1 -1
  19. package/docs/guides/module-project.md +5 -8
  20. package/docs/guides/release-and-deploy.md +2 -2
  21. package/docs/guides/site.md +112 -23
  22. package/docs/guides/system-project.md +1 -1
  23. package/docs/guides/writing-in-invented-scripts.md +1247 -0
  24. package/docs/index.md +1 -0
  25. package/docs/project-setup.md +28 -30
  26. package/docs/reference/format-details.md +677 -319
  27. package/docs/reference/note-types.md +47 -10
  28. package/docs/reference/theme-pages.md +178 -108
  29. package/docs/source-preview.md +16 -2
  30. package/engine/anchors.mjs +2 -2
  31. package/engine/asset-shipping.mjs +22 -10
  32. package/engine/base-compiler.mjs +62 -104
  33. package/engine/changelog-lint.mjs +1 -1
  34. package/engine/code-fences.mjs +3 -3
  35. package/engine/content-address.mjs +1 -1
  36. package/engine/content-alerts.mjs +8 -4
  37. package/engine/content-blocks.mjs +29 -14
  38. package/engine/content-embeds.mjs +1 -1
  39. package/engine/content-expand.mjs +376 -0
  40. package/engine/content-figures.mjs +18 -11
  41. package/engine/content-format.yaml +8 -0
  42. package/engine/content-glyphs.mjs +213 -0
  43. package/engine/content-images.mjs +6 -7
  44. package/engine/content-index.mjs +1 -1
  45. package/engine/content-links.mjs +3 -3
  46. package/engine/content-lint.mjs +1 -1
  47. package/engine/content-render.mjs +453 -0
  48. package/engine/content-scripts.mjs +376 -43
  49. package/engine/content-spans.mjs +6 -3
  50. package/engine/content-styles.mjs +91 -0
  51. package/engine/content-symbols.mjs +186 -0
  52. package/engine/content-verse.mjs +738 -0
  53. package/engine/core-report.mjs +169 -0
  54. package/engine/core-vocabulary.mjs +208 -0
  55. package/engine/dictionary-html.mjs +119 -0
  56. package/engine/dictionary-rows.mjs +15 -4
  57. package/engine/field-reference.mjs +5 -5
  58. package/engine/first-use.mjs +17 -27
  59. package/engine/frontmatter-lint.mjs +1 -1
  60. package/engine/heading-attributes.mjs +89 -0
  61. package/engine/helpers.mjs +61 -101
  62. package/engine/homepage.mjs +17 -29
  63. package/engine/html-minify.mjs +70 -0
  64. package/engine/index.mjs +8 -5
  65. package/engine/key-case-collisions.mjs +136 -0
  66. package/engine/language-data.mjs +203 -6
  67. package/engine/language-rows.mjs +8 -2
  68. package/engine/language-terms.mjs +186 -16
  69. package/engine/language-words.mjs +12 -0
  70. package/engine/logographic.mjs +255 -0
  71. package/engine/map-lexicon.mjs +262 -47
  72. package/engine/map-places.mjs +5 -4
  73. package/engine/markdown-expressions.mjs +0 -2
  74. package/engine/markdownlint-run.mjs +9 -3
  75. package/engine/markup-validation.mjs +21 -0
  76. package/engine/metre-declaration.mjs +134 -0
  77. package/engine/names.mjs +36 -9
  78. package/engine/note-matter.mjs +66 -0
  79. package/engine/note-vocabulary.mjs +187 -3
  80. package/engine/pdf-build.mjs +69 -133
  81. package/engine/pdf-render.mjs +205 -58
  82. package/engine/post-fields.mjs +157 -0
  83. package/engine/preview-index.mjs +130 -0
  84. package/engine/preview-source.mjs +450 -0
  85. package/engine/project-init.mjs +5 -12
  86. package/engine/prose-config.mjs +3 -3
  87. package/engine/read-stdin.mjs +29 -0
  88. package/engine/run-rows.mjs +2 -2
  89. package/engine/run-words.mjs +6 -112
  90. package/engine/script-emphasis-terms.mjs +38 -0
  91. package/engine/script-emphasis.mjs +295 -0
  92. package/engine/script-font-delivery.mjs +4 -4
  93. package/engine/site-build.mjs +260 -315
  94. package/engine/site-config.mjs +26 -238
  95. package/engine/site-files.mjs +175 -0
  96. package/engine/site-index.mjs +9 -4
  97. package/engine/site-preview.mjs +52 -525
  98. package/engine/site-render.mjs +593 -0
  99. package/engine/site-root.mjs +22 -13
  100. package/engine/site-search.mjs +161 -0
  101. package/engine/site-serve.mjs +273 -0
  102. package/engine/site-theme-banner.mjs +154 -0
  103. package/engine/site-theme-urls.mjs +125 -0
  104. package/engine/site-theme.mjs +926 -0
  105. package/engine/sql-tables.mjs +20 -3
  106. package/engine/text-maps.mjs +57 -10
  107. package/engine/typographer.mjs +248 -0
  108. package/engine/verse-metre.mjs +357 -0
  109. package/engine/wikilink-syntax.mjs +29 -47
  110. package/engine/wikilinks.mjs +758 -279
  111. package/engine/word-forms.mjs +364 -0
  112. package/engine/word-rows.mjs +12 -2
  113. package/engine/word-terms.mjs +38 -4
  114. package/manifest.mjs +3 -4
  115. package/package.json +11 -8
  116. package/{hugo-theme → site-theme}/data/banners.yaml +6 -10
  117. package/site-theme/layouts/base.hbs +31 -0
  118. package/site-theme/layouts/blog-list.hbs +30 -0
  119. package/site-theme/layouts/blog-post.hbs +40 -0
  120. package/site-theme/layouts/home.hbs +35 -0
  121. package/site-theme/layouts/list.hbs +20 -0
  122. package/site-theme/layouts/not-found.hbs +31 -0
  123. package/site-theme/layouts/page.hbs +7 -0
  124. package/site-theme/partials/copy-buttons.hbs +37 -0
  125. package/site-theme/partials/draft-notice.hbs +5 -0
  126. package/site-theme/partials/exturl.hbs +2 -0
  127. package/{hugo-theme/layouts/partials/footer.html → site-theme/partials/footer.hbs} +10 -7
  128. package/site-theme/partials/header.hbs +44 -0
  129. package/site-theme/partials/hero-banner.hbs +11 -0
  130. package/site-theme/partials/infobox-entry.hbs +2 -0
  131. package/site-theme/partials/infobox-section.hbs +31 -0
  132. package/site-theme/partials/infobox-value.hbs +2 -0
  133. package/site-theme/partials/infobox.hbs +18 -0
  134. package/site-theme/partials/list-members.hbs +9 -0
  135. package/site-theme/partials/nav-links.hbs +5 -0
  136. package/site-theme/partials/page-body.hbs +48 -0
  137. package/site-theme/partials/page-list.hbs +14 -0
  138. package/site-theme/partials/related.hbs +24 -0
  139. package/site-theme/partials/search-meta.hbs +6 -0
  140. package/site-theme/partials/search-names.hbs +2 -0
  141. package/site-theme/partials/search.hbs +52 -0
  142. package/site-theme/partials/tags.hbs +4 -0
  143. package/site-theme/partials/toc.hbs +7 -0
  144. package/{hugo-theme → site-theme}/static/css/style.css +106 -14
  145. package/stage.mjs +1 -2
  146. package/types/config.d.mts +8 -4
  147. package/types/content-config.d.mts +15 -15
  148. package/types/engine/asset-shipping.d.mts +5 -2
  149. package/types/engine/base-compiler.d.mts +20 -2
  150. package/types/engine/code-fences.d.mts +3 -3
  151. package/types/engine/content-address.d.mts +1 -1
  152. package/types/engine/content-alerts.d.mts +8 -2
  153. package/types/engine/content-blocks.d.mts +11 -4
  154. package/types/engine/content-expand.d.mts +288 -0
  155. package/types/engine/content-figures.d.mts +11 -3
  156. package/types/engine/content-glyphs.d.mts +85 -0
  157. package/types/engine/content-images.d.mts +6 -7
  158. package/types/engine/content-links.d.mts +1 -1
  159. package/types/engine/content-render.d.mts +199 -0
  160. package/types/engine/content-scripts.d.mts +70 -4
  161. package/types/engine/content-spans.d.mts +1 -1
  162. package/types/engine/content-styles.d.mts +33 -0
  163. package/types/engine/content-symbols.d.mts +35 -0
  164. package/types/engine/content-verse.d.mts +240 -0
  165. package/types/engine/core-report.d.mts +45 -0
  166. package/types/engine/core-vocabulary.d.mts +53 -0
  167. package/types/engine/dictionary-html.d.mts +8 -0
  168. package/types/engine/dictionary-rows.d.mts +10 -0
  169. package/types/engine/field-reference.d.mts +2 -2
  170. package/types/engine/heading-attributes.d.mts +20 -0
  171. package/types/engine/helpers.d.mts +3 -10
  172. package/types/engine/homepage.d.mts +11 -24
  173. package/types/engine/html-minify.d.mts +9 -0
  174. package/types/engine/index.d.mts +2 -1
  175. package/types/engine/key-case-collisions.d.mts +28 -0
  176. package/types/engine/language-data.d.mts +0 -2
  177. package/types/engine/language-terms.d.mts +24 -2
  178. package/types/engine/logographic.d.mts +45 -0
  179. package/types/engine/map-lexicon.d.mts +66 -13
  180. package/types/engine/map-places.d.mts +8 -0
  181. package/types/engine/markdown-expressions.d.mts +0 -2
  182. package/types/engine/metre-declaration.d.mts +39 -0
  183. package/types/engine/names.d.mts +34 -3
  184. package/types/engine/note-matter.d.mts +48 -0
  185. package/types/engine/pdf-render.d.mts +6 -2
  186. package/types/engine/post-fields.d.mts +44 -0
  187. package/types/engine/preview-index.d.mts +72 -0
  188. package/types/engine/preview-source.d.mts +51 -0
  189. package/types/engine/read-stdin.d.mts +11 -0
  190. package/types/engine/run-rows.d.mts +2 -2
  191. package/types/engine/run-words.d.mts +0 -15
  192. package/types/engine/script-emphasis-terms.d.mts +21 -0
  193. package/types/engine/script-emphasis.d.mts +84 -0
  194. package/types/engine/script-font-delivery.d.mts +4 -4
  195. package/types/engine/site-build.d.mts +71 -113
  196. package/types/engine/site-config.d.mts +14 -134
  197. package/types/engine/site-files.d.mts +73 -0
  198. package/types/engine/site-index.d.mts +11 -3
  199. package/types/engine/site-preview.d.mts +15 -19
  200. package/types/engine/site-render.d.mts +171 -0
  201. package/types/engine/site-root.d.mts +12 -4
  202. package/types/engine/site-search.d.mts +54 -0
  203. package/types/engine/site-serve.d.mts +44 -0
  204. package/types/engine/site-theme-banner.d.mts +51 -0
  205. package/types/engine/site-theme-urls.d.mts +60 -0
  206. package/types/engine/site-theme.d.mts +127 -0
  207. package/types/engine/sql-tables.d.mts +4 -1
  208. package/types/engine/text-maps.d.mts +17 -3
  209. package/types/engine/typographer.d.mts +11 -0
  210. package/types/engine/verse-metre.d.mts +131 -0
  211. package/types/engine/wikilink-syntax.d.mts +30 -48
  212. package/types/engine/wikilinks.d.mts +330 -14
  213. package/types/engine/word-forms.d.mts +83 -0
  214. package/types/manifest.d.mts +3 -4
  215. package/types/stage.d.mts +1 -2
  216. package/types/vendor/markdownlint.d.mts +8 -0
  217. package/vendor/markdownlint.mjs +20388 -0
  218. package/engine/content-poetry.mjs +0 -240
  219. package/engine/web-wikilinks.mjs +0 -594
  220. package/hugo-theme/layouts/404.html +0 -85
  221. package/hugo-theme/layouts/_default/baseof.html +0 -67
  222. package/hugo-theme/layouts/_default/list.html +0 -139
  223. package/hugo-theme/layouts/_default/single.html +0 -4
  224. package/hugo-theme/layouts/blog/list.html +0 -54
  225. package/hugo-theme/layouts/blog-post/single.html +0 -36
  226. package/hugo-theme/layouts/index.html +0 -75
  227. package/hugo-theme/layouts/partials/banner-url.html +0 -141
  228. package/hugo-theme/layouts/partials/cdn-url.html +0 -36
  229. package/hugo-theme/layouts/partials/dictionary-lists.html +0 -58
  230. package/hugo-theme/layouts/partials/draft-notice.html +0 -41
  231. package/hugo-theme/layouts/partials/draft-state.html +0 -26
  232. package/hugo-theme/layouts/partials/header.html +0 -35
  233. package/hugo-theme/layouts/partials/hero-banner.html +0 -85
  234. package/hugo-theme/layouts/partials/infobox/entry.html +0 -18
  235. package/hugo-theme/layouts/partials/infobox/items.html +0 -26
  236. package/hugo-theme/layouts/partials/infobox/section.html +0 -56
  237. package/hugo-theme/layouts/partials/infobox/value.html +0 -43
  238. package/hugo-theme/layouts/partials/infobox.html +0 -57
  239. package/hugo-theme/layouts/partials/page-body.html +0 -152
  240. package/hugo-theme/layouts/partials/related.html +0 -138
  241. package/hugo-theme/layouts/partials/search.html +0 -84
  242. package/hugo-theme/layouts/partials/site-url.html +0 -32
  243. package/hugo-theme/layouts/partials/tags.html +0 -30
  244. package/hugo-theme/layouts/partials/toc.html +0 -28
  245. package/hugo-theme/layouts/partials/warn-missing-banner.html +0 -3
  246. package/hugo-theme/layouts/shortcodes/cartouche.html +0 -64
  247. package/hugo-theme/layouts/shortcodes/exturl.html +0 -12
  248. package/hugo-theme/layouts/shortcodes/glyph.html +0 -43
  249. package/hugo-theme/layouts/shortcodes/pagelist.html +0 -63
  250. package/hugo-theme/theme.toml +0 -11
  251. package/types/engine/content-poetry.d.mts +0 -58
  252. package/types/engine/web-wikilinks.d.mts +0 -107
  253. /package/{hugo-theme → site-theme}/data/banner-kinds.yaml +0 -0
  254. /package/{hugo-theme → site-theme}/static/css/sohl-icons.css +0 -0
  255. /package/{hugo-theme → site-theme}/static/fonts/game-icons-kb.woff2 +0 -0
  256. /package/{hugo-theme → site-theme}/static/img/banners/takheperu-hieroglyphs.svg +0 -0
  257. /package/{hugo-theme → site-theme}/static/img/cartouches/takheperu.svg +0 -0
  258. /package/{hugo-theme → site-theme}/static/js/inline-spoilers.js +0 -0
@@ -13,7 +13,6 @@
13
13
  */
14
14
 
15
15
  /** Render stdin with optional date-only configuration and no content index. */
16
- import { readFileSync } from "node:fs";
17
16
  import { formatDiagnostic } from "../engine/diagnostics.mjs";
18
17
 
19
18
  const HELP = `Usage: package-build-preview [--dark] [--current-date DATE] [--css]
@@ -54,10 +53,11 @@ try {
54
53
  else {
55
54
  const { renderPreview, previewStyles, readPreviewCurrentDate } =
56
55
  await import("../engine/site-preview.mjs");
56
+ const { readStdin } = await import("../engine/read-stdin.mjs");
57
57
  if (css) process.stdout.write(`${previewStyles()}\n`);
58
58
  else {
59
59
  currentDate ??= await readPreviewCurrentDate(process.cwd());
60
- const result = await renderPreview(readFileSync(0, "utf8"), {
60
+ const result = await renderPreview(await readStdin(), {
61
61
  dark,
62
62
  file: "<stdin>",
63
63
  currentDate,
@@ -91,7 +91,7 @@ import { checkForeignAssetBindings } from "../engine/asset-bindings.mjs";
91
91
  import { emitDiagnostic, formatDiagnostic, positionOfYamlPath } from "../engine/diagnostics.mjs";
92
92
  import { writeSiteRoot } from "../engine/site-root.mjs";
93
93
  import { DEPLOY_ROOT } from "../engine/site-config.mjs";
94
- import { compilesFoundryDocuments } from "../content-config.mjs";
94
+ import { compilesFoundryDocuments, siteBase } from "../content-config.mjs";
95
95
  import { loadPackConfig, packConfigPath, resolveConfigFile } from "../engine/pack-config.mjs";
96
96
  import { cleanBuildArtifacts, resetStage, stageAssets } from "../stage.mjs";
97
97
  import { rowsStagingPictureRoots, stagePackageAssets } from "../engine/asset-shipping.mjs";
@@ -889,7 +889,7 @@ function manifestCommand() {
889
889
  * `package-build site-root` — the deployment's root files, and the search
890
890
  * index.
891
891
  *
892
- * Hugo owns everything under the `/<package>/` prefix; this owns what sits
892
+ * The site render owns everything under the `/<package>/` prefix; this owns what sits
893
893
  * beside it, which is what Cloudflare Pages reads from the uploaded directory
894
894
  * and nowhere else: `_headers`, and no `_redirects`, since the prefix root is
895
895
  * the homepage. It then indexes the rendered pages for search into
@@ -913,6 +913,7 @@ function siteRootCommand() {
913
913
 
914
914
  const { files, search } = await writeSiteRoot({
915
915
  pkg: shared.contentPackage,
916
+ base: siteBase(shared),
916
917
  out,
917
918
  search: shared.site.search,
918
919
  });
@@ -12,15 +12,15 @@
12
12
  */
13
13
 
14
14
  /**
15
- * CI guard: every banner `data/banners.yaml` declares must actually be published.
15
+ * CI guard: every banner `site-theme/data/banners.yaml` declares must actually be published.
16
16
  *
17
- * `partials/hero-banner.html` needs to know which banner images exist, and Hugo
18
- * cannot ask a remote asset host that question at build time — so the answer is
19
- * declared in `data/banners.yaml` instead. This checks every declaration and
17
+ * The hero banner needs to know which banner images exist, and the build
18
+ * cannot ask a remote asset host that question — so the answer is
19
+ * declared in `site-theme/data/banners.yaml` instead. This checks every declaration and
20
20
  * accepts HTTP 200 or 206. HTTP 404 and 410 identify absent artwork; other HTTP
21
21
  * statuses and transport errors leave publication unverified and fail the check.
22
22
  *
23
- * It also refuses a name in `data/banner-kinds.yaml` that `banners.yaml` does
23
+ * It also refuses a name in `site-theme/data/banner-kinds.yaml` that `banners.yaml` does
24
24
  * not declare, before any request is made: a mapping to undrawn art is a dead
25
25
  * URL on every page of that kind.
26
26
  *
@@ -41,8 +41,8 @@ import { dirname, resolve } from "node:path";
41
41
  import { parse } from "yaml";
42
42
 
43
43
  const HERE = dirname(fileURLToPath(import.meta.url));
44
- const DATA_FILE = resolve(HERE, "..", "hugo-theme", "data", "banners.yaml");
45
- const KINDS_FILE = resolve(HERE, "..", "hugo-theme", "data", "banner-kinds.yaml");
44
+ const DATA_FILE = resolve(HERE, "..", "site-theme", "data", "banners.yaml");
45
+ const KINDS_FILE = resolve(HERE, "..", "site-theme", "data", "banner-kinds.yaml");
46
46
  const DEFAULT_BASE = "https://cdn.heroiclands.org";
47
47
 
48
48
  function parseArgs(argv) {
package/config.mjs CHANGED
@@ -160,7 +160,7 @@ const ARTIFACT_OF_KIND = Object.freeze({
160
160
  *
161
161
  * Exported so a sibling module composing a configuration value this module
162
162
  * does not itself validate — {@link module:engine/site-config}'s
163
- * `hugoConfig`, checking `site.assets` — reports through the one helper
163
+ * `siteSettings`, checking `site.assets` — reports through the one helper
164
164
  * rather than a second copy.
165
165
  *
166
166
  * @param {string} where - Dotted path of the offending key.
@@ -657,8 +657,7 @@ export function resolveAssetTransform(declared, rootDir) {
657
657
  *
658
658
  * A package's Foundry manifest already derives its own `url` from
659
659
  * `contentPackage` (`packageHomepage` in `manifest.mjs`); `homepage` states
660
- * the same address a second time, in `package.json`, for the generated Hugo
661
- * configuration to read a `baseURL` from without knowing where each
660
+ * the same address a second time, in `package.json`, for the site build to read a base URL from without knowing where each
662
661
  * repository keeps its own site configuration.
663
662
  *
664
663
  * Required unconditionally: every package publishes a site, so there is no
@@ -670,28 +669,44 @@ export function resolveAssetTransform(declared, rootDir) {
670
669
  * `packageHomepage(contentPackage)`, independent of it. The right caller is
671
670
  * whatever reads `homepage` to write a site's `baseURL`.
672
671
  *
672
+ * A site that states `site.base` is served at that path instead, and its
673
+ * `homepage` is that path on its origin: `/` makes it the origin's root.
674
+ *
673
675
  * @param {string|null} homepage - The resolved `package.json` `homepage`, or
674
676
  * `null` when none is declared.
675
677
  * @param {string} contentPackage - The resolved `contentPackage`.
678
+ * @param {string} [base] - The resolved `site.base`; `""` or absent for a
679
+ * package site, served at `/<contentPackage>/`.
676
680
  * @returns {void}
677
681
  */
678
- export function checkHomepage(homepage, contentPackage) {
682
+ export function checkHomepage(homepage, contentPackage, base = "") {
679
683
  if (homepage === null) {
680
684
  fail(
681
685
  "homepage",
682
686
  "is not declared in `package.json`, and every package needs one " +
683
687
  "to build its site's `baseURL` from. Add " +
684
- `\`https://www.heroiclands.org/${contentPackage}/\``,
688
+ `\`https://www.heroiclands.org${base || `/${contentPackage}/`}\``,
685
689
  );
686
690
  }
687
691
 
688
- const suffix = `/${contentPackage}/`;
689
692
  let pathname;
690
693
  try {
691
694
  pathname = new URL(homepage).pathname;
692
695
  } catch {
693
696
  fail("homepage", `is \`${homepage}\`, which is not an absolute URL`);
694
697
  }
698
+ if (base) {
699
+ if (pathname === base) return;
700
+ fail(
701
+ "homepage",
702
+ base === "/" ?
703
+ `is \`${homepage}\`, but \`site.base\` is \`/\`, so \`package.json\`'s ` +
704
+ "`homepage` must be the root of its origin"
705
+ : `is \`${homepage}\`, but \`site.base\` is \`${base}\`, so \`package.json\`'s ` +
706
+ `\`homepage\` must be served at \`${base}\``,
707
+ );
708
+ }
709
+ const suffix = `/${contentPackage}/`;
695
710
  if (!pathname.endsWith(suffix)) {
696
711
  fail(
697
712
  "homepage",
@@ -215,6 +215,18 @@ export const DEFAULT_ADDRESS_SCHEME = Object.freeze({
215
215
  prefix: "",
216
216
  });
217
217
 
218
+ /**
219
+ * The path a package's site is served at: `site.base` where it states one,
220
+ * `/<contentPackage>/` otherwise.
221
+ *
222
+ * @param {{contentPackage: string, site: {base: string}}} config - The resolved
223
+ * configuration.
224
+ * @returns {string} The path, beginning and ending with `/`.
225
+ */
226
+ export function siteBase(config) {
227
+ return config.site.base || `/${config.contentPackage}/`;
228
+ }
229
+
218
230
  /**
219
231
  * Whether this package publishes the pages its content tree compiles to.
220
232
  *
@@ -347,7 +359,7 @@ export function publishesContentPages(config) {
347
359
  * for *every* declared dependency, not
348
360
  * only those supplying a catalogue.
349
361
  * @property {string} [navigationCache] Where the site navigation is fetched
350
- * to, for the generated Hugo menu.
362
+ * to, for the site's menu.
351
363
  */
352
364
 
353
365
  /**
@@ -779,9 +791,13 @@ const SITE_KEYS = [
779
791
  "pass",
780
792
  "passOptions",
781
793
  "notfound",
782
- "hugo",
783
794
  "search",
784
795
  "maps",
796
+ "sections",
797
+ "feed",
798
+ "tags",
799
+ "sitemaps",
800
+ "mergeIndexes",
785
801
  ];
786
802
  const SITE_NOTFOUND_KEYS = ["tagline", "sitenoun", "heroimage", "links"];
787
803
  const SITE_NOTFOUND_LINK_KEYS = ["title", "url", "text"];
@@ -1715,46 +1731,6 @@ function normalizeSiteDescription(value) {
1715
1731
  return description;
1716
1732
  }
1717
1733
 
1718
- /**
1719
- * Hugo keys a repository may **not** declare under `site.hugo`, because the
1720
- * site build generates them and would only overwrite what was written.
1721
- *
1722
- * The same rule `DERIVED_MANIFEST_KEYS` states for the manifest, for the same
1723
- * reason: an authored `baseURL` would look authoritative, sit there unread,
1724
- * and disagree with the site forever. Each key names where its value comes
1725
- * from. A dotted key names a nested one, and covers everything beneath it —
1726
- * `params.brand` refuses `params.brand.logo` too — so `site.hugo` reaches only
1727
- * what the generator does not write.
1728
- *
1729
- * @type {Readonly<Record<string, string>>}
1730
- */
1731
- export const DERIVED_HUGO_KEYS = Object.freeze({
1732
- baseURL: "package.json `homepage`",
1733
- title: "`site.title` or `packageBuild.manifest.title`",
1734
- locale: "the organisation's locale, in `engine/site-config.mjs`",
1735
- publishDir: "`contentPackage`, under the deployment root `build/site`",
1736
- contentDir: "the fixed content mount, `build/hugo/content`",
1737
- themesDir: "where `@heroiclands/hugo-theme` is installed",
1738
- theme: "the installed `@heroiclands/hugo-theme`",
1739
- disableKinds:
1740
- "the toolchain, which renders a site as its homepage and its pages: " +
1741
- "`section`, `taxonomy`, `term` and `RSS` are disabled on every site",
1742
- taxonomies:
1743
- "the toolchain, which renders a site as its homepage and its pages: no " +
1744
- "taxonomy is declared, because `taxonomy` and `term` are disabled kinds",
1745
- outputs:
1746
- "the toolchain, which renders a site as its homepage and its pages: no " +
1747
- "output format is declared, because every listing kind is disabled",
1748
- "params.description": "`site.description`",
1749
- "params.author": "the first entry of `authors`",
1750
- "params.cdnBaseURL": "`site.assets`",
1751
- "params.brand": "the organisation's brand links, in `engine/site-config.mjs`",
1752
- "params.notfound": "`site.notfound`",
1753
- "markup.goldmark.renderer.unsafe": "the toolchain, whose pages carry raw HTML",
1754
- "markup.goldmark.extensions.taskList": "the toolchain, whose format has no checkbox",
1755
- menu: "the navigation `package-build deps fetch` caches from heroiclands.org",
1756
- });
1757
-
1758
1734
  /**
1759
1735
  * The `site.notfound` block — the wording of the "page not found" page.
1760
1736
  *
@@ -1799,67 +1775,177 @@ function normalizeSiteNotfound(value) {
1799
1775
  }
1800
1776
 
1801
1777
  /**
1802
- * The `site.hugo` block — a mapping deep-merged over the generated Hugo
1803
- * configuration, last.
1778
+ * The top-level paths a site writes itself, which no section may claim: the
1779
+ * tag pages, the theme's stylesheets, scripts, fonts and images, and the
1780
+ * search index.
1804
1781
  *
1805
- * The escape hatch for the key nobody anticipated. Every key the generator
1806
- * writes is refused here by {@link DERIVED_HUGO_KEYS}, naming its source, so
1807
- * the block cannot grow into a second configuration file.
1782
+ * @type {readonly string[]}
1783
+ */
1784
+ const SITE_WRITTEN_SEGMENTS = Object.freeze(["tags", "css", "js", "fonts", "img", "pagefind"]);
1785
+
1786
+ /**
1787
+ * A note's address as a section names its page: `<type>-<shortcode>`, the
1788
+ * form a page is published at.
1789
+ */
1790
+ const PAGE_ADDRESS = /^[a-z][a-z0-9]*-[a-z0-9]+$/;
1791
+
1792
+ /**
1793
+ * Where a site is served, as `site.base` states it: a path that begins and
1794
+ * ends with `/`. `/` is the root of the origin.
1795
+ *
1796
+ * @param {unknown} value - The configured value, or `undefined`.
1797
+ * @returns {string} The path; `""` when unset, which is `/<contentPackage>/`.
1798
+ */
1799
+ function normalizeSiteBase(value) {
1800
+ if (value === undefined) return "";
1801
+ const base = requireNonEmptyString(value, "site.base");
1802
+ if (!base.startsWith("/") || !base.endsWith("/")) {
1803
+ fail(
1804
+ "site.base",
1805
+ `is \`${base}\`; it is the path the site is served at, which begins and ends with \`/\` ` +
1806
+ "— `/` for the root of the origin",
1807
+ );
1808
+ }
1809
+ return base;
1810
+ }
1811
+
1812
+ /**
1813
+ * One address segment naming a section, as `site.sections` keys it and
1814
+ * `site.feed` names it.
1815
+ *
1816
+ * @param {string} name - The section's name.
1817
+ * @param {string} field - Dotted path, for the error.
1818
+ * @returns {string} The name.
1819
+ */
1820
+ function sectionName(name, field) {
1821
+ if (!isAddressSegment(name)) {
1822
+ fail(
1823
+ field,
1824
+ `names the section \`${name}\`, which is published at \`/${name}/\` and so is one ` +
1825
+ "address segment: lowercase letters and digits",
1826
+ );
1827
+ }
1828
+ if (SITE_WRITTEN_SEGMENTS.includes(name)) {
1829
+ fail(
1830
+ field,
1831
+ `names the section \`${name}\`, a path the site writes itself ` +
1832
+ `(${SITE_WRITTEN_SEGMENTS.join(", ")})`,
1833
+ );
1834
+ }
1835
+ return name;
1836
+ }
1837
+
1838
+ /**
1839
+ * The `site.sections` mapping: each section's name, and the address of the
1840
+ * note that is its page.
1808
1841
  *
1809
1842
  * @param {unknown} value - The block, or `undefined`.
1810
- * @returns {Readonly<Record<string, unknown>>} It, frozen; `{}` when absent.
1843
+ * @returns {Readonly<Record<string, string>>} It, frozen; `{}` when absent.
1811
1844
  */
1812
- function normalizeSiteHugo(value) {
1845
+ function normalizeSiteSections(value) {
1813
1846
  if (value === undefined) return Object.freeze({});
1814
- if (!isPlainObject(value)) fail("site.hugo", "must be a mapping");
1815
- const input = /** @type {Record<string, unknown>} */ (value);
1816
- for (const [dotted, source] of Object.entries(DERIVED_HUGO_KEYS)) {
1817
- /** @type {unknown} */
1818
- let at = input;
1819
- for (const part of dotted.split(".")) {
1820
- at = isPlainObject(at) ? /** @type {Record<string, unknown>} */ (at)[part] : undefined;
1821
- if (at === undefined) break;
1847
+ if (!isPlainObject(value)) {
1848
+ fail(
1849
+ "site.sections",
1850
+ "must be a mapping of a section's name to the address of the note that is its page",
1851
+ );
1852
+ }
1853
+ /** @type {Record<string, string>} */
1854
+ const out = {};
1855
+ /** @type {Map<string, string>} */
1856
+ const claimed = new Map();
1857
+ for (const [name, page] of Object.entries(/** @type {object} */ (value))) {
1858
+ const field = `site.sections.${name}`;
1859
+ sectionName(name, field);
1860
+ if (typeof page !== "string" || !PAGE_ADDRESS.test(page)) {
1861
+ fail(
1862
+ field,
1863
+ `is \`${String(page)}\`; it is the address of the note that is the section's ` +
1864
+ "page, written `<type>-<shortcode>`, such as `doc-blog`",
1865
+ );
1822
1866
  }
1823
- if (at !== undefined) {
1867
+ if (page.startsWith("homepage-")) {
1824
1868
  fail(
1825
- `site.hugo.${dotted}`,
1826
- `is derived from ${source} and must not be declared — it ` +
1827
- `would be overwritten, and the two would disagree with ` +
1828
- `nothing to say so`,
1869
+ field,
1870
+ `is \`${page}\`, and the homepage is the site's front page, not a section's`,
1829
1871
  );
1830
1872
  }
1873
+ if (claimed.has(page)) {
1874
+ fail(field, `is \`${page}\`, which is already the page of \`${claimed.get(page)}\``);
1875
+ }
1876
+ claimed.set(page, name);
1877
+ out[name] = page;
1878
+ }
1879
+ return Object.freeze(out);
1880
+ }
1881
+
1882
+ /**
1883
+ * A list of package sites, by `contentPackage`, as `site.sitemaps` and
1884
+ * `site.mergeIndexes` name them.
1885
+ *
1886
+ * @param {unknown} value - The list, or `undefined`.
1887
+ * @param {string} field - Dotted path, for the error.
1888
+ * @param {string} contentPackage - This site's own package, which no entry names.
1889
+ * @returns {readonly string[]} The names, frozen; `[]` when absent.
1890
+ */
1891
+ function packageSiteList(value, field, contentPackage) {
1892
+ if (value === undefined) return Object.freeze([]);
1893
+ if (!Array.isArray(value)) {
1894
+ fail(field, "must be a list of package sites, each named by its `contentPackage`");
1831
1895
  }
1832
- return Object.freeze(structuredClone(input));
1896
+ const seen = new Set();
1897
+ return Object.freeze(
1898
+ value.map((entry, i) => {
1899
+ const where = `${field}[${i}]`;
1900
+ if (!isAddressSegment(entry)) {
1901
+ fail(
1902
+ where,
1903
+ `is \`${String(entry)}\`, which is not a package site: name one by its ` +
1904
+ "`contentPackage`, lowercase letters and digits",
1905
+ );
1906
+ }
1907
+ if (entry === contentPackage) {
1908
+ fail(where, `is \`${entry}\`, this site's own package`);
1909
+ }
1910
+ if (seen.has(entry)) fail(where, `names \`${entry}\` more than once`);
1911
+ seen.add(entry);
1912
+ return /** @type {string} */ (entry);
1913
+ }),
1914
+ );
1833
1915
  }
1834
1916
 
1835
1917
  /**
1836
1918
  * The `site` section — how this repository frames the website it publishes.
1837
1919
  *
1838
1920
  * Everything here is *framing*: which named pass bundle supplies the
1839
- * repository's own body rewrites, and the residue of the generated Hugo
1840
- * configuration that is genuinely this repository's own. Where the Hugo tree
1841
- * is written is not a choice: `package-build site` writes it under
1842
- * `build/hugo/`. How a page gets its
1843
- * **address** is deliberately not here either — that is `publish.address`,
1921
+ * repository's own body rewrites, and the residue of the site that is
1922
+ * genuinely this repository's own. How a page gets its
1923
+ * **address** is deliberately not here — that is `publish.address`,
1844
1924
  * shared with the link manifest so the two cannot disagree about where a
1845
1925
  * page is.
1846
1926
  *
1847
1927
  * Two switches sit here, both on by default. `search` is whether
1848
1928
  * `package-build site-root` writes a search index beside the rendered pages.
1849
- * `maps` is whether `package-build site` draws the map from each place that
1929
+ * `maps` is whether `package-build site build` draws the map from each place that
1850
1930
  * states a border or a route and writes it into that place's page — a site
1851
1931
  * carries both unless it says otherwise.
1852
1932
  *
1853
- * **What the site publishes is the homepage and the content tree, and nothing
1854
- * beside them.** A page of documentation is a note — `type: doc`, addressed
1855
- * by its shortcode, compiling into no document where it says `pack: none` —
1856
- * so there is no second mechanism for mounting a directory of markdown. An
1857
- * index between the homepage and the pages is a `doc` note too.
1933
+ * **A site is its homepage, its pages and the listings it asks for.** A page
1934
+ * of documentation is a note — `type: doc`, addressed by its shortcode,
1935
+ * compiling into no document where it says `pack: none` — so there is no
1936
+ * second mechanism for mounting a directory of markdown. `sections` publishes
1937
+ * a note at `/<section>/` as that section's page; `feed` names the section a
1938
+ * site's posts are listed and fed in; `tags` writes a page for each tag.
1939
+ * `base` is where the site is served, and a site at the root of its origin
1940
+ * (`/`) may name the package sites beside it: `sitemaps` for its sitemap
1941
+ * index and `robots.txt`, `mergeIndexes` for the search indexes its own
1942
+ * search reads.
1858
1943
  *
1859
1944
  * @param {unknown} value - The `site` block, or `undefined`.
1945
+ * @param {string} contentPackage - This repository's package.
1860
1946
  * @returns {Readonly<object>} It, frozen, with every default filled.
1861
1947
  */
1862
- function normalizeSite(value) {
1948
+ function normalizeSite(value, contentPackage) {
1863
1949
  const empty = Object.freeze({
1864
1950
  base: "",
1865
1951
  title: "",
@@ -1869,9 +1955,13 @@ function normalizeSite(value) {
1869
1955
  pass: "",
1870
1956
  passOptions: Object.freeze({}),
1871
1957
  notfound: null,
1872
- hugo: Object.freeze({}),
1873
1958
  search: true,
1874
1959
  maps: true,
1960
+ sections: Object.freeze({}),
1961
+ feed: "",
1962
+ tags: false,
1963
+ sitemaps: Object.freeze([]),
1964
+ mergeIndexes: Object.freeze([]),
1875
1965
  });
1876
1966
  if (value === undefined) return empty;
1877
1967
  if (!isPlainObject(value)) fail("site", "must be a mapping");
@@ -1886,8 +1976,28 @@ function normalizeSite(value) {
1886
1976
  packages = input.packages.map((p, i) => requireNonEmptyString(p, `site.packages[${i}]`));
1887
1977
  }
1888
1978
 
1979
+ const base = normalizeSiteBase(input.base);
1980
+ const search = optionalBoolean(input.search, "site.search", true);
1981
+ const sitemaps = packageSiteList(input.sitemaps, "site.sitemaps", contentPackage);
1982
+ if (sitemaps.length && base !== "/") {
1983
+ fail(
1984
+ "site.sitemaps",
1985
+ "names the package sites a sitemap index lists, and a sitemap index and " +
1986
+ "`robots.txt` belong at the root of an origin; this site is served at " +
1987
+ `\`${base || `/${contentPackage}/`}\`, so declare \`site.base: /\` or drop the key`,
1988
+ );
1989
+ }
1990
+ const mergeIndexes = packageSiteList(input.mergeIndexes, "site.mergeIndexes", contentPackage);
1991
+ if (mergeIndexes.length && !search) {
1992
+ fail(
1993
+ "site.mergeIndexes",
1994
+ "names the search indexes this site's search reads beside its own, and " +
1995
+ "`site.search` is `false`, so there is no search to read them",
1996
+ );
1997
+ }
1998
+
1889
1999
  return Object.freeze({
1890
- base: input.base === undefined ? "" : requireNonEmptyString(input.base, "site.base"),
2000
+ base,
1891
2001
  title: input.title === undefined ? "" : requireNonEmptyString(input.title, "site.title"),
1892
2002
  assets: normalizeSiteAssets(input.assets),
1893
2003
  description: normalizeSiteDescription(input.description),
@@ -1898,9 +2008,16 @@ function normalizeSite(value) {
1898
2008
  Object.freeze({})
1899
2009
  : Object.freeze({ ...input.passOptions }),
1900
2010
  notfound: normalizeSiteNotfound(input.notfound),
1901
- hugo: normalizeSiteHugo(input.hugo),
1902
- search: optionalBoolean(input.search, "site.search", true),
2011
+ search,
1903
2012
  maps: optionalBoolean(input.maps, "site.maps", true),
2013
+ sections: normalizeSiteSections(input.sections),
2014
+ feed:
2015
+ input.feed === undefined ?
2016
+ ""
2017
+ : sectionName(requireNonEmptyString(input.feed, "site.feed"), "site.feed"),
2018
+ tags: optionalBoolean(input.tags, "site.tags", false),
2019
+ sitemaps,
2020
+ mergeIndexes,
1904
2021
  });
1905
2022
  }
1906
2023
 
@@ -3150,7 +3267,9 @@ export function defineConfig(config) {
3150
3267
  packs: Object.freeze(packs),
3151
3268
  packDirectories: Object.freeze(packDirectories),
3152
3269
  docs: normalizeDocs(input.docs),
3153
- site: normalizeSite(input.site),
3270
+ // Validated above, in `contentPackage`: a site names the package sites
3271
+ // beside it, and never itself.
3272
+ site: normalizeSite(input.site, /** @type {string} */ (input.contentPackage)),
3154
3273
  // Validated above, in `contentPackage`: a book's default file stem is
3155
3274
  // `<contentPackage>-<id>`.
3156
3275
  pdf: normalizePdf(input.pdf, rootDir, /** @type {string} */ (input.contentPackage)),