@heroiclands/package-build 25.6.3 → 25.7.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 (55) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +19 -1
  3. package/bin/content-commands.mjs +14 -70
  4. package/bin/package-build.mjs +25 -10
  5. package/ci/wip-workflows.test.mjs +252 -0
  6. package/content-config.mjs +40 -12
  7. package/docs/api.md +237 -48
  8. package/docs/commands.md +73 -36
  9. package/docs/configuration.md +92 -48
  10. package/docs/guides/release-and-deploy.md +26 -14
  11. package/docs/guides/site.md +29 -6
  12. package/engine/cdn-publish.mjs +164 -6
  13. package/engine/content-index.mjs +71 -19
  14. package/engine/content-spans.mjs +18 -1
  15. package/engine/content-styles.mjs +85 -0
  16. package/engine/derived-sections.mjs +4 -3
  17. package/engine/event-views.mjs +6 -4
  18. package/engine/helpers.mjs +33 -9
  19. package/engine/index.mjs +1 -0
  20. package/engine/note-cache.mjs +1107 -0
  21. package/engine/note-page.mjs +424 -0
  22. package/engine/pack-config.mjs +47 -4
  23. package/engine/pathnames.mjs +22 -7
  24. package/engine/prepared-only.mjs +32 -0
  25. package/engine/preview-index.mjs +55 -13
  26. package/engine/site-build.mjs +34 -448
  27. package/engine/site-config.mjs +33 -4
  28. package/engine/site-maps.mjs +16 -24
  29. package/engine/site-origins.mjs +23 -0
  30. package/engine/site-page.mjs +648 -0
  31. package/engine/site-render.mjs +142 -10
  32. package/engine/site-serve.mjs +1 -1
  33. package/engine/sql-tables.mjs +12 -3
  34. package/package.json +6 -1
  35. package/site-theme/static/css/style.css +14 -0
  36. package/types/content-config.d.mts +24 -4
  37. package/types/engine/cdn-publish.d.mts +22 -0
  38. package/types/engine/content-index.d.mts +36 -2
  39. package/types/engine/content-styles.d.mts +20 -0
  40. package/types/engine/derived-sections.d.mts +3 -3
  41. package/types/engine/event-views.d.mts +4 -3
  42. package/types/engine/helpers.d.mts +18 -1
  43. package/types/engine/index.d.mts +1 -0
  44. package/types/engine/note-cache.d.mts +149 -0
  45. package/types/engine/note-page.d.mts +92 -0
  46. package/types/engine/pack-config.d.mts +30 -0
  47. package/types/engine/prepared-only.d.mts +14 -0
  48. package/types/engine/preview-index.d.mts +35 -0
  49. package/types/engine/site-build.d.mts +5 -100
  50. package/types/engine/site-config.d.mts +3 -2
  51. package/types/engine/site-maps.d.mts +7 -11
  52. package/types/engine/site-origins.d.mts +9 -0
  53. package/types/engine/site-page.d.mts +261 -0
  54. package/types/engine/site-render.d.mts +20 -1
  55. package/types/engine/sql-tables.d.mts +8 -4
package/docs/api.md CHANGED
@@ -193,16 +193,18 @@ Whether a content tree has anything to compile. The pack build's worst failure i
193
193
 
194
194
  The consuming repository's resolved `package-build.config.yaml`: the content package, the Foundry package and its kind, every path, the `_stats` identity, item-type membership and the pack list — read on call rather than hoisted at import, so importing this package never requires a configuration to exist.
195
195
 
196
- | Export | Signature | Returns | Use it when |
197
- | ------------------- | ------------------------------------ | ----------------------------- | ------------------------------------------------------------------------------------ |
198
- | `CONFIG_BASENAME` | `const CONFIG_BASENAME` | — | naming the stem every consuming repository declares its build under |
199
- | `CONFIG_FILENAMES` | `const CONFIG_FILENAMES` | — | listing the file names a configuration may be written as |
200
- | `findConfigFile` | `findConfigFile(from)` | `string \| undefined` | finding the nearest configuration file at or above a directory |
201
- | `resolveConfigFile` | `resolveConfigFile(...)` | `{path, fromCwd, fromModule}` | asking which configuration file a build launched here would read, without loading it |
202
- | `locateConfigError` | `locateConfigError(err, configPath)` | the decorated error | attaching the position of the key a configuration error names |
203
- | `configFromData` | `configFromData(data, configPath)` | `ContentBuildConfig` | turning a parsed YAML configuration into the frozen one the engine reads |
204
- | `loadPackConfig` | `loadPackConfig()` | `ContentBuildConfig` | reading the consuming repository's resolved, frozen configuration |
205
- | `packConfigPath` | `packConfigPath()` | `string` | naming the file `loadPackConfig` resolved the configuration from, for a diagnostic |
196
+ | Export | Signature | Returns | Use it when |
197
+ | ------------------- | ------------------------------------ | ----------------------------- | ------------------------------------------------------------------------------------------------ |
198
+ | `CONFIG_BASENAME` | `const CONFIG_BASENAME` | — | naming the stem every consuming repository declares its build under |
199
+ | `CONFIG_FILENAMES` | `const CONFIG_FILENAMES` | — | listing the file names a configuration may be written as |
200
+ | `findConfigFile` | `findConfigFile(from)` | `string \| undefined` | finding the nearest configuration file at or above a directory |
201
+ | `resolveConfigFile` | `resolveConfigFile(...)` | `{path, fromCwd, fromModule}` | asking which configuration file a build launched here would read, without loading it |
202
+ | `locateConfigError` | `locateConfigError(err, configPath)` | the decorated error | attaching the position of the key a configuration error names |
203
+ | `configFromData` | `configFromData(data, configPath)` | `ContentBuildConfig` | turning a parsed YAML configuration into the frozen one the engine reads |
204
+ | `loadPackConfig` | `loadPackConfig()` | `ContentBuildConfig` | reading the consuming repository's resolved, frozen configuration |
205
+ | `packConfigPath` | `packConfigPath()` | `string` | naming the file `loadPackConfig` resolved the configuration from, for a diagnostic |
206
+ | `readPackConfig` | `readPackConfig(configPath)` | `ContentBuildConfig` | reading the configuration one named file declares, unmemoised |
207
+ | `withPackConfig` | `withPackConfig(config, fn, file?)` | what `fn` returns | running work, asynchronous included, with `config` as the configuration `loadPackConfig` answers |
206
208
 
207
209
  ### `engine.packRouter`
208
210
 
@@ -621,12 +623,11 @@ The npm-installed Graphviz runtime draws SVG maps. The `.dot` source stays besid
621
623
 
622
624
  The map from each place, drawn for the site: `package-build site build` draws the map from every place of this package given a **From here** section — with the site's base, so every name links through it — and sets each drawing inline in that section, where its place names stay links. `site.maps: false` gives no place the section, and nothing is drawn.
623
625
 
624
- | Export | Signature | Returns | Use it when |
625
- | --------------------- | ------------------------------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------- |
626
- | `SITE_MAP_DIR` | `const SITE_MAP_DIR` | `string` | where the site build draws below a package's root, `build/map/site` |
627
- | `inlineSvg` | `inlineSvg(svg)` | `string` | rewriting a GraphViz SVG for inlining — no prologue, no comment, no `xlink:`, the root sized by `viewBox` alone |
628
- | `drawSiteMaps` | `drawSiteMaps({ records, foreignIndex, config, base })` | `{maps, findings}` | drawing the map from every place given a **From here** section, keyed by the file name the section names |
629
- | `inlineDrawingFigure` | `inlineDrawingFigure(svg, alt)` | `string` | the one HTML block the website sets an inline drawing in, with no blank line to end it early |
626
+ | Export | Signature | Returns | Use it when |
627
+ | -------------- | ----------------------------------------------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------- |
628
+ | `SITE_MAP_DIR` | `const SITE_MAP_DIR` | `string` | where the site build draws below a package's root, `build/map/site` |
629
+ | `inlineSvg` | `inlineSvg(svg)` | `string` | rewriting a GraphViz SVG for inlining — no prologue, no comment, no `xlink:`, the root sized by `viewBox` alone |
630
+ | `drawSiteMaps` | `drawSiteMaps({ records, foreignIndex, config, base, only?, outDir? })` | `{maps, findings}` | drawing the map from every place given a **From here** section, keyed by the file name the section names |
630
631
 
631
632
  ### `engine.systems`
632
633
 
@@ -1277,6 +1278,36 @@ The deployed layout of a package's pictures, and which of them ship. A Foundry p
1277
1278
  | `stagePackageAssets` | `stagePackageAssets({ assetsBase, contentPackage, stageDir, title, transform, shippingLicenses })` | `{staged, excluded, findings, manifest}` | staging a package's shipped pictures flat and writing `asset-sets/default.json`; nothing is staged when `findings` is non-empty |
1278
1279
  | `rowsStagingPictureRoots` | `rowsStagingPictureRoots(rows, { rootDir, assetsBase })` | `{index, from, root}[]` | finding `packageBuild.assets` rows that would copy a picture root around the filter |
1279
1280
 
1281
+ ### CDN publication transports
1282
+
1283
+ Import `@heroiclands/package-build/engine/cdn-publish` directly. `planCdnPublish`
1284
+ collects the same asset records and shipping selection as staging, applies the
1285
+ provided transform, and builds destination-relative keys plus a named set whose
1286
+ sources use its `host` argument. It does not upload anything.
1287
+
1288
+ `publishToCdn(plan, { store, dryRun, log })` accepts a destination-independent
1289
+ store with async `size(key)`, `read(key)`, optional `write(key, body, contentType)`
1290
+ and a safe `describe` string. It validates every planned relative key before
1291
+ reading or writing, skips pictures by size, compares set bytes, and withholds the
1292
+ set after a picture failure. `dryRun` never calls `write`.
1293
+
1294
+ `internalStore({ destination, fetch, transfer })` combines public HTTPS reads at
1295
+ `INTERNAL_CDN_HOST` with rsync writes, defaulting to `INTERNAL_CDN_DESTINATION`
1296
+ (`atlas:/srv/preview/cdn`). An explicit SSH alias may target `alias:cdn` for the
1297
+ forced-rrsync receiver. Writes stage only the supplied bytes in temporary regular
1298
+ files, invoke rsync without deletion, and clean up after success or failure.
1299
+ `fetch` and `transfer(args)` can be supplied for isolated tests; the default
1300
+ transfer executes rsync with batch SSH and strict host-key checking.
1301
+
1302
+ `s3Store(credentials)` retains signed S3/R2 requests. `publicStore(host, fetch)`
1303
+ provides read-only HTTP comparisons. `CANONICAL_CDN_ORIGIN`, exported from
1304
+ `engine/site-config`, names the owned public CDN origin used by pathname
1305
+ projection. Its single definition lives in the leaf `engine/site-origins` module
1306
+ to avoid a cycle through site configuration and content rendering.
1307
+ The CLI owns environment loading and
1308
+ `--dest` selection; site builds never call a publisher. See
1309
+ [CDN publication](commands.md#package-build-cdn-publish) for operator access.
1310
+
1280
1311
  ### `engine.assetReferences`
1281
1312
 
1282
1313
  Every asset reference in a compiled document states the asset's address: an `<img>` carries `data-asset`, and a path field's address sits in `flags.<system>.assets` on the document holding it, nested to mirror the field's path. A reference to a picture its package does not ship compiles to that package's stand-in and keeps the excluded address.
@@ -1357,6 +1388,16 @@ configuration and incremental render state stay in the calling thread. Every
1357
1388
  update is prepared in a separate output tree, then installed between HTTP
1358
1389
  requests so readers receive complete files.
1359
1390
 
1391
+ `buildPackageSite` invokes configured `site.inputs` before content work. The
1392
+ module's default export has the `SiteInputsProvider` contract:
1393
+ `({config}) => {navigation, files}` or a promise of that object. It receives the
1394
+ immutable effective configuration; `navigation` uses `NavigationEntry[]`, and
1395
+ `files` is `Record<string, string>` of relative output paths to UTF-8 text.
1396
+ Provider files are validated against unsafe paths, symlinks and builder-owned
1397
+ output before output replacement, written after the renderer's clearing step,
1398
+ and recorded in `written.files`. Without a provider, cached navigation remains
1399
+ required. See [local site inputs](configuration.md#local-site-inputs).
1400
+
1360
1401
  ### `engine.contentRender`
1361
1402
 
1362
1403
  `createRenderer(target, options?)` builds the one markdown-it configuration every target renders through. `target` is `"foundry"`, `"web"`, `"preview"` or `"book"`. The renderer holds the configured parser (`md`), the names of the plugins installed (`plugins`), and three functions: `prepare` writes the constructs markdown-it has no syntax for — marked text, captioned items, named blocks, verse and alerts — as HTML between blank lines; `render` prepares and renders to HTML; `renderInline` renders one inline run. The `book` target parses only, and its Typst stage reads the tokens.
@@ -1407,9 +1448,17 @@ locate it in the complete source. LF and CRLF retain their authored lengths.
1407
1448
 
1408
1449
  `previewIndex({ config, records, foreign? })` builds what [`renderPreview`](#enginesitepreview) takes as `index`: the link index, the asset address space, the languages' maps, the icon registry and an `expand` function that resolves a buffer's links, embeds, expressions and figure numbers as the website does. `records` are the content index's records and `foreign` the dependencies' indexes. Build it once per index and reuse it for every render.
1409
1450
 
1410
- | Export | Signature | Returns | Use it when |
1411
- | -------------- | -------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------ |
1412
- | `previewIndex` | `previewIndex({ config, records, foreign })` | `{ runs, icons, lookupAsset, expand }` | An editor holds a content index and wants the preview to render with it. |
1451
+ | Export | Signature | Returns | Use it when |
1452
+ | ------------------- | ------------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------- |
1453
+ | `previewIndex` | `previewIndex({ config, records, foreign })` | `{ runs, icons, lookupAsset, expand }` | An editor holds a content index and wants the preview to render with it. |
1454
+ | `recordSiteContext` | `recordSiteContext({ config, records, foreign })` | `{ site, assets, events, runs }` | Resolving a page against content-index records instead of a content tree. |
1455
+
1456
+ `recordSiteContext` places every note that publishes a page at the URL the
1457
+ site gives it: the homepage at the package root, a note `site.sections` names
1458
+ at its section, and every other note at its address. `anchorsOf(record)` may
1459
+ answer a note's anchors as its page emits them, generated sections included;
1460
+ answering nothing keeps the record's own. `pages` maps each page's URL to its
1461
+ record.
1413
1462
 
1414
1463
  ### `engine.sitePreview`
1415
1464
 
@@ -1589,16 +1638,16 @@ A list item opened with `[ ]` or `[x]`, reported. The format has no checkbox: a
1589
1638
 
1590
1639
  One authored pathname, and the four addresses it resolves to. A note names a file once — in `img:`, in `data.portrait:`, in the body of a markdown image — and the first segment says which package owns it when an `assets/` follows. Every surface derives its own address from that one statement: the path inside a Foundry install, the file in the owning repository's tree, the address the website serves, and where the book stages its copy.
1591
1640
 
1592
- | Export | Signature | Returns | Use it when |
1593
- | ----------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
1594
- | `ASSETS_SEGMENT` | `const ASSETS_SEGMENT` | — | The directory a package ships its files in. |
1595
- | `PATHNAME_SURFACES` | `const PATHNAME_SURFACES` | — | The surfaces one authored pathname resolves for. |
1596
- | `pathnameProblem` | `function pathnameProblem(raw)` | {string} The problem, as a finding's sentence, or `""`. | What is wrong with an authored pathname, or `""` when nothing is. |
1597
- | `packageAddresses` | `function packageAddresses(config)` | {Map<string, {root: string\|null, id: string\|null, own: boolean}>} The packages, by package name. | Every content package this build can resolve a pathname against. |
1598
- | `resolvePathname` | `function resolvePathname(raw, config)` | {PathnameForms\|null} The four forms, or `null` when the note names no file. | Resolve one authored pathname into the address each surface serves. |
1599
- | `assetPathnameKey` | `function assetPathnameKey(src, config)` | {string\|null} The pathname, or `null` for an address naming no asset record to look up. | The pathname one authored image address keys an asset's role and pixel size under, whichever form it was written in. |
1600
- | `servesFoundry` | `function servesFoundry(config)` | {boolean} Whether the package being built has a Foundry root. | Whether this build installs anything into a Foundry data directory. |
1601
- | `foundryAddressProblem` | `function foundryAddressProblem(raw, config)` | {string} The problem, as a finding's sentence, or `""`. | Why a pathname has no Foundry address, or `""` when it has one. |
1641
+ | Export | Signature | Returns | Use it when |
1642
+ | ----------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
1643
+ | `ASSETS_SEGMENT` | `const ASSETS_SEGMENT` | — | The directory a package ships its files in. |
1644
+ | `PATHNAME_SURFACES` | `const PATHNAME_SURFACES` | — | The surfaces one authored pathname resolves for. |
1645
+ | `pathnameProblem` | `function pathnameProblem(raw)` | {string} The problem, as a finding's sentence, or `""`. | What is wrong with an authored pathname, or `""` when nothing is. |
1646
+ | `packageAddresses` | `function packageAddresses(config)` | {Map<string, {root: string\|null, id: string\|null, own: boolean}>} The packages, by package name. | Every content package this build can resolve a pathname against. |
1647
+ | `resolvePathname` | `function resolvePathname(raw, config)` | {PathnameForms\|null} The four forms, or `null` when the note names no file. | Resolve one authored pathname into the address each surface serves; canonical Heroic Lands CDN assets use effective site.assets in the web form only. |
1648
+ | `assetPathnameKey` | `function assetPathnameKey(src, config)` | {string\|null} The pathname, or `null` for an address naming no asset record to look up. | The pathname one authored image address keys an asset's role and pixel size under, whichever form it was written in. |
1649
+ | `servesFoundry` | `function servesFoundry(config)` | {boolean} Whether the package being built has a Foundry root. | Whether this build installs anything into a Foundry data directory. |
1650
+ | `foundryAddressProblem` | `function foundryAddressProblem(raw, config)` | {string} The problem, as a finding's sentence, or `""`. | Why a pathname has no Foundry address, or `""` when it has one. |
1602
1651
 
1603
1652
  ### `engine.contentLinks`
1604
1653
 
@@ -1654,7 +1703,8 @@ Shared helpers for the pack compilers in `packages/package-build/`. The HeroicLa
1654
1703
  | `convertNoteWikilinks` | `convertNoteWikilinks(body, { type, id, pack, docPack, index, name, file, bodyLine, bodyColumn, lineMap })` | `{{markdown: string, unresolved: Array<object>}}` | Converts the wikilinks in one note's markdown, reporting any that have no target in the content tree. |
1655
1704
  | `expandNoteTables` | `function expandNoteTables(body,` | `{{markdown: string, lineMap: Array<{line: number, generated: boolean}>}}` The body with every table expanded, and where each emitted line came from — which is what lets a diagnostic about the expanded body name an authored position. | Expand prepared SQL tables in one note's markdown, before wikilinks are resolved — so a generated cell may itself be a wikilink. |
1656
1705
  | `folderFilename` | `function folderFilename(name, id)` | — | Builds a compendium-source filename for a folder JSON document: `folder_Name_id.json` with non-alphanumeric runs replaced by underscores. |
1657
- | `walkMarkdownTree` | `walkMarkdownTree(rootDir, { skipDirectories, sources, renderDescription })` | generator yielding `{frontmatter, body, description, raw, bodyLine?, bodyColumn?, file, absPath}` | walking a content tree and reading every markdown file's frontmatter and body; `skipDirectories` is required rather than defaulted, so two callers cannot silently disagree about the corpus's scope |
1706
+ | `walkMarkdownTree` | `walkMarkdownTree(rootDir, { skipDirectories, sources, renderDescription, parse })` | generator yielding `{frontmatter, body, description, raw, bodyLine?, bodyColumn?, file, absPath}` | walking a content tree and reading every markdown file's frontmatter and body; `skipDirectories` is required rather than defaulted, so two callers cannot silently disagree about the corpus's scope |
1707
+ | `walkMarkdownFiles` | `walkMarkdownFiles(rootDir, { skipDirectories })` | generator yielding each `.md` file's absolute path | listing the files `walkMarkdownTree` reads, in its order, without reading them |
1658
1708
 
1659
1709
  `buildContentLinkIndex` accepts `{ config?, records, sqlTables?, foreign? }`.
1660
1710
  `records` is the required, supplied content corpus. The optional prepared
@@ -1665,9 +1715,12 @@ so glossary word links resolve to their dictionary entries.
1665
1715
  `renderDescription` option defaults to true; false leaves `description` empty
1666
1716
  while retaining authored body and positions. `walkMarkdownTree` accepts a
1667
1717
  selected `sources` map in walk order and the same rendering option, also true
1668
- by default. Index collection explicitly omits unused rendered descriptions.
1669
- `indexRecordsFor` accepts that selected map, a `syntax` projection function
1670
- and a captured `foreignIndex`. These adapters do not retain state globally.
1718
+ by default, and a `parse` function called as `parseMarkdownFile` is, for a
1719
+ caller answering from earlier parses. Index collection explicitly omits unused
1720
+ rendered descriptions. `indexRecordsFor` accepts that selected map, a `syntax`
1721
+ projection function, a captured `foreignIndex` and a `cache` of earlier
1722
+ parses and per-note records, the one [`./note-page`](#note-page) keeps. These
1723
+ adapters do not retain state globally.
1671
1724
 
1672
1725
  The physical wrappers `lintContentCharset`, `lintContentHtml`,
1673
1726
  `lintContentIcons`, `lintContentImages` and `lintContentTaskLists` accept an
@@ -2152,6 +2205,138 @@ Where HM3 records the template priority — one statement (`data.templatePriorit
2152
2205
  | `HM3_DEFAULT_ITEM_ART` | `const HM3_DEFAULT_ITEM_ART` | — | reading the full map of default item artwork keyed by **note** type (not document subtype) for HM3 items |
2153
2206
  | `hm3DefaultItemArt` | `hm3DefaultItemArt(type)` | `string` — the default image path | getting the default art path for an HM3 item's note type, throwing when the type is unknown |
2154
2207
 
2208
+ ### `engine.sitePage`
2209
+
2210
+ One content page, from its note to its body HTML. The site build renders every
2211
+ page through these functions, and [`./note-page`](#note-page) renders a single
2212
+ note through the same ones. Nothing here reads a file.
2213
+
2214
+ | Export | Signature | Returns | Use it when |
2215
+ | --------------------------- | ---------------------------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------- |
2216
+ | `parseNote` | `parseNote(source, file, ctx)` | `{fm, body, bodyLine} \| null` | reading a note's frontmatter and body from its text, addresses decoded |
2217
+ | `contentPage` | `contentPage(note, ctx)` | `{page?, fmLinks, addressFinding?} \| null` | deciding whether a note publishes a page, and building it |
2218
+ | `inlineDrawingFigure` | `inlineDrawingFigure(svg, alt)` | `string` | the one HTML block the website sets an inline drawing in, with no blank line to end it early |
2219
+ | `pageFrontmatter` | `pageFrontmatter(page, options)` | `object` | the frontmatter a page publishes with |
2220
+ | `pageTitle` | `pageTitle(page)` | `string` | the title a page publishes under |
2221
+ | `resolveArtFields` | `resolveArtFields(data, webSrc, artSrc)` | `void` | rewriting a page's artwork addresses into the URLs the website serves |
2222
+ | `isPlainObject` | `isPlainObject(value)` | `boolean` | asking whether a value is a mapping a field may be read from |
2223
+ | `renderSitePage` | `renderSitePage(page, options)` | `{markdown, figures, data, resolved, …Errors}` | expanding one page with the passes the site writer uses |
2224
+ | `resolveSitePass` | `resolveSitePass(name, options)` | `{beforeLinks?: Function}` | resolving `site.pass` to its bundle |
2225
+ | `sitePageDecorator` | `sitePageDecorator(config, index)` | `Function` | adding the site's infobox and being metadata to a page's frontmatter |
2226
+ | `siteBodyRenderer` | `siteBodyRenderer(config)` | `(markdown, {runs, figures}) => string` | rendering an expanded page body to the HTML the site writer receives |
2227
+ | `pageDiagnostics` | `pageDiagnostics(result, rawOf)` | `Array<{file, line?, column?, severity, message}>` | turning a page render's findings into located diagnostics |
2228
+ | `frontmatterLinkDiagnostic` | `frontmatterLinkDiagnostic(finding)` | `{file, severity, message}` | reporting a wikilink authored in frontmatter |
2229
+ | `addressDiagnostic` | `addressDiagnostic(finding)` | `{file, severity, message}` | reporting a note whose page has no address |
2230
+
2231
+ `renderSitePage` takes `drawing`, a function from the file name a generated
2232
+ **From here** section names to that drawing's SVG text, and sets the drawing
2233
+ inline when it answers. The site build answers with the maps it drew.
2234
+
2235
+ ## `./note-page`
2236
+
2237
+ One note, rendered to the body HTML its site page carries, for a preview that
2238
+ reads like the published page: links as their labels and unresolved ones
2239
+ marked, `.name` spans in bold, query fences evaluated into tables, inline verse
2240
+ broken onto its lines and the generated sections a page carries.
2241
+
2242
+ ```js
2243
+ import { renderNoteFile } from "@heroiclands/package-build/note-page";
2244
+
2245
+ const { html, styles, findings, cache } = await renderNoteFile("assets/content/Lore/Tides.md", {
2246
+ root: "/path/to/repository",
2247
+ });
2248
+ ```
2249
+
2250
+ | Export | Signature | Returns | Use it when |
2251
+ | ---------------- | --------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------- |
2252
+ | `renderNoteFile` | `renderNoteFile(path, { root?, cache?, prepareTables? })` | `Promise<{ html, styles, findings, cache }>` | rendering a saved note of a repository as its site page shows |
2253
+
2254
+ `path` is the note, relative to `root` or absolute; `root` is the repository
2255
+ or any directory inside it, the working directory by default. The note is read
2256
+ from disk, so what is rendered is what is saved. `renderNoteFile` loads the
2257
+ repository's `package-build.config.yaml` and its dependencies' fetched indexes,
2258
+ derives the content tree's index records as every build does, answers the
2259
+ note's own queries and generated sections over them as `package-build site
2260
+ build` does, and renders the note through the site build's own page render.
2261
+ The repository's configuration answers every read of the configuration made
2262
+ during the call, wherever the caller stands. Nothing has to be built first: no
2263
+ content index and no site.
2264
+
2265
+ The page resolves against the notes it links as their own pages emit them.
2266
+ The note is rendered once to learn which of the package's pages it looks up,
2267
+ and those notes' query answers and generated sections are prepared beside its
2268
+ own, so a link to a generated section such as `#fromhere` resolves, and a
2269
+ cross-note `{{ref}}` finds the target's figures, numbered as its page numbers
2270
+ them. A place's **From here** drawing is drawn for that place alone, in a
2271
+ scratch directory under the system's temporary directory that is removed once
2272
+ the drawing is read, and set inline as the site sets it.
2273
+
2274
+ **The cache.** The records and the note's answers are kept in
2275
+ `build/cache/note-page/` under the repository root, and reused while every
2276
+ input they were derived from is unchanged. Nothing is reused otherwise: each
2277
+ stored answer is keyed by a hash of its inputs, which are
2278
+
2279
+ - package-build's version and the bytes of its own modules, the Node.js
2280
+ version and the DuckDB version;
2281
+ - the configuration file's bytes and the resolved configuration;
2282
+ - the bytes of every fetched dependency index, and the installed
2283
+ `@heroiclands/fonts` version and manifest;
2284
+ - every note's path and the hash of its text, and the package's asset records.
2285
+
2286
+ The dependencies' indexes and the configuration are read between two
2287
+ readings of those inputs, which are read again after the records are derived
2288
+ and around preparing any query answers the store lacks. When any reading
2289
+ differs, something changed while it was read: nothing is stored, and the whole
2290
+ render is done again from the start, the configuration resolved and the note's
2291
+ place in the tree checked afresh, so a retry answers as a fresh call would and
2292
+ a store always holds what was derived from the bytes its key names. After
2293
+ three such attempts the call throws. The cache holds two stores, each
2294
+ replaced whole by renaming a complete file over it. `notes.v8` keeps each note's parse, keyed by its text, and the
2295
+ records derived from that note alone, keyed also by what the tree gives every
2296
+ note: the setting date and calendars, the addresses the notes declare and the
2297
+ asset records. `tree.v8` keeps the whole tree's records, keyed by every input,
2298
+ and beside them the query answers of each note a render has needed. So a second render of an
2299
+ unchanged tree reads the records and the answers; a changed note re-derives
2300
+ its own records and re-runs the passes that read the whole tree; a changed
2301
+ address, calendar, asset, dependency index, configuration or toolchain
2302
+ re-derives every note. A store that cannot be read, that was written for
2303
+ other inputs, or that holds an entry of the wrong shape, down to each table
2304
+ result, inline answer, page-list result and parse inside it, is derived again
2305
+ and overwritten. Deleting the directory loses
2306
+ only time. `cache: false` derives everything and reads and writes nothing.
2307
+ `prepareTables` prepares the query answers, `prepareTreeSqlTables` from
2308
+ `engine/sql-tables.mjs` by default; a host may wrap it to time or observe the
2309
+ preparation.
2310
+
2311
+ `html` is the body before the site writer minifies it and before it unlinks
2312
+ links to pages its homepage cannot reach. Set it in
2313
+ `<div class="single-content"><div class="single-body">…</div></div>` with
2314
+ `styles`, the CSS the site theme applies to a page body, to show it as the page
2315
+ does. `styles` carries the theme's reset, body typography and ordinary link
2316
+ rules scoped to `.single-content`, so the body reads as the page does whatever
2317
+ the host's own styles, and sets the root font size on `:root`, since the
2318
+ theme measures its lengths in `rem`. Each finding is `{ file, line?, column?, severity, message }` and prints
2319
+ through `formatDiagnostic` as `file:line:column: severity: message`. `cache`
2320
+ says what the cache answered: `tree` is `"hit"` when the tree's records were
2321
+ reused whole, `"miss"` when they were derived, and `"off"` without the cache;
2322
+ `notes` counts the notes whose own records were derived again; `tables` is
2323
+ `"hit"` when the answers of the note and of every note it links were all
2324
+ reused, `"miss"` when any was prepared, and `"off"` without the cache.
2325
+
2326
+ It throws when there is no configuration at or above `root`; when the file is
2327
+ not a note of the content tree (outside `paths.content`, in a skipped
2328
+ directory, or not Markdown); when a dependency's index has not been fetched or
2329
+ cannot be read, naming `package-build deps fetch`; when a note of the tree
2330
+ cannot be indexed, as the content-index build reports it; and when the
2331
+ configuration, fonts or dependency indexes keep changing while they are read,
2332
+ three times over. A finding in the
2333
+ rendered note itself is returned in `findings`, never thrown.
2334
+
2335
+ `prepareTreeSqlTables(contentBase, { only })`, with one note's file or a
2336
+ collection of them, answers those notes' directives and generated sections and
2337
+ no other note's; its queries still read the whole corpus, so each note's
2338
+ results equal its results in a preparation of the whole tree.
2339
+
2155
2340
  ## `./preview`
2156
2341
 
2157
2342
  Import the buffer-only renderer directly without loading the build pipeline:
@@ -2193,21 +2378,21 @@ const config = defineConfig({
2193
2378
  });
2194
2379
  ```
2195
2380
 
2196
- | Export | Signature | Returns | Use it when |
2197
- | -------------------------- | ---------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2198
- | `defineConfig` | `defineConfig(config)` | `ContentBuildConfig` — the frozen, defaulted configuration | validating and normalizing a content configuration; every configuration reaches this function whether authored as YAML or as `.mjs` |
2199
- | `PACKAGE_KINDS` | `const PACKAGE_KINDS` | — | reading the three kinds of Foundry package a content module can be built into — `systems`, `modules`, and `documentation`, the kind that compiles no Foundry document at all (also the Foundry install directory name for the first two) |
2200
- | `DOCUMENTATION_KIND` | `const DOCUMENTATION_KIND` | — | naming the `packageKind` value that compiles no Foundry documents, spelled once so the validator, the CLI and the compile passes cannot disagree about what it means |
2201
- | `AUTHOR_ROLES` | `const AUTHOR_ROLES` | `readonly string[]` | reading the closed list an `authors` entry's `role` is drawn from — the configuration check refuses any other value |
2202
- | `RELATIONSHIP_KEYS` | `const RELATIONSHIP_KEYS` | `readonly string[]` | reading every key a declared relationship may carry — the configuration check refuses anything else, and the manifest writer drops the ones Foundry's own schema does not name |
2203
- | `compilesFoundryDocuments` | `compilesFoundryDocuments(config)` | `boolean` | checking whether a resolved configuration compiles Foundry documents at all — false only for a `documentation` package |
2204
- | `PACK_DOCUMENT_TYPES` | `const PACK_DOCUMENT_TYPES` | — | reading the Foundry document types this toolchain is able to compile a compendium pack of |
2205
- | `DEFAULT_PATHS` | `const DEFAULT_PATHS` | — | reading the conventional directory layout a build reads from and writes to, relative to `rootDir` |
2206
- | `DEFAULT_ADDRESS_SCHEME` | `const DEFAULT_ADDRESS_SCHEME` | — | reading an unconfigured repository's address-scheme defaults (`prefix`, where the content tree mounts inside the package) |
2207
- | `DERIVED_SYSTEM_VERSION` | `const DERIVED_SYSTEM_VERSION` | — | the loader-only symbol key `defineConfig` uses internally to receive a resolved system version; not something a configuration author writes |
2208
- | `publishesContentPages` | `publishesContentPages(config)` | `boolean` | checking whether the authored content tree contains notes beyond its homepage, shared by site, PDF, index, and Foundry address emission |
2209
- | `siteBase` | `siteBase(config)` | `string` | reading the path a package's site is served at — `site.base` where the configuration states one, `/<contentPackage>/` otherwise |
2210
- | `siteBuildConfig` | `siteBuildConfig(config, { baseUrl, assets })` | `ContentBuildConfig` | applying optional runtime site URLs to a frozen effective configuration; the caller first validates the authored homepage with `checkHomepage`; the CLI uses it before index/body preflight |
2381
+ | Export | Signature | Returns | Use it when |
2382
+ | -------------------------- | ---------------------------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2383
+ | `defineConfig` | `defineConfig(config)` | `ContentBuildConfig` — the frozen, defaulted configuration | validating and normalizing a content configuration; every configuration reaches this function whether authored as YAML or as `.mjs` |
2384
+ | `PACKAGE_KINDS` | `const PACKAGE_KINDS` | — | reading the three kinds of Foundry package a content module can be built into — `systems`, `modules`, and `documentation`, the kind that compiles no Foundry document at all (also the Foundry install directory name for the first two) |
2385
+ | `DOCUMENTATION_KIND` | `const DOCUMENTATION_KIND` | — | naming the `packageKind` value that compiles no Foundry documents, spelled once so the validator, the CLI and the compile passes cannot disagree about what it means |
2386
+ | `AUTHOR_ROLES` | `const AUTHOR_ROLES` | `readonly string[]` | reading the closed list an `authors` entry's `role` is drawn from — the configuration check refuses any other value |
2387
+ | `RELATIONSHIP_KEYS` | `const RELATIONSHIP_KEYS` | `readonly string[]` | reading every key a declared relationship may carry — the configuration check refuses anything else, and the manifest writer drops the ones Foundry's own schema does not name |
2388
+ | `compilesFoundryDocuments` | `compilesFoundryDocuments(config)` | `boolean` | checking whether a resolved configuration compiles Foundry documents at all — false only for a `documentation` package |
2389
+ | `PACK_DOCUMENT_TYPES` | `const PACK_DOCUMENT_TYPES` | — | reading the Foundry document types this toolchain is able to compile a compendium pack of |
2390
+ | `DEFAULT_PATHS` | `const DEFAULT_PATHS` | — | reading the conventional directory layout a build reads from and writes to, relative to `rootDir` |
2391
+ | `DEFAULT_ADDRESS_SCHEME` | `const DEFAULT_ADDRESS_SCHEME` | — | reading an unconfigured repository's address-scheme defaults (`prefix`, where the content tree mounts inside the package) |
2392
+ | `DERIVED_SYSTEM_VERSION` | `const DERIVED_SYSTEM_VERSION` | — | the loader-only symbol key `defineConfig` uses internally to receive a resolved system version; not something a configuration author writes |
2393
+ | `publishesContentPages` | `publishesContentPages(config)` | `boolean` | checking whether the authored content tree contains notes beyond its homepage, shared by site, PDF, index, and Foundry address emission |
2394
+ | `siteBase` | `siteBase(config)` | `string` | reading the path a package's site is served at — `site.base` where the configuration states one, `/<contentPackage>/` otherwise |
2395
+ | `siteBuildConfig` | `siteBuildConfig(config, { root, baseUrl, assets })` | `ContentBuildConfig` | selecting internal www by default, explicit root-derived CDN over configured assets, and low-level baseUrl/assets over derived values; exposes effective homepage/site.base/site.assets without changing authored configuration; the caller first validates the authored homepage with `checkHomepage`; the CLI uses it before index/body preflight |
2211
2396
 
2212
2397
  ## `./config`
2213
2398
 
@@ -2638,6 +2823,10 @@ preparation and kept separate.
2638
2823
  `prepareTreeSqlTables` retains the complete record membership when opening a
2639
2824
  public database, so derived citations can distinguish hidden notes from
2640
2825
  unknown addresses. Only public records contribute rendered sources and links.
2826
+ `prepareTreeSqlTables(contentBase, { only })`, with one note's file, answers
2827
+ that note's directives and generated sections and no other note's; its queries
2828
+ still read the whole corpus, so its results equal that note's in a preparation
2829
+ of the whole tree.
2641
2830
 
2642
2831
  | Export | Contract |
2643
2832
  | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
package/docs/commands.md CHANGED
@@ -1324,13 +1324,17 @@ package leaves out as a named asset set.
1324
1324
  **SYNOPSIS**
1325
1325
 
1326
1326
  ```
1327
- package-build cdn publish --set <file> --name <display name> [--dry-run]
1327
+ package-build cdn publish [--dest internal|cloudflare] --set <file> --name <display name> [--dry-run]
1328
1328
  ```
1329
1329
 
1330
1330
  **DESCRIPTION**
1331
1331
 
1332
- The one writer to `<site.assets>/<contentPackage>/`. It is run by hand, or by
1333
- a manually dispatched workflow; no build or release runs it.
1332
+ Publication is manual and separate from site building and deployment. No build
1333
+ or release runs it. `--dest internal` is the default: pictures go to
1334
+ `atlas:/srv/preview/cdn`, served at `https://cdn.internal.heroiclands.org`.
1335
+ `--dest cloudflare` uses the existing R2 bucket served at configured `site.assets`
1336
+ (normally `https://cdn.heroiclands.org`). Each destination uses the same planner,
1337
+ transforms, object keys and publication rules.
1334
1338
 
1335
1339
  Every icon and image under the package's picture roots is uploaded flat, as
1336
1340
  `icons/<basename>` or `images/<basename>` — shipped and left-out pictures
@@ -1339,11 +1343,10 @@ site build gives it. `packageBuild.assetTransform` runs on each file exactly
1339
1343
  as `package-build assets` runs it, so a picture on the CDN holds the bytes the
1340
1344
  staged package holds. SVG is served as `image/svg+xml`.
1341
1345
 
1342
- The bucket is served at `site.assets`, so an object's key is its path below
1343
- that address: `thalorna/images/thorn.webp` is
1346
+ An object's key is its path below the selected CDN origin. For Cloudflare, `thalorna/images/thorn.webp` is
1344
1347
  `https://cdn.heroiclands.org/thalorna/images/thorn.webp`.
1345
1348
 
1346
- `--set` names the set file, written at `<site.assets>/<contentPackage>/<file>`.
1349
+ `--set` names the set file, written at `<selected CDN origin>/<contentPackage>/<file>`.
1347
1350
  It lists only the pictures the package leaves out — an address a set does not
1348
1351
  list resolves to the shipped copy — chosen by the same rule the stage applies,
1349
1352
  `packageBuild.shippingLicenses` included. Its shape is the shipped set's, with
@@ -1380,9 +1383,32 @@ does not have, and a note naming left-out art that is not on the CDN when the
1380
1383
  run ends, are warnings at the note's line and column: an art slot under
1381
1384
  `data:`, an embed, or a body image.
1382
1385
 
1383
- **Credentials.** The store is an S3-compatible bucket — Cloudflare R2 for
1384
- `cdn.heroiclands.org` — signed with AWS Signature Version 4. Its settings are
1385
- read from the environment, after `.env.local` and `.env` are loaded:
1386
+ **Internal access.** Install `rsync` and OpenSSH on the publishing machine.
1387
+ The default uses your ordinary `atlas` SSH account, which must have write access
1388
+ to `/srv/preview/cdn`. Keys come from your SSH configuration or agent; the command
1389
+ uses batch mode and requires an already verified host key with
1390
+ `StrictHostKeyChecking=yes`. It uses only rsync's receiver protocol, keeps keys out
1391
+ of arguments and logs, and never deletes existing CDN objects. Transfer failures
1392
+ report the exit code and a bounded, sanitized receiver diagnostic; local staging
1393
+ failures name the operation and filesystem error code. Commands, key material and
1394
+ full child-process errors are omitted.
1395
+
1396
+ For an existing restricted publisher credential, configure an SSH alias with its
1397
+ `User preview-publish`, `IdentityFile` and verified `UserKnownHostsFile`. Then set
1398
+ `CDN_INTERNAL_DESTINATION=your-alias:cdn` in the environment or `.env.local`.
1399
+ The deployed forced command `rrsync -wo -munge /srv/preview` accepts that relative
1400
+ destination; do not repeat `/srv/preview` for this account. The override accepts
1401
+ only an SSH host alias followed by `:cdn` or `:/srv/preview/cdn`. It changes the
1402
+ connection, not the public origin. This command neither installs credentials nor
1403
+ changes server permissions.
1404
+
1405
+ Internal skip checks and set comparisons read the selected internal CDN over
1406
+ HTTPS. An internal dry run uses only those reads: no SSH connection, temporary
1407
+ staging or R2 credentials. The CDN must be reachable from the LAN or VPN.
1408
+
1409
+ **Cloudflare credentials.** With `--dest cloudflare`, the store is the existing
1410
+ S3-compatible bucket, signed with AWS Signature Version 4. Its settings are read
1411
+ from the environment, after `.env.local` and `.env` are loaded:
1386
1412
 
1387
1413
  | Variable | What it is |
1388
1414
  | ----------------------- | ------------------------------------------------------------------------- |
@@ -1391,46 +1417,35 @@ read from the environment, after `.env.local` and `.env` are loaded:
1391
1417
  | `CDN_ACCESS_KEY_ID` | The access key id of a token that can write the bucket. |
1392
1418
  | `CDN_SECRET_ACCESS_KEY` | That token's secret. Never printed. |
1393
1419
 
1394
- Setting some but not all of them is an error naming the missing ones. With
1395
- none set, `--dry-run` compares against the public CDN at `site.assets`
1396
- instead, read-only, and an upload is refused.
1397
-
1398
- A workflow calls the command the same way, passing the four values as named
1399
- secrets in the step's environment:
1400
-
1401
- ```yaml
1402
- - run: npx package-build cdn publish --set alt-art.json --name "Thalorna — illustrated"
1403
- env:
1404
- CDN_ENDPOINT: ${{ secrets.CDN_ENDPOINT }}
1405
- CDN_BUCKET: ${{ secrets.CDN_BUCKET }}
1406
- CDN_ACCESS_KEY_ID: ${{ secrets.CDN_ACCESS_KEY_ID }}
1407
- CDN_SECRET_ACCESS_KEY: ${{ secrets.CDN_SECRET_ACCESS_KEY }}
1408
- ```
1420
+ Setting some but not all of them is an error naming the missing ones. With none
1421
+ set, `--dest cloudflare --dry-run` compares against `site.assets` read-only, and an
1422
+ upload is refused. Internal publication ignores all four R2 settings.
1409
1423
 
1410
1424
  **OPTIONS**
1411
1425
 
1412
- | Option | Type | Default | Description |
1413
- | ----------- | ------- | ------- | --------------------------------------------------------------------------- |
1414
- | `--set` | string | — | Required. The set file's name under the package's CDN root, a `.json` file. |
1415
- | `--name` | string | — | Required. The set's display name. |
1416
- | `--dry-run` | boolean | `false` | List what would upload and what is skipped; write nothing. |
1426
+ | Option | Type | Default | Description |
1427
+ | ----------- | ------- | ---------- | --------------------------------------------------------------------------- |
1428
+ | `--dest` | string | `internal` | `internal` for atlas; `cloudflare` for the configured R2 bucket. |
1429
+ | `--set` | string | — | Required. The set file's name under the package's CDN root, a `.json` file. |
1430
+ | `--name` | string | — | Required. The set's display name. |
1431
+ | `--dry-run` | boolean | `false` | List what would upload and what is skipped; write nothing. |
1417
1432
 
1418
1433
  **EXIT STATUS**
1419
1434
 
1420
1435
  1 when a picture's file has no address, when a picture fails to upload, when
1421
- `--set` is `default.json` or a path, when `site.assets` is undeclared, and
1422
- when an upload has no store configured. Warnings about notes do not change
1436
+ `--set` is `default.json` or a path, when Cloudflare `site.assets` is undeclared, when an internal destination or
1437
+ planned relative key is unsafe, and when an upload lacks access to its store. Warnings about notes do not change
1423
1438
  it. Otherwise 0.
1424
1439
 
1425
1440
  **EXAMPLES**
1426
1441
 
1427
1442
  ```
1428
1443
  $ package-build cdn publish --set alt-art.json --name "Thalorna — illustrated" --dry-run
1429
- Dry run: comparing 582 pictures against https://cdn.heroiclands.org, read-only.
1444
+ Dry run: comparing 582 pictures against https://cdn.internal.heroiclands.org, read-only.
1430
1445
  would upload thalorna/icons/anvilbdg.svg (969 bytes)
1431
1446
  …
1432
1447
  would upload thalorna/alt-art.json (582 assets)
1433
- Would upload 582, skipped 0, failed 0; https://cdn.heroiclands.org/thalorna/alt-art.json lists 582 left-out pictures (would change).
1448
+ Would upload 582, skipped 0, failed 0; https://cdn.internal.heroiclands.org/thalorna/alt-art.json lists 582 left-out pictures (would change).
1434
1449
 
1435
1450
  $ package-build cdn publish --set default.json --name Shipped
1436
1451
  package-build: `--set default.json` is refused: `default.json` is the shipped set's name, the fallback every other set is read against. Name the set for its art
@@ -2685,7 +2700,7 @@ it for search.
2685
2700
  **SYNOPSIS**
2686
2701
 
2687
2702
  ```
2688
- package-build site build [--out <dir>] [--base-url <url>] [--assets <url>]
2703
+ package-build site build [--root <domain>] [--out <dir>] [--base-url <url>] [--assets <url>]
2689
2704
  ```
2690
2705
 
2691
2706
  **DESCRIPTION**
@@ -2748,6 +2763,11 @@ out in the [site guide](guides/site.md#search).
2748
2763
 
2749
2764
  The site's address, title and menu are read before the tree is, so a missing
2750
2765
  `homepage` or a cold navigation cache fails with the last good site in place.
2766
+ With [`site.inputs`](configuration.md#local-site-inputs), a local module supplies
2767
+ the selected navigation and text artifacts instead of reading the navigation
2768
+ cache. It receives the effective configuration, runs before content rendering,
2769
+ and may reject canonical navigation drift. Its files are validated before
2770
+ output replacement and written after the renderer clears the site.
2751
2771
  Every gate is then checked and reported, and the run stops at the first that
2752
2772
  fires, ordered so the report names the cause rather than its symptoms — an
2753
2773
  unusable dependency manifest, reported after the links that failed because of
@@ -2760,6 +2780,13 @@ run fails before the search index is built.
2760
2780
 
2761
2781
  `--out <dir>` — the directory that is deployed. Defaults to `build/site`.
2762
2782
 
2783
+ `--root <domain>` — domain for the build's addresses, defaulting to
2784
+ `internal.heroiclands.org`. The site uses `https://www.<domain>` with the
2785
+ package's existing homepage path. An explicit root also selects
2786
+ `https://cdn.<domain>` over configured `site.assets`; omitting this flag keeps
2787
+ configured assets, or derives the CDN when none are configured. Supply a plain
2788
+ DNS domain without a scheme, path, port, credentials, query or fragment.
2789
+
2763
2790
  `--base-url <url>` — the absolute HTTP(S) address of this build. Its pathname
2764
2791
  becomes the effective `site.base`, and must end with `/`; `/` serves the site
2765
2792
  at the origin's root. Pages, local links, theme files, script fonts and search
@@ -2768,8 +2795,18 @@ all use that prefix. Output lands under `<out>/<pathname>/`.
2768
2795
  `--assets <url>` — the absolute HTTP(S) asset root for this build. It may
2769
2796
  include a path; trailing slashes are trimmed.
2770
2797
 
2771
- Both options are optional and independent. Without them, the build uses the
2772
- authored `homepage`, `site.base` and `site.assets`. Overrides apply in memory
2798
+ Explicit `--base-url` overrides the root-derived site URL, including its path;
2799
+ explicit `--assets` overrides both root-derived and configured assets. These
2800
+ low-level flags are independent and may be combined with `--root`. Cached
2801
+ navigation links on the canonical Heroic Lands www origin and the brand's
2802
+ license link use the effective site origin. External links and authored content
2803
+ links keep their addresses. Asset references on the exact canonical
2804
+ `https://cdn.heroiclands.org` origin use the effective asset root, preserving
2805
+ the path, query and fragment. Other image origins stay unchanged.
2806
+ The build only writes local output; it neither
2807
+ uploads nor fetches missing dependencies or assets.
2808
+
2809
+ Overrides apply in memory
2773
2810
  before preflight and rendering; they never rewrite `package.json` or
2774
2811
  `package-build.config.yaml`. The authored homepage must still pass its usual
2775
2812
  validation. Both URLs require plain paths without percent escapes, dot