@heroiclands/package-build 11.1.0 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,9 +22,9 @@
22
22
  * says what to write instead rather than which value to correct.
23
23
  *
24
24
  * `package:` is retired the same way and is refused from `note-package.mjs`,
25
- * where the concept it belonged to still lives. `draft:` and the top-level
26
- * `aliases:` have no such home — there is no surviving concept either was part
27
- * of — so they are refused here.
25
+ * where the concept it belonged to still lives. `draft:`, the top-level
26
+ * `aliases:` and `section:` have no such home — there is no surviving concept
27
+ * any of them was part of — so they are refused here.
28
28
  *
29
29
  * **What `draft:` did (#69).** It excluded a note from the compiled packs, from
30
30
  * the link manifest and from a consuming site build. Nothing reported the
@@ -41,6 +41,13 @@
41
41
  * `name.full` and so decided what a note could be named (#179). The form and
42
42
  * the index are retired together, leaving the field with no reader at all.
43
43
  *
44
+ * **What `section:` did (#202).** It named the section a `collection` note
45
+ * headed, under the `collection` landing rule — its only reader anywhere. That
46
+ * rule is retired, a section being landed by the `README.md` in its directory,
47
+ * so the field has none. No schema or vocabulary ever declared it either, and
48
+ * nothing checks unrecognized top-level keys, so left in place it would be
49
+ * silently ignored rather than reported.
50
+ *
44
51
  * **`name.aliases` fed the same index and is nonetheless kept.** It is
45
52
  * **reserved** — held for a use that does not exist yet — so it is the one
46
53
  * field here that is neither retired nor read. Nothing consults it: no index,
@@ -216,6 +223,72 @@ export function declaresRetiredAliasesField(fm) {
216
223
  return Boolean(fm) && typeof fm === "object" && Object.hasOwn(fm, "aliases");
217
224
  }
218
225
 
226
+ /**
227
+ * What a note declaring `section:` is told, in one place.
228
+ *
229
+ * Shared by the compile-time refusal and the frontmatter lint, because an
230
+ * author meets whichever of the two runs first and they should read the same.
231
+ * It names what lands a section now rather than a value to correct: no value
232
+ * makes declaring the field right.
233
+ *
234
+ * **What it did (#202).** It named the section a `collection` note headed,
235
+ * under the `collection` landing rule — the only reader it ever had, in the
236
+ * second branch of `landingOf` (`engine/content-address.mjs`). That rule went
237
+ * first, and the whole mechanism went with it (#204): a section is a Hugo
238
+ * directory the note format does not carry, so no note lands one and a page
239
+ * that introduces a type is an ordinary note addressed `doc-<type>`. Nothing
240
+ * else read the field, and no schema or vocabulary declared it, so left in
241
+ * place it would be ignored in silence — the note saying one thing and the
242
+ * build doing another.
243
+ *
244
+ * @param {string} [file] - The note's path, named in the message. Omit it where
245
+ * the caller emits through a diagnostic, whose locator already starts the
246
+ * line — repeating it prints the path twice.
247
+ * @returns {string} The message, unpunctuated at the end as a finding is.
248
+ */
249
+ export function sectionRetiredMessage(file) {
250
+ return (
251
+ "`section:` is a retired frontmatter field — delete it" +
252
+ (file ? ` — ${file}` : "") +
253
+ ". It named the section a `collection` note headed, and both the rule " +
254
+ "and the sections it routed to are retired: a page that introduces " +
255
+ "the notes of a type is an ordinary note — `type: doc`, " +
256
+ "`subType: reference`, `shortcode: <type>` — addressed `doc-<type>`. " +
257
+ "Nothing else ever read the field"
258
+ );
259
+ }
260
+
261
+ /**
262
+ * Refuse a note that declares `section:` at all.
263
+ *
264
+ * Presence is the whole test, as it is for `draft:` and `aliases:`: an empty
265
+ * value reads as "this note heads a section and names none", a statement about
266
+ * a rule that no longer exists.
267
+ *
268
+ * @param {object|null|undefined} fm - Parsed frontmatter, or nothing when it
269
+ * could not be parsed.
270
+ * @param {object} [options] - Options.
271
+ * @param {string} [options.file] - The note's path, named in the message. Omit
272
+ * it where the caller emits through a diagnostic, which puts the locator at
273
+ * the start of the line already — repeating it prints the path twice.
274
+ * @param {string} [options.absPath] - The note's file on disk, read only on the
275
+ * failing path to locate the offending line and column. The position rides on
276
+ * the thrown error as `position`, for a caller that emits a diagnostic.
277
+ * @returns {void}
278
+ * @throws {Error} When the note declares the field.
279
+ */
280
+ export function assertNoSectionField(fm, { file, absPath } = {}) {
281
+ if (!fm || typeof fm !== "object" || !Object.hasOwn(fm, "section")) return;
282
+
283
+ const err = new Error(`${sectionRetiredMessage(file)}.`);
284
+ // Anchored at column 1: `site.trees[].section` is a *configuration* key of
285
+ // the same name, and a nested `section:` inside some other block is not
286
+ // this field — a finding about the top-level one must not open on it.
287
+ const position = locateFrontmatterKey(absPath, "section", undefined, { topLevel: true });
288
+ if (position) err.position = position;
289
+ throw err;
290
+ }
291
+
219
292
  /**
220
293
  * A frontmatter key's position in a note's file, or nothing.
221
294
  *
@@ -50,7 +50,7 @@ import { createRequire } from "node:module";
50
50
  import matter from "gray-matter";
51
51
 
52
52
  import { slugify } from "./content-slug.mjs";
53
- import { addressSlug, sectionOf } from "./content-address.mjs";
53
+ import { addressSlug } from "./content-address.mjs";
54
54
  import { protectCode } from "./code-fences.mjs";
55
55
  import { expandContentTables } from "./content-tables.mjs";
56
56
  import { buildSiteIndex, wikiContext } from "./site-index.mjs";
@@ -132,11 +132,10 @@ function readNote(file) {
132
132
  * @returns {{pages: object[], addressFindings: object[], fmLinkFindings: object[]}}
133
133
  */
134
134
  export function collectContentPages(contentBase, ctx) {
135
- // Where an addressed page publishes. The section below is guarded so a note
136
- // is never "written to `undefined/`"; the same reasoning applies here, and a
137
- // missing `base` would put *every* page there rather than one. It is the
138
- // caller's contract rather than a note's defect, so it throws instead of
139
- // being collected as a finding (#195).
135
+ // Where an addressed page publishes. A missing `base` would put *every*
136
+ // page at `undefined/` rather than one, and it is the caller's contract
137
+ // rather than a note's defect, so it throws instead of being collected as a
138
+ // finding (#195).
140
139
  if (typeof ctx.base !== "string" || !ctx.base) {
141
140
  throw new TypeError(
142
141
  "collectContentPages: `ctx.base` must be a non-empty string — it is the package address every page's URL is built on",
@@ -178,20 +177,6 @@ export function collectContentPages(contentBase, ctx) {
178
177
  }
179
178
 
180
179
  const base = path.basename(file);
181
- const isReadme = base.toLowerCase() === "readme.md";
182
- const sec = sectionOf(fm);
183
- // A page's URL no longer contains its section, but the section is still
184
- // what decides the directory the file is written to — and Hugo derives
185
- // a page's section from that directory, not from its URL. So a note
186
- // with none is still a note with nowhere to be published, and is
187
- // reported rather than written to `undefined/`.
188
- if (typeof sec !== "string" || !sec) {
189
- addressFindings.push({
190
- file,
191
- reason: `type "${fm.type}" has no section, so there is nowhere to file the page`,
192
- });
193
- continue;
194
- }
195
180
  const rel = path.relative(contentBase, file);
196
181
  pages.push({
197
182
  kind: "content",
@@ -218,15 +203,12 @@ export function collectContentPages(contentBase, ctx) {
218
203
  // The immediate source subfolder, the only surviving record of the
219
204
  // authoring folder, for grouped landings.
220
205
  folder: path.basename(path.dirname(file)),
221
- sec,
222
- // A landing page **is** its section, so it is addressed by the
223
- // mount the section lives at; every other page is addressed by
224
- // `(type, shortcode)` at the package root, which takes no mount
225
- // (#181). The file is still written into `<sec>/` either way — see
226
- // {@link pageDestination} — and the front matter carries this `url`
227
- // so Hugo publishes it at its address rather than at its path.
228
- url: isReadme ? `${ctx.mount}${sec}/` : `${ctx.base}${slug}/`,
229
- isReadme,
206
+ // Every page is addressed by `(type, shortcode)` at the package
207
+ // root, which takes no content mount (#181). The file is written
208
+ // flat under the mount see {@link pageDestination} and the
209
+ // front matter carries this `url` so Hugo publishes it at its
210
+ // address rather than at its path.
211
+ url: `${ctx.base}${slug}/`,
230
212
  });
231
213
  }
232
214
  return { pages, addressFindings, fmLinkFindings };
@@ -557,12 +539,10 @@ export function sectionFrontmatter(meta) {
557
539
  * redirects of its own.
558
540
  *
559
541
  * A content page states its own **`url`**, which is its address rather than its
560
- * path (#181). Hugo would otherwise publish it where the file sits under the
561
- * mount, inside its section directory and the file sits there for a reason:
562
- * Hugo derives a page's section from its directory, which is what gives the
563
- * section its landing page, `.CurrentSection` and its per-section layout
564
- * lookup. So the directory stays and the address is stated, and the two are
565
- * free to differ.
542
+ * path (#181). It is written flat under the content mount (#204), so Hugo would
543
+ * otherwise publish it at `<mount><type>-<shortcode>/` rather than at the
544
+ * package-wide address the link manifest records the same address, one
545
+ * segment too deep. So the address is stated and the mount does not reach it.
566
546
  *
567
547
  * A content page carries the package the build **derived** (#65). No note
568
548
  * declares one — `package:` is retired (#56) — so the note's frontmatter alone
@@ -574,8 +554,12 @@ export function sectionFrontmatter(meta) {
574
554
  * self-describing and makes sweeping the field out of a content tree
575
555
  * output-preserving for a site as it already is for the packs.
576
556
  *
557
+ * A **tree** page is the one that still reads `readmeSections`: a `trees` entry
558
+ * keeps its source layout below a named section, so its own `README` is that
559
+ * section's landing and takes the title and hero the section declares.
560
+ *
577
561
  * @param {object} page - The page.
578
- * @param {object} options - `{ sections, readmeSections, decorate }`.
562
+ * @param {object} options - `{ readmeSections, decorate }`.
579
563
  * @returns {object} The frontmatter to write.
580
564
  */
581
565
  export function pageFrontmatter(page, { readmeSections = {}, decorate }) {
@@ -598,14 +582,6 @@ export function pageFrontmatter(page, { readmeSections = {}, decorate }) {
598
582
  kbfolder: page.folder,
599
583
  };
600
584
  if (decorate) decorate(data, page);
601
- if (isReadme) {
602
- const meta = readmeSections[sec];
603
- // What the section says about itself wins over what its README
604
- // happens to carry — the landing has to match the card linking to
605
- // it. Assigned rather than transcribed key by key, so a section's
606
- // vocabulary is decided in one place (#91).
607
- if (meta) Object.assign(data, sectionFrontmatter(meta));
608
- }
609
585
  } else {
610
586
  // A tree's own landing describes the *mount*, and nothing beneath it. A
611
587
  // nested README is a sub-section's landing, and reading the section's
@@ -623,23 +599,25 @@ export function pageFrontmatter(page, { readmeSections = {}, decorate }) {
623
599
  /**
624
600
  * Where a page is written, relative to the output root.
625
601
  *
626
- * **Into its section directory, which is not where it publishes** (#181). A
627
- * content page's URL is its address — `/<package>/<type>-<shortcode>/` — and it
628
- * is stated in the front matter; the file still goes to `<section>/`, because
629
- * Hugo reads a page's section from its path and nothing else. Flattening the
630
- * tree to match the URL would take the section landings, `.CurrentSection` and
631
- * every per-section layout with it.
602
+ * **Flat, under the mount, named by its address** (#204). A content page's URL
603
+ * is its address — `/<package>/<type>-<shortcode>/` — and the file is now named
604
+ * the same way, so the two agree. It used to be filed into `<section>/` so that
605
+ * Hugo would read a section off its path; a section appears in no address, and
606
+ * a directory chosen only to satisfy a rendering engine's idea of what a
607
+ * section is has no business in the note format.
608
+ *
609
+ * The name is the *whole* address rather than a section-relative half of it, so
610
+ * two types cannot fight over one file: a `doc` note's `subType` may be spelled
611
+ * the same as another note's `type`, and `doc-gear.md` and `weapongear-gear.md`
612
+ * are distinct whatever the sections used to be.
632
613
  *
633
- * The filename is the address rather than the section-relative half of it, so
634
- * two sections cannot fight over one file: a `doc` note routes by its `subType`,
635
- * which may be spelled the same as another note's `type`.
614
+ * **A `trees` entry is the exception, and always was.** Those pages preserve
615
+ * their source layout below a named section they are a book with chapters,
616
+ * addressed by their path so a `README` there is still its directory's
617
+ * `_index.md`.
636
618
  */
637
619
  export function pageDestination(page) {
638
- if (page.kind === "content") {
639
- return page.isReadme ?
640
- path.join(page.sec, "_index.md")
641
- : path.join(page.sec, `${page.slug}.md`);
642
- }
620
+ if (page.kind === "content") return `${page.slug}.md`;
643
621
  const rel =
644
622
  page.isReadme ? path.posix.join(path.posix.dirname(page.rel), "_index.md") : page.rel;
645
623
  return path.join(page.sec, rel);
@@ -681,7 +659,11 @@ export function renderPages(pages, options) {
681
659
  const byKind = {};
682
660
 
683
661
  for (const page of pages) {
684
- const src = page.rel ?? `${page.sec}/${page.base}`;
662
+ // The page's path in the tree an author edits: below the content root
663
+ // for a content note, below the tree's own root for a `trees` page. It
664
+ // used to be composed as `<section>/<basename>` for a content note,
665
+ // which named a directory that was never the note's (#204).
666
+ const src = page.relPath ?? page.rel ?? page.base;
685
667
  const ctx = wikiContext(index, {
686
668
  src,
687
669
  file: page.file,
@@ -724,26 +706,43 @@ export function renderPages(pages, options) {
724
706
  }
725
707
 
726
708
  /**
727
- * Writes the section landings a published tree needs but no note supplies.
709
+ * Writes the Hugo sections a published tree declares.
710
+ *
711
+ * **This is where a section lives now, and the only place** (#204). A content
712
+ * note carries none: it is addressed by `(type, shortcode)` and emitted flat
713
+ * under the mount, so nothing a page does creates a directory. A site that wants
714
+ * `/<package>/<prefix><section>/` to answer — with a title, a hero, and whatever
715
+ * listing its layout builds — says so here, in configuration, and this writes
716
+ * the `_index.md` that makes Hugo agree it is a section.
728
717
  *
729
- * Two separate jobs, and both exist because of how Hugo decides what a section
730
- * is:
718
+ * Three jobs, all of them Hugo's directory semantics rather than the note
719
+ * format's:
731
720
  *
721
+ * - **The mount's own landing**, so `/<package>/<prefix>` is a page rather than
722
+ * a directory listing. It carries a `type` of its own: Hugo's template lookup
723
+ * walks up a page's path, so an untyped landing template at the mount would
724
+ * also serve every section below it that has none.
732
725
  * - **Declared sections** get a titled `_index.md` with their hero, so a landing
733
726
  * matches the card that links to it instead of showing Hugo's auto-humanised
734
- * directory name. The body is empty, which lets the theme list the section's
735
- * children or say it is empty, for a section whose content has not shipped.
736
- * - **Every other section directly under the mount** gets a bare `_index.md`,
727
+ * directory name. The body is empty, which lets the theme decide what to list.
728
+ * - **Every other directory directly under the mount** gets a bare `_index.md`,
737
729
  * or its own address publishes nothing. Hugo generates a section page
738
730
  * automatically only for a *top-level* content directory; below that, a
739
- * directory without an `_index.md` is not a section, so its URL 404s while
740
- * its children publish normally. Mounting a tree one level down demotes every
741
- * section it holds, and the ones with no landing of their own quietly stop
742
- * existing while every page inside them keeps working.
731
+ * directory without an `_index.md` is not a section, so its URL 404s while its
732
+ * children publish normally. With content pages flat, what that reaches is a
733
+ * `trees` entry's directory the one thing left below the mount that a note
734
+ * creates.
735
+ *
736
+ * **A section listing is not a page listing any more.** A layout that reads
737
+ * `.Pages` off a section it declares here will find nothing, because no file is
738
+ * filed into it; one that queries `site.RegularPages` by `Params.type` — which
739
+ * is how `sohl`'s eleven catalog layouts already work — is unaffected. That is a
740
+ * consumer's layout to choose, and it is stated here because the choice is no
741
+ * longer free.
743
742
  *
744
743
  * Scoped to one level on purpose. A directory further down was not a section
745
- * before the move either, and giving it one here would silently re-scope the
746
- * prev/next navigation of every page inside it.
744
+ * before either, and giving it one here would silently re-scope the prev/next
745
+ * navigation of every page inside it.
747
746
  *
748
747
  * @param {string} outRoot - The mount directory.
749
748
  * @param {object} options - `{ sections, landing, sectionTitle }`.
@@ -752,11 +751,6 @@ export function renderPages(pages, options) {
752
751
  export function writeSectionLandings(outRoot, { sections = {}, landing, sectionTitle }) {
753
752
  let written = 0;
754
753
 
755
- // The mount's own landing carries a `type` of its own. Hugo's template
756
- // lookup walks up a page's path, so a landing template at the mount would
757
- // also serve every section below it that has no template of its own —
758
- // each would render the mount's front page. Typing the landing moves its
759
- // template out of the path where it could be inherited.
760
754
  if (landing) {
761
755
  fs.mkdirSync(outRoot, { recursive: true });
762
756
  fs.writeFileSync(path.join(outRoot, "_index.md"), matter.stringify("", landing));
@@ -1024,9 +1018,7 @@ export function buildSite({ config, outRoot } = {}) {
1024
1018
  name: page.fm.name?.full ?? homepageTitle(page.fm, resolved),
1025
1019
  slug: addressSlug(page.fm),
1026
1020
  base: path.basename(page.file),
1027
- sec: sectionOf(page.fm),
1028
1021
  url: `${base}${addressSlug(page.fm)}/`,
1029
- isReadme: false,
1030
1022
  }));
1031
1023
 
1032
1024
  const trees = site.trees.map((t) => ({
@@ -65,10 +65,15 @@ import { isDraftNote } from "./note-vocabulary.mjs";
65
65
  * @property {object} fm The note's frontmatter.
66
66
  * @property {string} name Display name.
67
67
  * @property {string} slug URL segment.
68
- * @property {string} sec Section the page is filed under.
68
+ * @property {string} [sec] The Hugo section a **tree** page is filed under, and
69
+ * the first segment of the `<sec>/<slug>` address it
70
+ * is reachable by. A content page has none: it is
71
+ * addressed by `(type, shortcode)` and emitted flat
72
+ * (#204).
69
73
  * @property {string} base Source file's basename, e.g. `Climbing.md`.
70
74
  * @property {string} url The page's published address.
71
- * @property {boolean} isReadme Whether the page is its section's landing.
75
+ * @property {boolean} [isReadme] Whether a tree page is its directory's
76
+ * landing.
72
77
  */
73
78
 
74
79
  /**
@@ -169,13 +174,20 @@ export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
169
174
  const ownPackage = contentPackage();
170
175
  const packages = new Set(ownPackage ? [ownPackage] : []);
171
176
 
172
- // `section/slug` is unique by construction. A page's name, filename and
173
- // bare slug were indexed here too, as collision-aware fallbacks the bare
174
- // `[[Name]]` form looked up; that form is retired and nothing consults
175
- // them, so they are gone and with them the rule that two pages of a type
176
- // may not share a name (#179, #180).
177
+ // `section/slug` is unique by construction, and is now a **tree** page's
178
+ // address: a `trees` entry keeps its source layout below a named section,
179
+ // so `dev-docs/testing` is how one is cited. A content page carries no
180
+ // section at all (#204) and is addressed by `(type, shortcode)` below
181
+ // indexing it here as well would have written `weapongear/weapongear-dagger`,
182
+ // a key no author could reasonably write.
183
+ //
184
+ // A page's name, filename and bare slug were indexed here too, as
185
+ // collision-aware fallbacks the bare `[[Name]]` form looked up; that form is
186
+ // retired and nothing consults them, so they are gone and with them the rule
187
+ // that two pages of a type may not share a name (#179, #180).
177
188
  for (const e of entries) {
178
- sections.add(String(e.sec).toLowerCase());
189
+ if (typeof e.sec !== "string" || !e.sec) continue;
190
+ sections.add(e.sec.toLowerCase());
179
191
  // `draft` rides on every key a page is addressable by, because a link
180
192
  // into a draft note renders marked whichever of them the author wrote
181
193
  // (#183). It decides nothing about resolution: the page is indexed and
@@ -28,7 +28,7 @@
28
28
  * pack build together.
29
29
  *
30
30
  * The KB *section* is not always the type: prose pages (`type: doc`) route by
31
- * their `category`, so `doc/quickstart` lands on `/user-guide/sohl-quickstart/`.
31
+ * their `category`, so `doc/quickstart` lands on `/userguide/sohl-quickstart/`.
32
32
  * The caller supplies that mapping already resolved, in the index it builds.
33
33
  *
34
34
  * Lives here rather than in a consumer so every package resolves a link the
@@ -36,7 +36,7 @@
36
36
  * Nothing narrower than `(type, shortcode)` is consulted — a note's directory
37
37
  * and its `category` play no part in resolution — and nothing wider: a note's
38
38
  * *name* is not an address, so two notes of a type may share a display name
39
- * ("Gear" as a rules page and as a user-guide page) with nothing to disambiguate
39
+ * ("Gear" as a rules page and as a user guide page) with nothing to disambiguate
40
40
  * (#179, #180).
41
41
  *
42
42
  * At compile time each becomes a Foundry UUID enricher, routed to the pack that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "11.1.0",
3
+ "version": "13.0.0",
4
4
  "description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
@@ -14,30 +14,6 @@ export function publishesContentPages(config: {
14
14
  site: SiteMode;
15
15
  };
16
16
  }): boolean;
17
- /**
18
- * Every URL section this repository names, in declaration order (#197).
19
- *
20
- * A section is *declared* by describing it: `site.sections` for one whose
21
- * landing this build generates, `site.readmeSections` for one whose landing is
22
- * a `README`. Between them they are the open set of addresses a repository
23
- * says it publishes under — which is what a `README` landing's `subType` names,
24
- * since `sectionOf` reads that field as the section rather than as a genre.
25
- *
26
- * Read from the same two maps the site build renders each landing from, so the
27
- * set a note is checked against and the set a landing is written from cannot
28
- * come to disagree. A repository that describes no section declares none, and
29
- * the answer is empty rather than a guess assembled from the tree.
30
- *
31
- * @param {{site: {sections: object, readmeSections: object}}} config - A
32
- * resolved configuration.
33
- * @returns {readonly string[]} The section names, deduplicated.
34
- */
35
- export function declaredSections(config: {
36
- site: {
37
- sections: object;
38
- readmeSections: object;
39
- };
40
- }): readonly string[];
41
17
  /**
42
18
  * Validate and normalize a content configuration.
43
19
  *
@@ -80,28 +56,36 @@ export namespace DEFAULT_PATHS {
80
56
  */
81
57
  export const PACK_DOCUMENT_TYPES: readonly ["Actor", "Adventure", "Item", "JournalEntry", "Macro", "Scene"];
82
58
  /**
83
- * The landing-page rules a repository may route by.
59
+ * The landing-page rules a repository may route by. **Inert since #204.**
84
60
  *
85
- * A *landing page* is a note that addresses a whole section rather than a page
86
- * within one, so it has no slug of its own. Which note that is differs between
87
- * repositories, and both live rules are represented here because switching
88
- * either one on for the other repository moves addresses that are already
89
- * published:
61
+ * A *landing page* was a note that addressed a whole section rather than a page
62
+ * within one, so it had no slug of its own. There are no sections in the note
63
+ * format any more a section is a Hugo content directory, and a page's address
64
+ * names no directory so there are no landings and this selects nothing.
90
65
  *
91
- * - `readme` a `README.md` **is** its section's landing page. This is `sohl`'s
92
- * rule, and a `doc` note routes by its `category` like any other, so `sohl`'s
93
- * eleven `category: collection` notes publish under a literal `collection/`
94
- * section (`kb/collection/skills/`).
95
- * - `collection` a `doc` note whose `category` is `collection` addresses the
96
- * section it introduces, named by its authored `section`. This is `thalorna`'s
97
- * rule, under which the same note publishes at `creature/`.
98
- *
99
- * The two are not disjoint and cannot simply both apply: each tree holds notes
100
- * the other rule would move.
66
+ * The key survives its own mechanism on purpose. Both publishing consumers
67
+ * declare `landing: readme`, which stated something true when they wrote it;
68
+ * refusing it now would break them over a correct statement, and silently
69
+ * ignoring an unknown value would be worse. So `readme` stays accepted, the
70
+ * retired `collection` stays refused by name (below), and the key is deleted
71
+ * once no configuration writes it `content-config.mjs` has no warning channel
72
+ * with which to say "accepted, and does nothing" in between.
101
73
  *
102
74
  * @type {readonly string[]}
103
75
  */
104
76
  export const LANDING_RULES: readonly string[];
77
+ /**
78
+ * What a configuration naming the retired `collection` landing rule is told.
79
+ *
80
+ * A retired *value* is refused the way a retired *field* is (see
81
+ * `engine/retired-fields.mjs`): left merely unrecognized it would be reported
82
+ * as a bad value, which names something to correct and leaves the author to
83
+ * work out for themselves that the mechanism is gone. The message says the rule
84
+ * is retired, what lands a section instead, and what to do with the key.
85
+ *
86
+ * @type {Readonly<Record<string, string>>}
87
+ */
88
+ export const RETIRED_LANDING_RULES: Readonly<Record<string, string>>;
105
89
  /**
106
90
  * A repository's address scheme, with the defaults an unconfigured one gets.
107
91
  *
@@ -111,6 +95,8 @@ export const LANDING_RULES: readonly string[];
111
95
  * own mount point: where the package itself is served is the consuming build's
112
96
  * knowledge, held in `PACKAGE_BASE` (`engine/kb-manifest.mjs`) and prefixed at
113
97
  * resolve time, so it is never recorded here (#1465).
98
+ *
99
+ * `landing` is inert — see {@link LANDING_RULES}.
114
100
  */
115
101
  export const DEFAULT_ADDRESS_SCHEME: Readonly<{
116
102
  prefix: "";
@@ -530,7 +516,9 @@ export type AddressSchemeInput = {
530
516
  */
531
517
  prefix?: string | undefined;
532
518
  /**
533
- * Which note addresses a whole section.
519
+ * Which note addressed a whole section. Inert
520
+ * since #204 retired sections from the note format — see
521
+ * {@link LANDING_RULES}.
534
522
  */
535
523
  landing?: string | undefined;
536
524
  };
@@ -1,14 +1,3 @@
1
- /**
2
- * The URL section a note routes to.
3
- *
4
- * A `doc` is narrative content whose only identity is its subtype label, so it
5
- * routes by `subType`; every other type names its own section.
6
- *
7
- * @param {object} fm - Parsed frontmatter.
8
- * @returns {string|undefined} The section, or `undefined` when the note has
9
- * none — a `doc` with no subtype has no address and is not published.
10
- */
11
- export function sectionOf(fm: object): string | undefined;
12
1
  /**
13
2
  * The single path segment a note is addressed by: `type-shortcode`.
14
3
  *
@@ -34,15 +23,16 @@ export function addressSlug(fm: object): string;
34
23
  /**
35
24
  * A note's address below the knowledgebase mount, e.g. `affliction-aconite/`.
36
25
  *
37
- * A `README.md` **is** its section's landing page rather than a page within it,
38
- * so it addresses the section itself and has no address of its own.
26
+ * Every note, without exception. A `README.md` used to be its section's landing
27
+ * page and to address the section instead of itself; a section is a Hugo
28
+ * directory concept the note format no longer carries (#204), so a file's name
29
+ * decides nothing about where it publishes.
39
30
  *
40
31
  * @param {object} fm - Parsed frontmatter.
41
- * @param {boolean} isReadme - Whether the file is a `README.md`.
42
32
  * @returns {string} The mount-relative address, with a trailing slash.
43
33
  * @throws {Error} When the note has no address.
44
34
  */
45
- export function contentAddress(fm: object, isReadme: boolean): string;
35
+ export function contentAddress(fm: object): string;
46
36
  /**
47
37
  * A note's address relative to its **package**, e.g. `affliction-aconite/`.
48
38
  *
@@ -51,36 +41,34 @@ export function contentAddress(fm: object, isReadme: boolean): string;
51
41
  * address the site does not publish resolves at build time and 404s for the
52
42
  * reader, which is the failure this module exists to prevent.
53
43
  *
44
+ * **It is a pure function of the frontmatter.** Nothing about the file the note
45
+ * was read from reaches it: the `README.md` convention that made one note
46
+ * address a whole section is retired with the section itself (#204), so there
47
+ * is one rule and no branch.
48
+ *
54
49
  * **The prefix does not apply to a page's own address.** `prefix` says where the
55
- * content tree *mounts inside the package* — where its section directories and
56
- * their landing pages live — and a landing page is addressed by that mount
57
- * (`kb/rules/`). An ordinary page is addressed by `(type, shortcode)`, which is
58
- * a package-wide identity and takes no mount: `sohl` publishes
59
- * `/sohl/affliction-aconite/` while its section landings stay at
60
- * `/sohl/kb/affliction/`. The `type-` half is what keeps that flat namespace
61
- * clear of the package's fixed mounts — `/<package>/` for the landing,
50
+ * content tree *mounts inside the package* — the Hugo directory its pages are
51
+ * written under — and an address is `(type, shortcode)`, a package-wide identity
52
+ * that takes no mount: `sohl` publishes `/sohl/affliction-aconite/` from a file
53
+ * written under `kb/`. The `type-` half is what keeps that flat namespace clear
54
+ * of the package's fixed mounts `/<package>/` for the landing,
62
55
  * `/<package>/api/` for generated API docs, neither of which contains a hyphen
63
56
  * or names a type.
64
57
  *
65
- * **The section still decides where the *file* is written**, which is why a
66
- * note without one still has no address: Hugo derives a section from a page's
67
- * directory rather than from its URL, so a page with nowhere to be filed is a
68
- * page with no section landing, no `.CurrentSection` and no per-section layout.
69
- *
70
58
  * @param {object} fm - Parsed frontmatter.
71
59
  * @param {object} [options] - Options.
72
- * @param {boolean} [options.isReadme] - Whether the file is a `README.md`.
73
60
  * @param {{prefix?: string, landing?: string}} [options.scheme] - The
74
61
  * repository's address scheme; defaults to {@link DEFAULT_ADDRESS_SCHEME}.
62
+ * `landing` is validated against {@link LANDING_RULES} and selects nothing —
63
+ * it is accepted so a configuration declaring the still-true `landing: readme`
64
+ * keeps loading, and is removed once none does.
75
65
  * @returns {string} The package-relative address, with a trailing slash and no
76
66
  * leading one.
77
- * @throws {Error} When the note has no address no section, a landing page
78
- * naming no section, or no shortcode to be addressed by. Each is a note that
79
- * is not published, and inventing an address for one would put a dead entry
80
- * in the manifest.
67
+ * @throws {Error} When the note has no type or no shortcode to be addressed by.
68
+ * Such a note is not published, and inventing an address for one would put a
69
+ * dead entry in the manifest.
81
70
  */
82
- export function packageAddress(fm: object, { isReadme, scheme }?: {
83
- isReadme?: boolean | undefined;
71
+ export function packageAddress(fm: object, { scheme }?: {
84
72
  scheme?: {
85
73
  prefix?: string;
86
74
  landing?: string;