@heroiclands/package-build 22.1.1 → 22.3.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 (42) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/CONTENT.md +27 -32
  3. package/bin/content-build.mjs +0 -3
  4. package/bin/package-build.mjs +9 -26
  5. package/config.mjs +7 -2
  6. package/content-config.mjs +73 -41
  7. package/docs/api.md +26 -25
  8. package/docs/commands.md +22 -22
  9. package/docs/configuration.md +78 -46
  10. package/docs/content-format.md +36 -6
  11. package/engine/foundry-entries.mjs +12 -1
  12. package/engine/frontmatter-lint.mjs +2 -3
  13. package/engine/helpers.mjs +4 -1
  14. package/engine/ids.mjs +12 -0
  15. package/engine/note-claims.mjs +21 -0
  16. package/engine/pack-config.mjs +14 -1
  17. package/engine/pack-router.mjs +82 -5
  18. package/engine/pdf-build.mjs +2 -2
  19. package/engine/retired-fields.mjs +2 -3
  20. package/engine/schema-check.mjs +6 -6
  21. package/engine/site-build.mjs +59 -218
  22. package/engine/site-config.mjs +22 -22
  23. package/engine/site-index.mjs +18 -62
  24. package/engine/web-wikilinks.mjs +2 -16
  25. package/engine/wikilinks.mjs +35 -14
  26. package/manifest.mjs +16 -10
  27. package/package.json +1 -1
  28. package/release.mjs +41 -8
  29. package/sohl/kb-passes.mjs +14 -85
  30. package/types/config.d.mts +17 -0
  31. package/types/content-config.d.mts +6 -6
  32. package/types/engine/ids.d.mts +11 -0
  33. package/types/engine/pack-router.d.mts +20 -2
  34. package/types/engine/schema-check.d.mts +5 -5
  35. package/types/engine/site-build.d.mts +12 -62
  36. package/types/engine/site-config.d.mts +6 -8
  37. package/types/engine/site-index.d.mts +3 -28
  38. package/types/engine/web-wikilinks.d.mts +1 -1
  39. package/types/engine/wikilinks.d.mts +6 -3
  40. package/types/manifest.d.mts +5 -3
  41. package/types/release.d.mts +7 -6
  42. package/types/sohl/kb-passes.d.mts +5 -36
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 22.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a79b33a: **A note may declare `pack: none`.** The universal `pack` key accepts one more value: `none` says the note publishes to the site and compiles into no Foundry document. Such a note is walked, published as a page, present in the content index with an address and no UUID, and addressable by wikilink like any other — on the web the link reaches the page, and in a compiled journal the reader gets the link's text with no document to open. Every pack compiler passes over it without a finding. It is accepted only on a type whose sole document is the JournalEntry its prose becomes (`doc`, `place`, `lore`, `scenario`) and refused by name on a type that compiles an Item, an Actor, a Macro, a Scene or an Adventure, naming the document it would drop. It is read per system like any other `pack:`, and a configured pack may not be called `none`.
8
+
9
+ **`site.trees` and `site.readmeSections` are refused.** A page is a note in the content tree, and there is no second mechanism for mounting a directory of markdown beside it. A configuration declaring either key fails with a message saying so. A package that mounted a directory of pages moves them into `assets/content/` as `doc` notes — each with a `shortcode` and `pack: none` — rewrites their relative links as wikilinks, and declares the section that lists them under `site.sections`. The `sohlKb` site pass no longer rewrites repository-relative links, and its `blob` option has no reader.
10
+
11
+ ## 22.2.0
12
+
13
+ ### Minor Changes
14
+
15
+ - 241451e: **`schema.json` is a release asset, not a committed file.** `package-build schema` writes `build/schema.json` instead of the repository root, and `--check` is gone — there is no committed copy left to compare against. `package-build release` publishes `schema.json` beside the archive and the manifest, the way it already publishes the content index, whenever the staged tree carries one.
16
+
17
+ To keep publishing a schema: drop the committed `schema.json` and any lint step that runs `package-build schema --check`; add `{ from: build/schema.json, to: schema.json }` to `packageBuild.assets`, and run `package-build schema` before `package-build assets` in the build chain so the release never ships one older than the source it was cut from.
18
+ - 483b630: **`content-build site` refuses to generate a configuration with no `site.assets`.** The theme resolves every relative asset — the brand logo, the 404 hero, every CDN-resolved image — against `site.assets`, and there is no defensible default: a package that built a site with the key absent published every one of those as a broken relative path. Declare `site.assets` — the `https://` host every package's imagery is served from — in `package-build.config.yaml` for any package that publishes a site.
19
+ - 985f599: **A package now describes itself once for Foundry and once for the site.** Foundry's package browser wants a pitch — HTML, any length — and a site's `<meta name="description">` wants one plain sentence; deriving both from `package.json`'s `description` forced one string onto both. Declare `packageBuild.manifest.descriptionHtml` in `package-build.config.yaml` for the Foundry pitch (HTML allowed, emitted as the manifest's `description`) and `site.description` for the site's meta description (plain text, required for `content-build site`). `package.json`'s own `description` is read by neither any more — a warning names both keys when one is still declared — and the field can be deleted.
20
+
3
21
  ## 22.1.1
4
22
 
5
23
  ### Patch Changes
package/CONTENT.md CHANGED
@@ -488,6 +488,17 @@ pack: mysteries
488
488
  type one system maps and another does not stays silent for the system that
489
489
  declines it.
490
490
 
491
+ - **`pack: none` compiles the note into no document, on purpose.** The note is
492
+ walked, published as a page, present in the content index with an address
493
+ and no UUID, and addressable by wikilink; every pass passes over it without
494
+ a finding, and the finding above says nothing about it. It is accepted only
495
+ on a type whose sole document is the JournalEntry its prose becomes — a
496
+ `doc`, say — and refused by name on a type that compiles an Item, an Actor,
497
+ a Macro, a Scene or an Adventure, where it would drop that document. A
498
+ configured pack may not be called `none`. This is how a page of developer
499
+ documentation lives in the content tree beside everything else: it is a
500
+ note like any other, and it reaches no compendium.
501
+
491
502
  **The configuration is found by walking up from the working directory, and from
492
503
  the installed package only if that finds nothing.** `engine/pack-config.mjs`
493
504
  climbs from `process.cwd()` first, so a build reads the tree it was run in —
@@ -2256,10 +2267,10 @@ note-format knowledge against game-system knowledge, and a homepage carries no
2256
2267
 
2257
2268
  `publish.site` then says how much _else_ is published:
2258
2269
 
2259
- | Mode | What is published |
2260
- | ---------- | ------------------------------------------------------------------------------- |
2261
- | `homepage` | The authored homepage, and no other page. **The default, and the floor.** |
2262
- | `content` | The homepage plus every page the content tree compiles to, and its extra trees. |
2270
+ | Mode | What is published |
2271
+ | ---------- | ------------------------------------------------------------------------- |
2272
+ | `homepage` | The authored homepage, and no other page. **The default, and the floor.** |
2273
+ | `content` | The homepage plus every page the content tree compiles to. |
2263
2274
 
2264
2275
  There is no value meaning "no web presence": every package publishes its
2265
2276
  homepage. A boolean is refused, with a message naming the mode to write
@@ -2273,8 +2284,8 @@ content_ — journal text, artwork, item descriptions, compiled notes — and a
2273
2284
  announcing the module discloses none of it. Because the failure mode is silent,
2274
2285
  the mode **fences the content surfaces off** rather than trusting a
2275
2286
  configuration to stay empty: in `homepage` mode the tree is never walked for
2276
- pages, and `sections`, `trees`, `landing` and `backfillSections` emit nothing
2277
- even when they are declared.
2287
+ pages, and `sections`, `landing` and `backfillSections` emit nothing even when
2288
+ they are declared.
2278
2289
 
2279
2290
  That is separate from the **dependency** edge, which such a module also
2280
2291
  declines: being cited by another package is what would stop it being
@@ -2293,9 +2304,12 @@ does not reach it.
2293
2304
  **What it does not do is decide addresses.** Those come from `publish.address`,
2294
2305
  the same setting the content index reads, so a page and its index record cannot
2295
2306
  disagree about where the page is. Everything under `site:` is _framing_ —
2296
- what a section is called, which extra trees are published beside the content,
2297
- and the residue of the generated Hugo configuration that is this repository's
2298
- own:
2307
+ what a section is called, and the residue of the generated Hugo configuration
2308
+ that is this repository's own. What the site publishes is the content tree and
2309
+ nothing beside it: a page of documentation is a note (`type: doc`, with
2310
+ `pack: none` where it compiles into no document), so there is no second
2311
+ mechanism for mounting a directory of markdown, and a `trees:` or
2312
+ `readmeSections:` key is refused with a message saying so.
2299
2313
 
2300
2314
  ```yaml
2301
2315
  site:
@@ -2307,16 +2321,12 @@ site:
2307
2321
  passOptions:
2308
2322
  apiBase: /sohl/api/
2309
2323
  symbolMap: kb/data/api-symbols.json
2310
- blob: https://github.com/HeroicLands/…/blob/main/
2311
- trees:
2312
- - { from: kb/dev-docs, section: dev-docs }
2313
2324
  sections:
2314
2325
  being:
2315
2326
  title: Beings
2316
2327
  banner: banners/creature.webp
2317
2328
  description: Folk, animals and the things that walk the world.
2318
- readmeSections:
2319
- dev-docs: { title: Developer Documentation, banner: banners/dev-docs.webp }
2329
+ reference: { title: Reference, listType: doc, listSubType: reference }
2320
2330
  list: { shortcodes: true }
2321
2331
  notfound:
2322
2332
  tagline: Song of Heroic Lands has no page at
@@ -2330,10 +2340,8 @@ site:
2330
2340
  | `base` | Where the package is served: the prefix on every rendered `href`, and what a manifest `path` is measured against. It reaches no page's own `url:` — see [A page's URL is its address](#a-pages-url-is-its-address). Defaults to `/<contentPackage>/`. |
2331
2341
  | `packages` | Which content packages this site renders. Defaults to its own. |
2332
2342
  | `sections` | The Hugo sections this site declares, and what each says about itself — see below. |
2333
- | `readmeSections` | The same, for a `trees` entry, whose landing comes from its own `README`. |
2334
2343
  | `landing` | Frontmatter for the mount's own `_index.md`. Passed through — the vocabulary is the theme's. |
2335
2344
  | `backfillSections` | Write a bare `_index.md` for any other directory directly under the mount. |
2336
- | `trees` | Extra source trees published beside the content, preserving their source layout below a section. |
2337
2345
  | `pass` | A named bundle of this repository's own body rewrites. |
2338
2346
  | `passOptions` | That bundle's options. |
2339
2347
  | `assets` | The host every package's imagery is served from; the generated `params.cdnBaseURL`. |
@@ -2377,12 +2385,6 @@ Whatever the entry may carry is the whole of what the section can say.
2377
2385
  | `listType` | no | The content **type** whose pages this section lists. |
2378
2386
  | `listSubType` | no | Narrows that to one **subType**. Only with a `listType`. |
2379
2387
 
2380
- `readmeSections` takes the same keys, for a **`trees`** entry: those pages keep
2381
- their source layout below a named section, so the tree's own `README.md` is that
2382
- section's landing. What the section declares wins over what the `README` happens
2383
- to carry — the landing has to match the card that links to it. No content note
2384
- reaches this map any more.
2385
-
2386
2388
  #### Saying what a section lists
2387
2389
 
2388
2390
  A generic list layout has nothing to render, because the membership a section
@@ -2477,15 +2479,9 @@ a site publishes dead `{@link}` tags at exit 0. A map that is read reports
2477
2479
  its symbol count at info level, which is the only way to tell a map that loaded
2478
2480
  from one that loaded empty without reading the emitted HTML.
2479
2481
 
2480
- A bundle supplies up to two hooks, and their order around the shared work is the
2481
- point:
2482
-
2483
- 1. `beforeLinks`, on every page, before wikilinks resolve — a `{@link}` tag may
2484
- sit in prose a wikilink also touches.
2485
- 2. `afterLinks`, on pages from an extra tree only — repository-relative links are
2486
- a property of how those pages are authored, not of content notes.
2487
-
2488
- Both run inside code-fence protection, so neither can rewrite a fenced example.
2482
+ A bundle supplies one hook, `beforeLinks`, which runs on every page before
2483
+ wikilinks resolve — a `{@link}` tag may sit in prose a wikilink also touches.
2484
+ It runs inside code-fence protection, so it cannot rewrite a fenced example.
2489
2485
 
2490
2486
  ### The gates
2491
2487
 
@@ -2499,7 +2495,6 @@ after the links that failed because of it reads as a pile of broken notes.
2499
2495
  | Addresses | A note with no shortcode to be addressed by, or no section to be filed under. |
2500
2496
  | Unusable manifest | A vendored manifest this build cannot read. |
2501
2497
  | Unaddressable manifest | One it can read but cannot look anything up in. |
2502
- | Package conflicts | One address claimed by two packages. |
2503
2498
  | Tables and wikilinks | A table directive that cannot be honoured, or a link that lands nowhere. |
2504
2499
 
2505
2500
  None of them exits the process from inside the library; the command decides. That
@@ -1561,9 +1561,6 @@ function siteCommand() {
1561
1561
  for (const f of gates.unaddressable) {
1562
1562
  console.error(formatUnaddressable(f, loadPackConfig()));
1563
1563
  }
1564
- for (const c of gates.conflicts) {
1565
- log.error(`address ${c.key} is also published by ${c.package}`);
1566
- }
1567
1564
  if (gatesFailed(gates)) {
1568
1565
  process.exitCode = 1;
1569
1566
  return;
@@ -334,9 +334,11 @@ async function formatGenerated(text, filepath) {
334
334
  * so the system publishes the field sets as data — the same shape the link
335
335
  * manifest already uses for addresses.
336
336
  *
337
- * `--check` fails when the committed copy disagrees with what the source would
338
- * produce now, because a generated file nothing checks drifts from its
339
- * generator silently and this one is read by other repositories.
337
+ * Writes under `build/`, alongside every other generated artifact: a release
338
+ * publishes it beside the archive and the manifest (see `package-build
339
+ * release`), and `packageBuild.assets` carries it into the staged tree from
340
+ * there. There is nothing committed to compare it against, so there is no
341
+ * `--check`.
340
342
  *
341
343
  * @returns {object} The yargs command module.
342
344
  */
@@ -344,15 +346,8 @@ function schemaCommand() {
344
346
  return {
345
347
  command: "schema",
346
348
  describe: "Publish this package's DataModel field sets as schema.json",
347
- builder: (y) =>
348
- y.option("check", {
349
- type: "boolean",
350
- default: false,
351
- describe:
352
- "Fail when the committed schema.json is out of date " +
353
- "rather than rewriting it",
354
- }),
355
- handler: handler(async (argv) => {
349
+ builder: (y) => y,
350
+ handler: handler(async () => {
356
351
  const config = loadPackageBuildConfig();
357
352
  if (!config.schema.length) {
358
353
  console.log(
@@ -369,25 +364,13 @@ function schemaCommand() {
369
364
  version: pkg.version,
370
365
  });
371
366
 
372
- const out = path.join(config.rootDir, SCHEMA_ARTIFACT_FILE);
367
+ const out = path.join(config.rootDir, "build", SCHEMA_ARTIFACT_FILE);
373
368
  const text = await formatGenerated(JSON.stringify(artifact), out);
374
369
  const counts = Object.entries(artifact.documents)
375
370
  .map(([kind, subtypes]) => `${Object.keys(subtypes).length} ${kind}`)
376
371
  .join(", ");
377
372
 
378
- if (argv.check) {
379
- const current = fs.existsSync(out) ? fs.readFileSync(out, "utf8") : null;
380
- if (current !== text) {
381
- die(
382
- `${SCHEMA_ARTIFACT_FILE} does not match what this ` +
383
- `package's data models would produce — regenerate ` +
384
- `it with \`package-build schema\`.`,
385
- );
386
- }
387
- console.log(`✅ ${SCHEMA_ARTIFACT_FILE} is up to date ` + `(${counts} subtypes).`);
388
- return;
389
- }
390
-
373
+ fs.mkdirSync(path.dirname(out), { recursive: true });
391
374
  fs.writeFileSync(out, text, "utf8");
392
375
  console.log(
393
376
  `✅ Wrote ${SCHEMA_ARTIFACT_FILE} for ${artifact.system} ` +
package/config.mjs CHANGED
@@ -91,7 +91,7 @@ const SECTION_KEYS = [
91
91
  export const DERIVED_MANIFEST_KEYS = Object.freeze({
92
92
  id: "`foundryPackage`, itself derived from package.json `name`",
93
93
  version: "package.json `version`",
94
- description: "package.json `description`",
94
+ description: "`packageBuild.manifest.descriptionHtml`",
95
95
  url: "package.json `repository`",
96
96
  bugs: "package.json `repository`",
97
97
  manifest: "package.json `repository` and the release tag",
@@ -152,11 +152,16 @@ const ARTIFACT_OF_KIND = Object.freeze({
152
152
  * message, so {@link loadPackageBuildConfig} — the half that knows which file
153
153
  * was read — can resolve it to a line and column. This half stays pure.
154
154
  *
155
+ * Exported so a sibling module composing a configuration value this module
156
+ * does not itself validate — {@link module:engine/site-config}'s
157
+ * `hugoConfig`, checking `site.assets` — reports through the one helper
158
+ * rather than a second copy.
159
+ *
155
160
  * @param {string} where - Dotted path of the offending key.
156
161
  * @param {string} problem - What is wrong with it.
157
162
  * @returns {never}
158
163
  */
159
- function fail(where, problem) {
164
+ export function fail(where, problem) {
160
165
  throw Object.assign(new TypeError(`package-build config: \`${where}\` ${problem}.`), {
161
166
  field: where,
162
167
  });
@@ -73,7 +73,7 @@ import { ADDRESS_SEGMENT_PATTERN, isAddressSegment } from "./engine/address-char
73
73
  import { ASSET_TYPE_NAMES } from "./engine/asset-types.mjs";
74
74
  import { isReservedPackage } from "./engine/packages.mjs";
75
75
  import { EMPTY_ICON_REGISTRY, checkIconRegistry } from "./engine/content-icons.mjs";
76
- import { MAP_TYPES, PACK_BY_TYPE } from "./engine/ids.mjs";
76
+ import { MAP_TYPES, NO_PACK, PACK_BY_TYPE } from "./engine/ids.mjs";
77
77
  import { ACTOR_TYPES } from "./engine/subtype-registry.mjs";
78
78
  import { NOTE_VOCABULARY } from "./engine/note-vocabulary.mjs";
79
79
 
@@ -256,10 +256,10 @@ export const DEFAULT_ADDRESS_SCHEME = Object.freeze({
256
256
  * and the default.
257
257
  *
258
258
  * - `homepage` — the authored homepage, and **no other page**. The content tree
259
- * is not walked for pages, `site.sections` / `site.trees` / `site.landing`
260
- * emit nothing, and nothing serves a page for its addresses.
259
+ * is not walked for pages, `site.sections` / `site.landing` emit nothing,
260
+ * and nothing serves a page for its addresses.
261
261
  * - `content` — the homepage *plus* every page the content tree publishes: the
262
- * knowledgebase, the extra trees, the section landings.
262
+ * knowledgebase and the section landings.
263
263
  *
264
264
  * **Homepage-only is a first-class mode, not an accommodation.**
265
265
  * `sohl-kethira-basic` (unofficial Hârn fan material under Keléstia Productions'
@@ -759,11 +759,10 @@ const DOCS_KEYS = ["itemFields"];
759
759
  const SITE_KEYS = [
760
760
  "base",
761
761
  "assets",
762
+ "description",
762
763
  "packages",
763
764
  "sections",
764
- "readmeSections",
765
765
  "landing",
766
- "trees",
767
766
  "pass",
768
767
  "passOptions",
769
768
  "backfillSections",
@@ -771,7 +770,6 @@ const SITE_KEYS = [
771
770
  "notfound",
772
771
  "hugo",
773
772
  ];
774
- const SITE_TREE_KEYS = ["from", "section"];
775
773
  const SITE_LIST_KEYS = ["shortcodes"];
776
774
  const SITE_NOTFOUND_KEYS = ["tagline", "sitenoun", "heroimage", "links"];
777
775
  const SITE_NOTFOUND_LINK_KEYS = ["title", "url", "text"];
@@ -1078,6 +1076,15 @@ function normalizePack(value, where, nested = false) {
1078
1076
  rejectUnknownKeys(pack, PACK_KEYS, `${where}.`);
1079
1077
 
1080
1078
  const name = requireNonEmptyString(pack.name, `${where}.name`);
1079
+ // A note writes `pack: none` to compile into no document, so a pack of
1080
+ // that name could never be addressed by one.
1081
+ if (name === NO_PACK) {
1082
+ fail(
1083
+ `${where}.name`,
1084
+ `may not be \`${NO_PACK}\` — a note declares \`pack: ${NO_PACK}\` to ` +
1085
+ `compile into no document, so no pack may answer to the name`,
1086
+ );
1087
+ }
1081
1088
  const type = pack.type;
1082
1089
  if (
1083
1090
  typeof type !== "string" ||
@@ -1575,6 +1582,33 @@ function normalizeSiteAssets(value) {
1575
1582
  return assets.replace(/\/+$/, "");
1576
1583
  }
1577
1584
 
1585
+ /**
1586
+ * The site's `<meta name="description">`, plain text.
1587
+ *
1588
+ * Foundry's package browser wants a pitch — HTML, any length,
1589
+ * `packageBuild.manifest.descriptionHtml` — and a `<meta>` tag wants one
1590
+ * plain sentence. The two audiences are different enough that one string
1591
+ * cannot serve both, so this is the site's own, checked for markup rather
1592
+ * than trusted to carry none: a value with a `<` in it is refused, naming
1593
+ * `descriptionHtml` as where markup belongs.
1594
+ *
1595
+ * @param {unknown} value - The configured value, or `undefined`.
1596
+ * @returns {string} The description; `""` when unset.
1597
+ */
1598
+ function normalizeSiteDescription(value) {
1599
+ if (value === undefined) return "";
1600
+ const description = requireNonEmptyString(value, "site.description");
1601
+ if (description.includes("<")) {
1602
+ fail(
1603
+ "site.description",
1604
+ "contains `<` — this is plain text for the site's " +
1605
+ '`<meta name="description">`; markup belongs in ' +
1606
+ "`packageBuild.manifest.descriptionHtml`",
1607
+ );
1608
+ }
1609
+ return description;
1610
+ }
1611
+
1578
1612
  /**
1579
1613
  * A map of section name → landing metadata.
1580
1614
  *
@@ -1616,7 +1650,7 @@ export const DERIVED_HUGO_KEYS = Object.freeze({
1616
1650
  disableKinds: "whether any note in the tree carries `tags:`, which the site walk discovers",
1617
1651
  taxonomies: "whether any note in the tree carries `tags:`, which the site walk discovers",
1618
1652
  outputs: "whether any note in the tree carries `tags:`, which the site walk discovers",
1619
- "params.description": "package.json `description`",
1653
+ "params.description": "`site.description`",
1620
1654
  "params.author": "package.json `author`",
1621
1655
  "params.cdnBaseURL": "`site.assets`",
1622
1656
  "params.brand": "the organisation's brand links, in `engine/site-config.mjs`",
@@ -1722,15 +1756,22 @@ function normalizeSiteHugo(value) {
1722
1756
  /**
1723
1757
  * The `site` section — how this repository frames the website it publishes.
1724
1758
  *
1725
- * Everything here is *framing*: what a section is called, which extra trees
1726
- * are published beside the content, which named pass bundle supplies the
1727
- * repository's own body rewrites, and the residue of the generated Hugo
1728
- * configuration that is genuinely this repository's own. Where the Hugo tree
1729
- * is written is not a choice: `content-build site` writes it under
1730
- * `build/hugo/`, and a `site.out` is refused by name. How a page gets its
1731
- * **address** is deliberately not here either that is `publish.address`,
1732
- * shared with the link manifest so the two cannot disagree about where a
1733
- * page is.
1759
+ * Everything here is *framing*: what a section is called, which named pass
1760
+ * bundle supplies the repository's own body rewrites, and the residue of the
1761
+ * generated Hugo configuration that is genuinely this repository's own. Where
1762
+ * the Hugo tree is written is not a choice: `content-build site` writes it
1763
+ * under `build/hugo/`, and a `site.out` is refused by name. How a page gets
1764
+ * its **address** is deliberately not here either that is
1765
+ * `publish.address`, shared with the link manifest so the two cannot disagree
1766
+ * about where a page is.
1767
+ *
1768
+ * **What the site publishes is the content tree, and nothing beside it.** A
1769
+ * page of documentation is a note — `type: doc`, addressed by its shortcode,
1770
+ * compiling into no document where it says `pack: none` — so there is no
1771
+ * second mechanism for mounting a directory of markdown, and a configuration
1772
+ * that names one (`site.trees`, and the `site.readmeSections` that titled
1773
+ * such a tree's landing) is refused with a message saying where the page
1774
+ * goes instead.
1734
1775
  *
1735
1776
  * @param {unknown} value - The `site` block, or `undefined`.
1736
1777
  * @returns {Readonly<object>} It, frozen, with every default filled.
@@ -1739,11 +1780,10 @@ function normalizeSite(value) {
1739
1780
  const empty = Object.freeze({
1740
1781
  base: "",
1741
1782
  assets: "",
1783
+ description: "",
1742
1784
  packages: Object.freeze([]),
1743
1785
  sections: Object.freeze({}),
1744
- readmeSections: Object.freeze({}),
1745
1786
  landing: null,
1746
- trees: Object.freeze([]),
1747
1787
  pass: "",
1748
1788
  passOptions: Object.freeze({}),
1749
1789
  backfillSections: false,
@@ -1764,27 +1804,20 @@ function normalizeSite(value) {
1764
1804
  "the location is not configurable. Remove the key",
1765
1805
  );
1766
1806
  }
1767
- rejectUnknownKeys(input, SITE_KEYS, "site.");
1768
-
1769
- const trees = [];
1770
- if (input.trees !== undefined) {
1771
- if (!Array.isArray(input.trees)) fail("site.trees", "must be a list");
1772
- input.trees.forEach((entry, i) => {
1773
- const where = `site.trees[${i}]`;
1774
- if (!isPlainObject(entry)) fail(where, "must be a mapping");
1775
- const tree = /** @type {Record<string, unknown>} */ (entry);
1776
- rejectUnknownKeys(tree, SITE_TREE_KEYS, `${where}.`);
1777
- trees.push(
1778
- Object.freeze({
1779
- from: requireNonEmptyString(tree.from, `${where}.from`),
1780
- section: requireNonEmptyString(tree.section, `${where}.section`),
1781
- // The tree's own path, POSIX-separated — what a
1782
- // repository-relative link inside it is resolved against.
1783
- rel: String(tree.from).split(path.sep).join("/"),
1784
- }),
1785
- );
1786
- });
1807
+ // Refused by name too, and ahead of the vocabulary check for the same
1808
+ // reason: a page is a note in the content tree, and the useful thing to
1809
+ // say is where it goes rather than that the key is unknown.
1810
+ for (const key of ["trees", "readmeSections"]) {
1811
+ if (input[key] === undefined) continue;
1812
+ fail(
1813
+ `site.${key}`,
1814
+ "is retired a page is a note in the content tree. Give each page " +
1815
+ "`type: doc`, a `shortcode` and `pack: none`, file it under " +
1816
+ "`assets/content/`, and declare the section that lists it under " +
1817
+ "`site.sections`",
1818
+ );
1787
1819
  }
1820
+ rejectUnknownKeys(input, SITE_KEYS, "site.");
1788
1821
 
1789
1822
  let packages = [];
1790
1823
  if (input.packages !== undefined) {
@@ -1807,11 +1840,10 @@ function normalizeSite(value) {
1807
1840
  return Object.freeze({
1808
1841
  base: input.base === undefined ? "" : requireNonEmptyString(input.base, "site.base"),
1809
1842
  assets: normalizeSiteAssets(input.assets),
1843
+ description: normalizeSiteDescription(input.description),
1810
1844
  packages: Object.freeze(packages),
1811
1845
  sections: normalizeSectionMap(input.sections, "site.sections"),
1812
- readmeSections: normalizeSectionMap(input.readmeSections, "site.readmeSections"),
1813
1846
  landing,
1814
- trees: Object.freeze(trees),
1815
1847
  pass: input.pass === undefined ? "" : requireNonEmptyString(input.pass, "site.pass"),
1816
1848
  passOptions:
1817
1849
  input.passOptions === undefined ?
package/docs/api.md CHANGED
@@ -103,6 +103,7 @@ Deterministic document ids, derived by hashing rather than stored, so compile pa
103
103
  | `MAP_TYPES` | `const MAP_TYPES` | — | enumerating the content types that compile into a Foundry `Scene` |
104
104
  | `MAP_SUBTYPES` | `const MAP_SUBTYPES` | — | reading the map subTypes, which differ only in derived canvas defaults |
105
105
  | `JOURNAL_TYPES` | `const JOURNAL_TYPES` | — | enumerating content types whose whole document _is_ a JournalEntry |
106
+ | `NO_PACK` | `const NO_PACK` | — | spelling the `pack:` value (`none`) that routes a note's document into no compendium |
106
107
  | `PACK_BY_TYPE` | `const PACK_BY_TYPE` | — | looking up the conventional pack name and document type for a content type |
107
108
  | `RETIRED_TYPES` | `const RETIRED_TYPES` | — | looking up what a retired content type was replaced by |
108
109
  | `assertTypeNotRetired` | `assertTypeNotRetired(type, where)` | throws | refusing a note whose type has been retired outright |
@@ -200,6 +201,8 @@ Which pack a note's document lands in, when a document type has more than one co
200
201
  | ------------------ | -------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------- |
201
202
  | `PackRoutingError` | `class PackRoutingError extends Error` | — | catching a note that cannot be routed to any configured pack |
202
203
  | `PACK_FIELD` | `const PACK_FIELD` | — | naming the frontmatter field a note declares its pack in |
204
+ | `NO_PACK` | `const NO_PACK` | — | spelling the `pack:` value (`none`) that routes a note's document into no compendium |
205
+ | `declaresNoPack` | `declaresNoPack(fm, system)` | `boolean` | asking, without a router, whether a note declares `pack: none` for one system's document |
203
206
  | `createPackRouter` | `createPackRouter(packs)` | `{resolve, resolveOrNull}` | building a router for one configured pack list, purely, for testing without a config file on disk |
204
207
  | `routerFor` | `routerFor(config)` | the router | getting the router for a resolved configuration, built once per configuration |
205
208
  | `packRouter` | `packRouter()` | the router | asking, as every module that emits a UUID does, where a note's document lives |
@@ -605,25 +608,23 @@ The toolchain's own content index — the files it ships, addressed. Every other
605
608
 
606
609
  Publishing a content tree as a website. Compiling a content tree into compendium packs is `content-build package compile`. Publishing the _same tree_ as a website was a script each consumer wrote for itself — 473 code lines in `sohl` and 462 in `sohl-thalorna`, 87 of them identical — and the copies drifted in ways neither repository could see. `sohl-thalorna` reimplemented four things this package already exported, not because it needed different behaviour but because its script predates the extraction. That is the failure a command removes: a consumer cannot accidentally reimplement one.
607
610
 
608
- | Export | Signature | Returns | Use it when |
609
- | ---------------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
610
- | `walkSiteTree` | `function walkSiteTree(dir, skip` | {string[]} Absolute paths. | Every `.md` file under `dir`, depth-first in directory order. |
611
- | `collectContentPages` | `function collectContentPages(contentBase, ctx)` | {{pages: object[], addressFindings: object[], fmLinkFindings: object[]}} | The content tree's pages, and what could not be addressed. |
612
- | `collectTreePages` | `function collectTreePages(tree, ctx)` | {{pages: object[], fmLinkFindings: object[]}} | An extra tree's pages a documentation tree published alongside the content. |
613
- | `collectHomepages` | `function collectHomepages(contentBase, ctx)` | {{pages: object[], addressFindings: object[]}} The homepage notes, in walk order, and the ones among them that could not be addressed. | The package's homepage notes the authored page at `/<contentPackage>/`. |
614
- | `writeHomepages` | `function writeHomepages(outRoot, pages, config)` | {number} How many pages were written. | Writes each homepage at its address, below the package's own root. |
615
- | `siteGates` | `function siteGates(pages, findings,` | {object} The gate results and, when they pass, the built index. | The integrity gates a site build runs before it writes anything. |
616
- | `emptyGates` | `function emptyGates()` | {object} An all-clear gate result. | The gate result of a build that ran none of them. |
617
- | `gatesFailed` | `function gatesFailed(gates)` | | Whether any gate produced a finding. |
618
- | `tableUniverse` | `function tableUniverse(pages)` | {Map<string, object[]>} Package the notes it may tabulate. | The universe a generated table searches, grouped by package. |
619
- | `sectionFrontmatter` | `function sectionFrontmatter(meta)` | {object} Its front matter, `title` first. | The front matter a section's landing states about itself. |
620
- | `pageFrontmatter` | `function pageFrontmatter(page,` | {object} The frontmatter to write. | The frontmatter a page publishes with. |
621
- | `pageDestination` | `function pageDestination(page)` | — | Where a page is written, relative to the output root. |
622
- | `renderPages` | `function renderPages(pages, options)` | {{written: number, byKind: Record<string, number>, tableErrors: object[], wikiErrors: object[]}} | Renders and writes every page. |
623
- | `writeSectionLandings` | `function writeSectionLandings(outRoot,` | {number} How many landings were written. | Writes the Hugo sections a published tree declares. |
624
- | `pluralTitle` | `function pluralTitle(name)` | {string} The display title. | A section landing's title, from its directory name `macro` `Macros`. |
625
- | `resolveSitePass` | `function resolveSitePass(name, options)` | {{beforeLinks?: Function, afterLinks?: Function}} The bundle. | Resolves `site.pass` to its bundle. |
626
- | `buildSite` | `buildSite({ config, sqlTables })` | {{gates: object, stats: object\|null, tableErrors: object[], wikiErrors: object[], manifests: object\|null}} | Builds a Hugo content tree from a content tree, and reports what it found. |
611
+ | Export | Signature | Returns | Use it when |
612
+ | ---------------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
613
+ | `collectContentPages` | `function collectContentPages(contentBase, ctx)` | {{pages: object[], addressFindings: object[], fmLinkFindings: object[]}} | The content tree's pages, and what could not be addressed. |
614
+ | `collectHomepages` | `function collectHomepages(contentBase, ctx)` | {{pages: object[], addressFindings: object[]}} The homepage notes, in walk order, and the ones among them that could not be addressed. | The package's homepage notes — the authored page at `/<contentPackage>/`. |
615
+ | `writeHomepages` | `function writeHomepages(outRoot, pages, config)` | {number} How many pages were written. | Writes each homepage at its address, below the package's own root. |
616
+ | `siteGates` | `function siteGates(pages, findings,` | {object} The gate results and, when they pass, the built index. | The integrity gates a site build runs before it writes anything. |
617
+ | `emptyGates` | `function emptyGates()` | {object} An all-clear gate result. | The gate result of a build that ran none of them. |
618
+ | `gatesFailed` | `function gatesFailed(gates)` | | Whether any gate produced a finding. |
619
+ | `tableUniverse` | `function tableUniverse(pages)` | {Map<string, object[]>} Package the notes it may tabulate. | The universe a generated table searches, grouped by package. |
620
+ | `sectionFrontmatter` | `function sectionFrontmatter(meta)` | {object} Its front matter, `title` first. | The front matter a section's landing states about itself. |
621
+ | `pageFrontmatter` | `function pageFrontmatter(page,` | {object} The frontmatter to write. | The frontmatter a page publishes with. |
622
+ | `pageDestination` | `function pageDestination(page)` | — | Where a page is written, relative to the output root. |
623
+ | `renderPages` | `function renderPages(pages, options)` | {{written: number, byKind: Record<string, number>, tableErrors: object[], wikiErrors: object[]}} | Renders and writes every page. |
624
+ | `writeSectionLandings` | `function writeSectionLandings(outRoot,` | {number} How many landings were written. | Writes the Hugo sections a published tree declares. |
625
+ | `pluralTitle` | `function pluralTitle(name)` | {string} The display title. | A section landing's title, from its directory name `macro` → `Macros`. |
626
+ | `resolveSitePass` | `function resolveSitePass(name, options)` | {{beforeLinks?: Function}} The bundle. | Resolves `site.pass` to its bundle. |
627
+ | `buildSite` | `buildSite({ config, sqlTables })` | {{gates: object, stats: object\|null, tableErrors: object[], wikiErrors: object[], manifests: object\|null}} | Builds a Hugo content tree from a content tree, and reports what it found. |
627
628
 
628
629
  ### `engine.contentLint`
629
630
 
@@ -1138,13 +1139,12 @@ Which of SoHL's facts a note's summary panel carries, and how they group. Read o
1138
1139
 
1139
1140
  ### `sohl.kbPasses`
1140
1141
 
1141
- The `sohl` knowledgebase's own body passes: two rewrites driven by a TypeDoc symbol map and a repository layout only this package has, named from `site.passOptions` the same way an asset transform is named from configuration. Neither rewrite ever fails a build — an unresolved `{@link}` degrades to a code span, and a relative link outside the documentation tree becomes a GitHub blob URL — but building the bundle from a misconfigured `symbolMap` fails loudly before any page renders.
1142
+ The `sohl` knowledgebase's own body pass: a rewrite driven by a TypeDoc symbol map only this package has, named from `site.passOptions` the same way an asset transform is named from configuration. The rewrite never fails a build — an unresolved `{@link}` degrades to a code span — but building the bundle from a misconfigured `symbolMap` fails loudly before any page renders.
1142
1143
 
1143
- | Export | Signature | Returns | Use it when |
1144
- | ------------------ | ----------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1145
- | `resolveApiLinks` | `resolveApiLinks(body, symbols, apiBase)` | `string` — the body with every tag resolved | resolving inline TypeDoc `{@link}` / `{@linkcode}` / `{@linkplain}` tags in a markdown body against the API symbol map |
1146
- | `rewriteRepoLinks` | `rewriteRepoLinks(body, docRel, options)` | `string` the body with every relative link rewritten | rewriting a developer doc's repository-relative links so they resolve on the published site (documentation-tree links become routes, everything else becomes a GitHub blob URL) |
1147
- | `sohlKbPass` | `sohlKbPass(options)` | `{beforeLinks: Function, afterLinks: Function}` | building the `sohl` knowledgebase pass bundle the site renderer calls around wikilink resolution |
1144
+ | Export | Signature | Returns | Use it when |
1145
+ | ----------------- | ----------------------------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
1146
+ | `resolveApiLinks` | `resolveApiLinks(body, symbols, apiBase)` | `string` — the body with every tag resolved | resolving inline TypeDoc `{@link}` / `{@linkcode}` / `{@linkplain}` tags in a markdown body against the API symbol map |
1147
+ | `sohlKbPass` | `sohlKbPass(options)` | `{beforeLinks: Function}` | building the `sohl` knowledgebase pass bundle the site renderer calls before wikilink resolution |
1148
1148
 
1149
1149
  ### Flat exports (not under a namespace)
1150
1150
 
@@ -1290,6 +1290,7 @@ console.log(config.stageDir);
1290
1290
  | `resolvePackageBuildConfig` | `resolvePackageBuildConfig(shared)` | `Readonly<PackageBuildConfig>` | validating the `packageBuild:` section from an already-loaded shared configuration — the pure half, usable without touching disk |
1291
1291
  | `loadPackageBuildConfig` | `loadPackageBuildConfig()` | `Readonly<PackageBuildConfig>` | reading and validating the repository's resolved package-build configuration from disk, read fresh on each call rather than cached at import |
1292
1292
  | `checkHomepage` | `checkHomepage(homepage, contentPackage)` | `void` | validating a resolved `homepage` against `contentPackage` — called by `content-build site` before the generated `baseURL` is written, not by `resolvePackageBuildConfig` itself |
1293
+ | `fail` | `fail(where, problem)` | `never` | rejecting a configured value from a sibling module, naming the dotted key path it was written under, so the loader can resolve it to a line and column |
1293
1294
 
1294
1295
  ## `./prettier`
1295
1296