@heroiclands/package-build 22.1.0 → 22.2.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.
@@ -752,21 +752,22 @@ Any other key under `docs.itemFields` is refused:
752
752
 
753
753
  **Type:** object · **Optional** · every key defaults to nothing published:
754
754
 
755
- | Key | Type | Default |
756
- | ----------------------- | -------- | --------------------- |
757
- | `site.base` | string | `""` |
758
- | `site.assets` | string | `""` |
759
- | `site.packages` | string[] | `[]` |
760
- | `site.sections` | object | `{}` |
761
- | `site.readmeSections` | object | `{}` |
762
- | `site.landing` | object | `null` |
763
- | `site.trees` | array | `[]` |
764
- | `site.pass` | string | `""` |
765
- | `site.passOptions` | object | `{}` |
766
- | `site.backfillSections` | boolean | `false` |
767
- | `site.list` | object | `{shortcodes: false}` |
768
- | `site.notfound` | object | `null` |
769
- | `site.hugo` | object | `{}` |
755
+ | Key | Type | Default |
756
+ | ----------------------- | -------- | ------------------------------------------- |
757
+ | `site.base` | string | `""` |
758
+ | `site.assets` | string | `""`, but required for `content-build site` |
759
+ | `site.description` | string | `""`, but required for `content-build site` |
760
+ | `site.packages` | string[] | `[]` |
761
+ | `site.sections` | object | `{}` |
762
+ | `site.readmeSections` | object | `{}` |
763
+ | `site.landing` | object | `null` |
764
+ | `site.trees` | array | `[]` |
765
+ | `site.pass` | string | `""` |
766
+ | `site.passOptions` | object | `{}` |
767
+ | `site.backfillSections` | boolean | `false` |
768
+ | `site.list` | object | `{shortcodes: false}` |
769
+ | `site.notfound` | object | `null` |
770
+ | `site.hugo` | object | `{}` |
770
771
 
771
772
  How much of a package reaches the web at all is **not** here — it is
772
773
  [`publish.site`](#publish). `site` is framing: what a section is called,
@@ -785,7 +786,7 @@ published. A `site.out` is refused by name:
785
786
 
786
787
  > ``package-build config: `site` must be a mapping.``
787
788
 
788
- > ``package-build config: `site.<key>` is not a recognized option (expected one of: base, assets, packages, sections, readmeSections, landing, trees, pass, passOptions, backfillSections, list, notfound, hugo).``
789
+ > ``package-build config: `site.<key>` is not a recognized option (expected one of: base, assets, description, packages, sections, readmeSections, landing, trees, pass, passOptions, backfillSections, list, notfound, hugo).``
789
790
 
790
791
  `site.assets` is the host every package's imagery is served from, and it is
791
792
  the one address in this file that is not this repository's own. A note names
@@ -798,11 +799,30 @@ Absolute, and the trailing slash is trimmed:
798
799
 
799
800
  > ``package-build config: `site.assets` must be an absolute `http://` or `https://` address — it is the host every package's imagery is served from, and a relative value resolves against whichever page happens to carry the image.``
800
801
 
802
+ `content-build site` refuses to generate a configuration with no
803
+ `site.assets` at all — there is no defensible default, because the theme
804
+ resolves every relative asset against it:
805
+
806
+ > ``package-build config: `site.assets` is not declared, and a site build needs one — it is the host every package's imagery is served from, and the theme resolves every relative asset against it.``
807
+
801
808
  The generated Hugo configuration carries the same host as
802
809
  `params.cdnBaseURL`, which the theme resolves a relative asset path against.
803
810
  The two are one value read by two readers: the toolchain emits it into a
804
811
  page, and the theme joins it onto anything the toolchain left relative.
805
812
 
813
+ `site.description` is the site's `<meta name="description">` — one plain
814
+ sentence, distinct from the Foundry package browser's pitch
815
+ ([`packageBuild.manifest.descriptionHtml`](#packagebuildmanifest), which
816
+ allows HTML). Required for `content-build site`, the way `packageBuild.manifest.title`
817
+ is:
818
+
819
+ > ``package-build config: `site.description` is not declared, and the site's `<meta name="description">` reads from it.``
820
+
821
+ Markup belongs in `descriptionHtml`, not here — a value containing `<` is
822
+ refused:
823
+
824
+ > ``package-build config: `site.description` contains `<` — this is plain text for the site's `<meta name="description">`; markup belongs in `packageBuild.manifest.descriptionHtml`.``
825
+
806
826
  `site.packages` names which content packages' notes the site walks, beyond
807
827
  this one's own; `site.pass` names a repository's own body-rewrite bundle
808
828
  (the one part of the site contract that is code, exactly as `itemBuilders`
@@ -956,24 +976,26 @@ the block cannot grow into a second configuration file:
956
976
  `content-build site` writes `build/hugo/hugo.toml` on every run. Every value
957
977
  in it has one source, and that source is where it is edited:
958
978
 
959
- | Key | Derived from |
960
- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
961
- | `baseURL` | `package.json` `homepage`, checked by `checkHomepage` — an absolute URL ending `/<contentPackage>/` |
962
- | `title` | `packageBuild.manifest.title`, which is required |
963
- | `locale` | the organisation's locale, `en-us`, in `engine/site-config.mjs` |
964
- | `publishDir` | `contentPackage`, under the deployment root `build/site` — written relative to `build/hugo/`, so `../site/<contentPackage>` |
965
- | `contentDir` | the fixed content mount, `build/hugo/content` — written as `content` |
966
- | `themesDir` | where `@heroiclands/hugo-theme` is installed, resolved the way Node resolves a package and written relative to `build/hugo/` |
967
- | `theme` | the installed `@heroiclands/hugo-theme`, so `hugo-theme` |
968
- | `disableKinds` | the toolchain, which renders the same kinds on every site: `["taxonomy", "term", "RSS"]` |
969
- | `params.description` | `package.json` `description`; absent when the package declares none |
970
- | `params.author` | `package.json` `author`, its `name`; absent when the package declares none |
971
- | `params.cdnBaseURL` | `site.assets`; absent when unset |
972
- | `params.brand` | the organisation's brand links `logo`, `licenseURL`, `discordURL` in `engine/site-config.mjs` |
973
- | `params.list` | `site.list` |
974
- | `params.notfound` | `site.notfound`; absent when undeclared |
975
- | `markup.goldmark.renderer.unsafe` | the toolchain, whose pages carry raw HTML — a `<figure>` for every image, a `<span>` marking an unresolved link |
976
- | `menu` | the navigation `content-build deps fetch` caches from `https://www.heroiclands.org/nav.json`, entry for entry, a dropdown's entries as `parent` entries |
979
+ | Key | Derived from |
980
+ | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
981
+ | `baseURL` | `package.json` `homepage`, checked by `checkHomepage` — an absolute URL ending `/<contentPackage>/` |
982
+ | `title` | `packageBuild.manifest.title`, which is required |
983
+ | `locale` | the organisation's locale, `en-us`, in `engine/site-config.mjs` |
984
+ | `publishDir` | `contentPackage`, under the deployment root `build/site` — written relative to `build/hugo/`, so `../site/<contentPackage>` |
985
+ | `contentDir` | the fixed content mount, `build/hugo/content` — written as `content` |
986
+ | `themesDir` | where `@heroiclands/hugo-theme` is installed, resolved the way Node resolves a package and written relative to `build/hugo/` |
987
+ | `theme` | the installed `@heroiclands/hugo-theme`, so `hugo-theme` |
988
+ | `disableKinds` | whether any note in the tree carries `tags:`, which the site walk discovers: `["taxonomy", "term", "RSS"]` when none does, `["RSS"]` when at least one does |
989
+ | `taxonomies` | the same fact — written as `{ tag = "tags" }` when at least one note carries `tags:`, absent otherwise |
990
+ | `outputs` | the same fact — written as `{ taxonomy = ["HTML"], term = ["HTML"] }` when at least one note carries `tags:`, absent otherwise |
991
+ | `params.description` | `site.description`, which is required |
992
+ | `params.author` | `package.json` `author`, its `name`; absent when the package declares none |
993
+ | `params.cdnBaseURL` | `site.assets`, which is required |
994
+ | `params.brand` | the organisation's brand links — `logo`, `licenseURL`, `discordURL` — in `engine/site-config.mjs` |
995
+ | `params.list` | `site.list` |
996
+ | `params.notfound` | `site.notfound`; absent when undeclared |
997
+ | `markup.goldmark.renderer.unsafe` | the toolchain, whose pages carry raw HTML — a `<figure>` for every image, a `<span>` marking an unresolved link |
998
+ | `menu` | the navigation `content-build deps fetch` caches from `https://www.heroiclands.org/nav.json`, entry for entry, a dropdown's entries as `parent` entries |
977
999
 
978
1000
  The site build reads the navigation from the cache only. A cold cache is an
979
1001
  error naming the command that fills it:
@@ -989,9 +1011,24 @@ none fails the site build:
989
1011
 
990
1012
  > ``package-build config: `packageBuild.manifest.title` is not declared, and the site's `title` reads from it.``
991
1013
 
992
- Nothing else is emitted. `[taxonomies]` and `[outputs]` are Hugo's defaults
993
- once the taxonomy kinds and RSS are disabled, and every other key is
994
- `site.hugo`'s to add.
1014
+ `params.description` reads from `site.description` the same way, and fails
1015
+ the same way when it is absent:
1016
+
1017
+ > ``package-build config: `site.description` is not declared, and the site's `<meta name="description">` reads from it.``
1018
+
1019
+ `params.cdnBaseURL` reads from `site.assets`, and the theme resolves every
1020
+ relative asset against it, so a configuration declaring none fails the site
1021
+ build the same way:
1022
+
1023
+ > ``package-build config: `site.assets` is not declared, and a site build needs one — it is the host every package's imagery is served from, and the theme resolves every relative asset against it.``
1024
+
1025
+ Nothing else is emitted. A site whose notes carry no `tags:` publishes no
1026
+ taxonomy pages — `[taxonomies]` and `[outputs]` go unwritten, and Hugo's
1027
+ defaults never apply because `taxonomy` and `term` are disabled kinds. A site
1028
+ with at least one tagged note publishes `/tags/` and a page per tag: Hugo's
1029
+ own default taxonomy pair also declares `category`, so `[taxonomies]` names
1030
+ only `tag`, and `[outputs]` restricts both to `HTML` so neither produces a
1031
+ feed. Every other key is `site.hugo`'s to add.
995
1032
 
996
1033
  ### `pdf`
997
1034
 
@@ -1149,6 +1186,7 @@ Each entry, in any of the four lists:
1149
1186
  | `relationships.systems[].manifest` | string | no | none |
1150
1187
  | `relationships.systems[].compatibility` | object, `{minimum?, verified?}` | no | none |
1151
1188
  | `relationships.systems[].itemCatalog` | boolean | no | `false` |
1189
+ | `relationships.systems[].contentIndex` | boolean | no | `true` |
1152
1190
 
1153
1191
  (the same keys apply under `requires[]`, `recommends[]` and
1154
1192
  `conflicts[]`.)
@@ -1157,7 +1195,7 @@ Each entry, in any of the four lists:
1157
1195
 
1158
1196
  > ``package-build config: `relationships.<kind>[<index>].id` must be a non-empty string.``
1159
1197
 
1160
- > ``package-build config: `relationships.<kind>[<index>].<key>` is not a recognized option (expected one of: id, contentPackage, type, manifest, compatibility, itemCatalog).``
1198
+ > ``package-build config: `relationships.<kind>[<index>].<key>` is not a recognized option (expected one of: id, contentPackage, type, manifest, compatibility, itemCatalog, contentIndex).``
1161
1199
 
1162
1200
  `contentPackage` names what the other package's _content_ is called, where
1163
1201
  that differs from its Foundry id. A note addresses a file by the content
@@ -1176,6 +1214,20 @@ item catalogue at build time. It requires a `manifest`:
1176
1214
 
1177
1215
  > ``package-build config: `relationships.<kind>[<index>].itemCatalog` needs a `manifest` naming the package to fetch.``
1178
1216
 
1217
+ `contentIndex` and `itemCatalog` are the two edges a relationship may declare,
1218
+ and a package may have either without the other. `itemCatalog` says a
1219
+ dependency supplies _items_; `contentIndex`, `true` by default, says
1220
+ `deps fetch` fetches its published note index and this tree may cite its
1221
+ addresses by wikilink. Declaring `contentIndex: false` narrows the
1222
+ relationship to the Foundry manifest only — a dependency Foundry installs but
1223
+ this tree never cites — so `deps fetch` fetches nothing for it and a wikilink
1224
+ into it fails, naming the key, rather than resolving against a stale
1225
+ declaration or an index nobody fetched:
1226
+
1227
+ > ``package-build config: `relationships.<kind>[<index>].contentIndex` must be true or false.``
1228
+
1229
+ > ``package-build config: `relationships.<kind>[<index>].contentIndex` cannot be false together with `itemCatalog: true` — a catalogue is fetched from the same index.``
1230
+
1179
1231
  ### `systems`
1180
1232
 
1181
1233
  **Type:** object (`{id: spec}`) · **Optional** · default `{}`. Refused in a
@@ -1415,11 +1467,22 @@ that has a wrong answer rather than an unknown one: a key the build
1415
1467
  silently overwritten and the two would be free to disagree with nothing to
1416
1468
  say so.
1417
1469
 
1470
+ `packageBuild.manifest.descriptionHtml` is the exception worth calling out on
1471
+ its own: it is not forbidden, it **is** how `description` is authored. It is
1472
+ the pitch Foundry's package browser shows — HTML allowed, any length — and it
1473
+ is emitted into the generated manifest as `description`; the key itself never
1474
+ survives into the manifest under its own name. `package.json`'s own
1475
+ `description` is read by neither this nor the site (see
1476
+ [`site.description`](#site)) — a declared one is reported as a warning naming
1477
+ both real keys, so it cannot drift back into use:
1478
+
1479
+ > `package.json: warning: \`description\` is read by nothing; the Foundry pitch is \`packageBuild.manifest.descriptionHtml\` and the site's is \`site.description\`` — a JSON manifest carries no line to point at, so only the file is named.
1480
+
1418
1481
  | Forbidden key | Derived from |
1419
1482
  | ------------------------------------- | ---------------------------------------------------------------- |
1420
1483
  | `packageBuild.manifest.id` | `foundryPackage`, itself derived from `package.json` `name` |
1421
1484
  | `packageBuild.manifest.version` | `package.json` `version` |
1422
- | `packageBuild.manifest.description` | `package.json` `description` |
1485
+ | `packageBuild.manifest.description` | `packageBuild.manifest.descriptionHtml` |
1423
1486
  | `packageBuild.manifest.url` | `package.json` `repository` |
1424
1487
  | `packageBuild.manifest.bugs` | `package.json` `repository` |
1425
1488
  | `packageBuild.manifest.manifest` | `package.json` `repository` and the release tag |
@@ -1430,7 +1493,7 @@ say so.
1430
1493
 
1431
1494
  > ``package-build config: `packageBuild.manifest.version` is derived from package.json `version` and must not be declared — it would be overwritten, and the two would disagree with nothing to say so.``
1432
1495
 
1433
- > ``package-build config: `packageBuild.manifest.description` is derived from package.json `description` and must not be declared — it would be overwritten, and the two would disagree with nothing to say so.``
1496
+ > ``package-build config: `packageBuild.manifest.description` is derived from `packageBuild.manifest.descriptionHtml` and must not be declared — it would be overwritten, and the two would disagree with nothing to say so.``
1434
1497
 
1435
1498
  > ``package-build config: `packageBuild.manifest` must be a mapping.``
1436
1499
 
@@ -1647,17 +1710,17 @@ the source directory:
1647
1710
 
1648
1711
  ## Every retired or forbidden key, in one place
1649
1712
 
1650
- | Key | Why |
1651
- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
1652
- | `publish.address.landing` | Retired — named a whole-section landing, and there are no sections to address. |
1653
- | `packs[].folders` | Retired — a folder is a note (`type: folder`), materialised by the pack whose documents reference it. |
1654
- | `rootDir` | Forbidden in a YAML configuration — always the file's own directory. |
1655
- | `foundryPackage` | Forbidden in a YAML configuration — always the adjacent `package.json` `name`. |
1656
- | `homepage`, `author` | Forbidden in a YAML configuration — always the adjacent `package.json`'s own `homepage` and `author`. |
1657
- | `stats.systemId` | Forbidden in every configuration — derived from `packageKind`, `requiresSystem` or a lone declared system. |
1658
- | `stats.systemVersion` | Forbidden in every configuration — derived from `package.json` (a system) or `systems:` / `relationships.systems` (a module). |
1659
- | `packageBuild.manifest.id`, `.version`, `.description`, `.url`, `.bugs`, `.manifest`, `.download`, `.compatibility`, `.relationships`, `.packs` | Forbidden — each is derived from `package.json` or the top level of `package-build.config.yaml`; see [`packageBuild.manifest`](#packagebuildmanifest). |
1660
- | `site.out` | Retired — the site build writes its content mount at `build/hugo/content`, beside the generated `hugo.toml`. |
1661
- | `site.hugo.baseURL`, `.title`, `.locale`, `.publishDir`, `.contentDir`, `.themesDir`, `.theme`, `.disableKinds`, `.params.description`, `.params.author`, `.params.cdnBaseURL`, `.params.brand`, `.params.list`, `.params.notfound`, `.markup.goldmark.renderer.unsafe`, `.menu` | Forbidden — each is written by the site build from a source it names; see [the generated Hugo configuration](#the-generated-hugo-configuration). |
1662
- | `publish.site: true` / `publish.site: false` | Refused rather than mapped — write `homepage` or `content`. |
1663
- | `packs`, `itemBuilders`, `docs`, `compatibility`, `relationships`, `systems`, `requiresSystem`, `stats`, `foundryPackage` | Forbidden in a `documentation` package — each describes a Foundry package this kind is not; see the key's own section for its located refusal message. |
1713
+ | Key | Why |
1714
+ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
1715
+ | `publish.address.landing` | Retired — named a whole-section landing, and there are no sections to address. |
1716
+ | `packs[].folders` | Retired — a folder is a note (`type: folder`), materialised by the pack whose documents reference it. |
1717
+ | `rootDir` | Forbidden in a YAML configuration — always the file's own directory. |
1718
+ | `foundryPackage` | Forbidden in a YAML configuration — always the adjacent `package.json` `name`. |
1719
+ | `homepage`, `author` | Forbidden in a YAML configuration — always the adjacent `package.json`'s own `homepage` and `author`. |
1720
+ | `stats.systemId` | Forbidden in every configuration — derived from `packageKind`, `requiresSystem` or a lone declared system. |
1721
+ | `stats.systemVersion` | Forbidden in every configuration — derived from `package.json` (a system) or `systems:` / `relationships.systems` (a module). |
1722
+ | `packageBuild.manifest.id`, `.version`, `.description`, `.url`, `.bugs`, `.manifest`, `.download`, `.compatibility`, `.relationships`, `.packs` | Forbidden — each is derived from `package.json` or the top level of `package-build.config.yaml`; see [`packageBuild.manifest`](#packagebuildmanifest). |
1723
+ | `site.out` | Retired — the site build writes its content mount at `build/hugo/content`, beside the generated `hugo.toml`. |
1724
+ | `site.hugo.baseURL`, `.title`, `.locale`, `.publishDir`, `.contentDir`, `.themesDir`, `.theme`, `.disableKinds`, `.taxonomies`, `.outputs`, `.params.description`, `.params.author`, `.params.cdnBaseURL`, `.params.brand`, `.params.list`, `.params.notfound`, `.markup.goldmark.renderer.unsafe`, `.menu` | Forbidden — each is written by the site build from a source it names; see [the generated Hugo configuration](#the-generated-hugo-configuration). |
1725
+ | `publish.site: true` / `publish.site: false` | Refused rather than mapped — write `homepage` or `content`. |
1726
+ | `packs`, `itemBuilders`, `docs`, `compatibility`, `relationships`, `systems`, `requiresSystem`, `stats`, `foundryPackage` | Forbidden in a `documentation` package — each describes a Foundry package this kind is not; see the key's own section for its located refusal message. |
@@ -86,7 +86,7 @@ import {
86
86
  PACKAGE_BASE,
87
87
  readCanonicalKey,
88
88
  } from "./content-address.mjs";
89
- import { loadForeignIndexes } from "./metadata-index.mjs";
89
+ import { loadForeignIndexes, noContentIndexPackages } from "./metadata-index.mjs";
90
90
  import { frontmatterWikilinks, slugify } from "./web-wikilinks.mjs";
91
91
  import { homepageAddresses, isHomepage } from "./homepage.mjs";
92
92
  import { RETIRED_TYPES } from "./ids.mjs";
@@ -268,6 +268,10 @@ export function buildLinkIndex(
268
268
  for (const v of foreign.index.values()) if (v.type) types.add(v.type);
269
269
 
270
270
  const packages = new Set([...(byKey.size ? [pkg] : []), ...foreign.packages]);
271
+ // Packages declared `contentIndex: false` — a Foundry dependency only, with
272
+ // no fetched index. A link naming one is refused with a diagnostic that
273
+ // names the key, rather than reading as an undeclared package or a typo.
274
+ const noIndexPackages = config ? noContentIndexPackages(config) : new Set();
271
275
 
272
276
  // The address space an `![[…]]` embed resolves against, shaped as every
273
277
  // other asset resolver reads one so the checker cannot answer an authored
@@ -464,7 +468,7 @@ export function buildLinkIndex(
464
468
  * @returns {object|undefined} The note it addresses.
465
469
  */
466
470
  function resolveAddress(target, keyPath) {
467
- const qualified = readQualifier(target, types, packages);
471
+ const qualified = readQualifier(target, types, packages, noIndexPackages);
468
472
  if (!qualified || qualified.reason) return undefined;
469
473
  // Every omitted segment defaults from where the link is written,
470
474
  // so the target expands to exactly one canonical address and this is a
@@ -501,7 +505,7 @@ export function buildLinkIndex(
501
505
  * @returns {object[]} The foreign entries, each carrying its `package`.
502
506
  */
503
507
  function foreignHits(target, keyPath) {
504
- const q = readQualifier(target, types, packages);
508
+ const q = readQualifier(target, types, packages, noIndexPackages);
505
509
  if (!q || q.reason) return [];
506
510
  // An omitted package means *this* package, so a short form
507
511
  // addresses nothing foreign and never reaches a dependency's index.
@@ -552,7 +556,7 @@ export function buildLinkIndex(
552
556
  * @returns {object|null} The note, asset record or foreign entry declaring it.
553
557
  */
554
558
  function referenceHit(target) {
555
- const q = readQualifier(target, types, packages);
559
+ const q = readQualifier(target, types, packages, noIndexPackages);
556
560
  if (!q || q.reason) return null;
557
561
  const local = matchAddress([...byKey, ...byAssetKey], q);
558
562
  if (local.length) return local[0][1];
@@ -566,6 +570,8 @@ export function buildLinkIndex(
566
570
  anchors,
567
571
  types,
568
572
  packages,
573
+ /** Packages declared `contentIndex: false`, a Foundry dependency only. */
574
+ noIndexPackages,
569
575
  /**
570
576
  * The files this package ships, by canonical address. Separate from the
571
577
  * notes because the two record shapes are read differently, and exposed
@@ -597,7 +603,7 @@ export function buildLinkIndex(
597
603
  foreignHits,
598
604
  referenceHit,
599
605
  /** Whether a target reads as a qualified address at all. */
600
- isAddress: (target) => Boolean(readQualifier(target, types, packages)),
606
+ isAddress: (target) => Boolean(readQualifier(target, types, packages, noIndexPackages)),
601
607
  };
602
608
  }
603
609
 
@@ -922,8 +928,9 @@ export function auditHomepageLinks(index) {
922
928
  * which addresses a foreign manifest answered. Each `deadAddresses` entry
923
929
  * carries a `reason` from {@link LINK_FINDING_REASONS} —
924
930
  * `"not-an-address"`, `"unknown-type"`, `"ambiguous"` (with the claiming
925
- * `packages`), or `"unresolved"` — and every one of them is an **error**:
926
- * the three resolvers agree on severity for every class.
931
+ * `packages`), `"no-content-index"`, or `"unresolved"` — and every one of
932
+ * them is an **error**: the three resolvers agree on severity for every
933
+ * class.
927
934
  */
928
935
  export function auditLinks(index) {
929
936
  const { notes, anchors, linksOf, embedsOf, resolve, manifestHit, isAddress } = index;
@@ -1004,10 +1011,13 @@ export function auditLinks(index) {
1004
1011
  });
1005
1012
  continue;
1006
1013
  }
1007
- const read = readQualifier(target, index.types, index.packages);
1014
+ const read = readQualifier(target, index.types, index.packages, index.noIndexPackages);
1008
1015
  deadAddresses.push({
1009
1016
  ...at,
1010
- reason: read?.reason === "unknown-type" ? "unknown-type" : "unresolved",
1017
+ reason:
1018
+ read?.reason === "unknown-type" ? "unknown-type"
1019
+ : read?.reason === "no-content-index" ? "no-content-index"
1020
+ : "unresolved",
1011
1021
  });
1012
1022
  }
1013
1023
  }
@@ -43,7 +43,7 @@ import { contentPackage, foundryPackageId } from "./content-package.mjs";
43
43
  import { searchableFrontmatter } from "./note-package.mjs";
44
44
  import { PACKAGE_BASE } from "./content-address.mjs";
45
45
  import { resolveNoteId } from "./note-ids.mjs";
46
- import { loadForeignIndexes } from "./metadata-index.mjs";
46
+ import { loadForeignIndexes, noContentIndexPackages } from "./metadata-index.mjs";
47
47
  // The record accessors only — deriving records reaches the pack router and the
48
48
  // manifest emitter, which reach the compilers, which load this module. Reading
49
49
  // a record needs none of that.
@@ -787,6 +787,7 @@ export function buildContentLinkIndex(
787
787
  );
788
788
  return buildWikilinkIndex(docs, resolved.foundryPackage, foreign, resolved.contentPackage, {
789
789
  assets,
790
+ noIndexPackages: noContentIndexPackages(resolved),
790
791
  });
791
792
  }
792
793
 
@@ -84,6 +84,11 @@ export const METADATA_RELATIONSHIP_KINDS = Object.freeze(["systems", "requires"]
84
84
  * and needing no items is the mirror of it. Gating the index on the catalogue
85
85
  * flag would serve neither.
86
86
  *
87
+ * **Excludes a relationship declaring `contentIndex: false`.** That opts a
88
+ * dependency out of both edges at once: it is a Foundry dependency only, cited
89
+ * by neither a wikilink nor an item reference, so there is nothing here for
90
+ * `deps fetch` to fill and no cache this build will ever read.
91
+ *
87
92
  * The declaration is the one already in the emitted `system.json` /
88
93
  * `module.json`, so it cannot drift from what Foundry itself installs, and
89
94
  * there is no new configuration key to keep in step. Each entry carries the
@@ -98,6 +103,7 @@ export function metadataRelationships(config) {
98
103
  const out = [];
99
104
  for (const kind of METADATA_RELATIONSHIP_KINDS) {
100
105
  for (const rel of config?.relationships?.[kind] ?? []) {
106
+ if (rel.contentIndex === false) continue;
101
107
  out.push({
102
108
  id: rel.id,
103
109
  manifest: rel.manifest,
@@ -109,6 +115,32 @@ export function metadataRelationships(config) {
109
115
  return out;
110
116
  }
111
117
 
118
+ /**
119
+ * Every package a relationship declares `contentIndex: false` on, keyed by
120
+ * the content package name a link into it would use.
121
+ *
122
+ * A separate set from {@link metadataRelationships}, which answers "what does
123
+ * `deps fetch` fill" — this answers "what does the link resolver recognise as
124
+ * a package with no fetched index", which a wikilink checker or pack compiler
125
+ * needs to tell that case apart from a package nobody declared at all.
126
+ *
127
+ * Walked across every relationship kind, not only the citable ones: the
128
+ * config validation refuses the flag nowhere by kind, so a resolver reading it
129
+ * back should not assume one either.
130
+ *
131
+ * @param {object} config - The resolved build configuration.
132
+ * @returns {ReadonlySet<string>} The content package names.
133
+ */
134
+ export function noContentIndexPackages(config) {
135
+ const out = new Set();
136
+ for (const entries of Object.values(config?.relationships ?? {})) {
137
+ for (const rel of entries ?? []) {
138
+ if (rel.contentIndex === false) out.add(rel.contentPackage ?? rel.id);
139
+ }
140
+ }
141
+ return Object.freeze(out);
142
+ }
143
+
112
144
  /**
113
145
  * The cache directory for one dependency's index at one version.
114
146
  *
@@ -496,6 +496,26 @@ export function hasTag(fm, tag) {
496
496
  return false;
497
497
  }
498
498
 
499
+ /**
500
+ * Whether a note carries any `tags:` at all, however authored.
501
+ *
502
+ * The one question the site build asks of tags in aggregate — whether the
503
+ * tree publishes taxonomy pages — rather than about a particular tag. Reads
504
+ * `tags` and `tag` exactly as {@link hasTag} does, and treats an empty list
505
+ * or a blank string as carrying none.
506
+ *
507
+ * @param {object|null|undefined} fm - Parsed frontmatter.
508
+ * @returns {boolean} Whether the note carries at least one tag.
509
+ */
510
+ export function hasAnyTag(fm) {
511
+ const raw = fm?.tags ?? fm?.tag;
512
+ if (raw == null) return false;
513
+ for (const entry of Array.isArray(raw) ? raw : [raw]) {
514
+ if (typeof entry === "string" && entry.trim() !== "") return true;
515
+ }
516
+ return false;
517
+ }
518
+
499
519
  /**
500
520
  * Whether a note is tagged as an unfinished **draft**.
501
521
  *
@@ -570,9 +570,22 @@ export function configFromData(data, configPath) {
570
570
  );
571
571
  }
572
572
  }
573
- const { pkg } = readPackageJson(rootDir);
573
+ const { manifestPath, pkg } = readPackageJson(rootDir);
574
574
  if (pkg.homepage !== undefined) input.homepage = pkg.homepage;
575
575
  if (pkg.author !== undefined) input.author = pkg.author;
576
+ // `description` is npm metadata nothing displays for a private package —
577
+ // read by neither the manifest nor the site, so a declared one is a
578
+ // warning rather than a silent no-op that looks like it did something.
579
+ if (pkg.description !== undefined) {
580
+ emitDiagnostic({
581
+ file: manifestPath,
582
+ severity: "warning",
583
+ message:
584
+ "`description` is read by nothing; the Foundry pitch is " +
585
+ "`packageBuild.manifest.descriptionHtml` and the site's is " +
586
+ "`site.description`",
587
+ });
588
+ }
576
589
 
577
590
  if (input.itemBuilders !== undefined) {
578
591
  const declared = input.itemBuilders;
@@ -466,8 +466,8 @@ export function compareEmittedSystem({
466
466
  * Two places to find it, because a system checks itself against source it owns
467
467
  * while a module checks against a dependency it fetched:
468
468
  *
469
- * - **A system**: its own `schema.json`, generated from its `src/` and
470
- * committed beside it.
469
+ * - **A system**: its own `schema.json`, generated from its `src/` into
470
+ * `build/` by `package-build schema`.
471
471
  * - **A module**: the copy cached by `content-build deps fetch`, from the
472
472
  * archive of the version it pins — which is what makes the comparison happen
473
473
  * at `verified` rather than against whatever the system's `main` holds today.
@@ -505,7 +505,7 @@ export function resolveSchemaArtifact(config, system = undefined) {
505
505
 
506
506
  // The system checking itself, against the schema its own build published.
507
507
  if (config.packageKind === "systems" && config.foundryPackage === systemId) {
508
- const own = path.join(config.rootDir, SCHEMA_ARTIFACT_FILE);
508
+ const own = path.join(config.rootDir, "build", SCHEMA_ARTIFACT_FILE);
509
509
  return fs.existsSync(own) ? read(own) : null;
510
510
  }
511
511
 
@@ -683,9 +683,9 @@ export function checkAuthoredSystemData(
683
683
  * subtype declares.
684
684
  *
685
685
  * The build-time face of {@link compareEmittedSystem}: it resolves the schema
686
- * the way every other check here does — the system's own committed artifact, or
687
- * the cached one from the release a module pins — and attaches the message a
688
- * reader sees.
686
+ * the way every other check here does — the system's own published artifact,
687
+ * or the cached one from the release a module pins — and attaches the message
688
+ * a reader sees.
689
689
  *
690
690
  * **Silent where there is nothing to check against**, exactly as its two
691
691
  * siblings are: a module pinning a system version released before the artifact
@@ -57,13 +57,14 @@ import { renderImageFigures } from "./content-images.mjs";
57
57
  import { pathnameProblem, resolvePathname } from "./pathnames.mjs";
58
58
  import { buildSiteIndex, resolveInfoboxRef, wikiContext } from "./site-index.mjs";
59
59
  import { frontmatterWikilinks, resolveWebWikilinks } from "./web-wikilinks.mjs";
60
- import { loadForeignIndexes } from "./metadata-index.mjs";
60
+ import { loadForeignIndexes, noContentIndexPackages } from "./metadata-index.mjs";
61
61
  import { noteInfoboxes } from "./infobox-registry.mjs";
62
62
  import { formatUnaddressableFinding, unaddressableForeignPackages } from "./metadata-index.mjs";
63
63
  import { deriveBeingInfo, isBeing } from "../sohl/being-info.mjs";
64
64
  import { loadPackConfig } from "./pack-config.mjs";
65
65
  import { routerFor } from "./pack-router.mjs";
66
66
  import { searchableFrontmatter } from "./note-package.mjs";
67
+ import { hasAnyTag } from "./note-vocabulary.mjs";
67
68
  // The corpus, from the one pass that derives it.
68
69
  import { indexRecordsFor } from "./content-index.mjs";
69
70
  import { isNoteRecord, noteFile } from "./index-records.mjs";
@@ -476,7 +477,10 @@ export function siteGates(pages, findings, { config }) {
476
477
  out.unaddressable = unaddressableForeignPackages(foreign.index);
477
478
  if (out.unaddressable.length) return out;
478
479
 
479
- const index = buildSiteIndex(pages, { foreignIndex: foreign.index });
480
+ const index = buildSiteIndex(pages, {
481
+ foreignIndex: foreign.index,
482
+ noIndexPackages: noContentIndexPackages(config),
483
+ });
480
484
  out.conflicts = index.conflicts;
481
485
  if (out.conflicts.length) return out;
482
486
 
@@ -1043,7 +1047,10 @@ export function resolveSitePass(name, options) {
1043
1047
  * `sql` directive with none prepared is a table error: nothing here runs a
1044
1048
  * query.
1045
1049
  * @returns {{gates: object, stats: object|null, tableErrors: object[],
1046
- * wikiErrors: object[], imageErrors: object[], manifests: object|null}}
1050
+ * wikiErrors: object[], imageErrors: object[], manifests: object|null,
1051
+ * hasTags: boolean}} `hasTags` is whether any note the walk read carries
1052
+ * `tags:` — what {@link module:engine/site-config.hugoConfig} reads to
1053
+ * decide whether the site emits taxonomy pages.
1047
1054
  */
1048
1055
  export function buildSite({ config, sqlTables } = {}) {
1049
1056
  const resolved = config ?? loadPackConfig();
@@ -1142,6 +1149,7 @@ export function buildSite({ config, sqlTables } = {}) {
1142
1149
  wikiErrors: [],
1143
1150
  imageErrors: [],
1144
1151
  stats: null,
1152
+ hasTags: homepages.some((p) => hasAnyTag(p.fm)),
1145
1153
  };
1146
1154
  }
1147
1155
 
@@ -1164,6 +1172,7 @@ export function buildSite({ config, sqlTables } = {}) {
1164
1172
  landings: 0,
1165
1173
  out: homeRoot,
1166
1174
  },
1175
+ hasTags: homepages.some((p) => hasAnyTag(p.fm)),
1167
1176
  };
1168
1177
  }
1169
1178
 
@@ -1211,6 +1220,7 @@ export function buildSite({ config, sqlTables } = {}) {
1211
1220
  tableErrors: [],
1212
1221
  wikiErrors: [],
1213
1222
  imageErrors: [],
1223
+ hasTags: [...pages, ...homepageEntries].some((p) => hasAnyTag(p.fm)),
1214
1224
  };
1215
1225
  }
1216
1226
 
@@ -1269,6 +1279,7 @@ export function buildSite({ config, sqlTables } = {}) {
1269
1279
  landings,
1270
1280
  out,
1271
1281
  },
1282
+ hasTags: [...pages, ...homepageEntries].some((p) => hasAnyTag(p.fm)),
1272
1283
  };
1273
1284
  }
1274
1285