@heroiclands/package-build 10.0.1 → 11.1.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 (47) hide show
  1. package/CHANGELOG.md +375 -0
  2. package/CONTENT.md +234 -71
  3. package/MIGRATING.md +64 -0
  4. package/bin/content-build.mjs +79 -75
  5. package/content-config.mjs +28 -0
  6. package/docs/content-format.md +90 -67
  7. package/engine/base-compiler.mjs +7 -1
  8. package/engine/content-address.mjs +71 -18
  9. package/engine/content-format-check.mjs +1 -1
  10. package/engine/content-links.mjs +93 -112
  11. package/engine/content-lint.mjs +14 -10
  12. package/engine/content-slug.mjs +39 -105
  13. package/engine/diagnostics.mjs +16 -2
  14. package/engine/frontmatter-lint.mjs +161 -18
  15. package/engine/helpers.mjs +31 -68
  16. package/engine/homepage.mjs +131 -86
  17. package/engine/index.mjs +2 -5
  18. package/engine/manifest-emit.mjs +23 -4
  19. package/engine/note-vocabulary.mjs +58 -1
  20. package/engine/retired-fields.mjs +117 -6
  21. package/engine/site-build.mjs +182 -59
  22. package/engine/site-index.mjs +57 -102
  23. package/engine/web-wikilinks.mjs +183 -127
  24. package/engine/wikilink-syntax.mjs +174 -34
  25. package/engine/wikilinks.mjs +159 -117
  26. package/package.json +1 -1
  27. package/types/content-config.d.mts +24 -0
  28. package/types/engine/base-compiler.d.mts +1 -1
  29. package/types/engine/content-address.d.mts +46 -14
  30. package/types/engine/content-links.d.mts +13 -17
  31. package/types/engine/content-slug.d.mts +11 -48
  32. package/types/engine/diagnostics.d.mts +14 -1
  33. package/types/engine/frontmatter-lint.d.mts +27 -2
  34. package/types/engine/helpers.d.mts +4 -3
  35. package/types/engine/homepage.d.mts +96 -60
  36. package/types/engine/index.d.mts +0 -1
  37. package/types/engine/note-vocabulary.d.mts +43 -0
  38. package/types/engine/retired-fields.d.mts +78 -1
  39. package/types/engine/site-build.d.mts +70 -17
  40. package/types/engine/site-index.d.mts +19 -21
  41. package/types/engine/web-wikilinks.d.mts +29 -28
  42. package/types/engine/wikilink-syntax.d.mts +126 -40
  43. package/types/engine/wikilinks.d.mts +29 -24
  44. package/engine/abbreviations.mjs +0 -0
  45. package/engine/alias-index.mjs +0 -153
  46. package/types/engine/abbreviations.d.mts +0 -44
  47. package/types/engine/alias-index.d.mts +0 -122
package/CONTENT.md CHANGED
@@ -343,7 +343,7 @@ consumer keeps it somewhere else.
343
343
  **The configuration is resolved on first read, never at import.** Every module
344
344
  here can be imported — and `content-build --version` and `--help` answered — in a
345
345
  directory with no `package-build.config.yaml` and no Foundry package manifest, so
346
- a consumer can reach for one pure helper (`engine/content-slug`,
346
+ a consumer can reach for one pure helper (`engine/content-address`,
347
347
  `engine/wikilinks`) without standing up a pack build. Anything derived from
348
348
  configuration is therefore an accessor rather than a hoisted constant —
349
349
  `loadPackConfig()`, `contentPackage()`, `foundryPackageId()`, `itemTypes()`,
@@ -666,7 +666,7 @@ each finding in the located form below:
666
666
  - **Uniqueness** — `(type, shortcode)` names one note. A document is addressed
667
667
  across _every_ pack of its document type, so routing two same-address notes to
668
668
  different packs with `pack:` does not separate them.
669
- - **The package's own address** — exactly one note claims `/<package>/`. See
669
+ - **One front page** — exactly one note declares `type: homepage`. See
670
670
  [Exactly one homepage](#exactly-one-homepage) below.
671
671
 
672
672
  It compiles nothing, opens no LevelDB and needs no Foundry manifest, so it runs
@@ -675,11 +675,12 @@ rather than passing: "every one of nothing is unique" is a vacuous pass, and it
675
675
  is exactly what a tree that failed to check out produces.
676
676
 
677
677
  What that guard reports is an **empty walk**, not an empty set of addresses. A
678
- note may be keyless by design — a homepage carries no `shortcode`, because it is
679
- addressed by the package rather than by a slug so a package in
680
- `publish.site: homepage` mode has a content tree that is populated, correct and
681
- permanently unkeyed. That tree passes; a tree holding no notes at all still
682
- fails.
678
+ note may be keyless — a folder document carries no `shortcode` so a tree of
679
+ them is populated, correct and unkeyed. That tree passes; a tree holding no
680
+ notes at all still fails. (The homepage used to be the headline example, because
681
+ it was addressed by the package rather than by a slug. It carries an address
682
+ like every other note now (#182); the guard is unchanged, because what it reads
683
+ was never the key count.)
683
684
 
684
685
  ### Exactly one homepage
685
686
 
@@ -690,11 +691,15 @@ package whose front page is not the page a person chose.
690
691
  - _Zero_ and the package serves nothing at `/<package>/` — the failure the
691
692
  authored homepage exists to prevent, and a silent one: the site build reports
692
693
  `wrote 0 homepage(s)` and exits 0.
693
- - _Two_ and it serves a page nobody chose. Every homepage is written to the same
694
- `_index.md`, so the second overwrites the first and the front page is decided
695
- by the order the walk reached the files in by _filename_, on a type whose
696
- whole point is that it is routed by frontmatter. There is no "first wins"
697
- convention to fall back on, so nothing can pick the right one.
694
+ - _Two_ and it serves a page nobody chose. **This is a cardinality rule, and
695
+ since #182 it is only that.** It used to rest on the fixed destination every
696
+ homepage shared the second overwrote the firstso the address rule
697
+ enforced it as a side effect. A homepage is written at its own address now, so
698
+ two of them publish two pages and collide over nothing: the duplicate-address
699
+ check catches only the pair that happen to share a shortcode, and says nothing
700
+ at all about a `homepage-root` beside a `homepage-front`. Which of the two the
701
+ redirect at `/<package>/` should name is a question nothing here can answer,
702
+ and both being reachable is not an answer to it.
698
703
 
699
704
  Neither has a safe default, so neither is a warning: a build that proceeded past
700
705
  either would publish the wrong front page while reporting success, which is
@@ -720,7 +725,7 @@ naming the other, because each note is a place an author has to open and edit:
720
725
 
721
726
  ```text
722
727
  assets/content: error: holds no `type: homepage` note, so package "sohl" publishes nothing at its own address /sohl/ — a package's front page is one authored note in this tree, routed by `type:` rather than by filename
723
- assets/content/homepage.md:3:7: error: duplicate `type: homepage` note, also declared by assets/content/Landing.md; a package has one front page, at /sohl/, and every homepage is written to the same `_index.md` — so the one the walk reaches last silently overwrites the rest
728
+ assets/content/homepage.md:3:7: error: duplicate `type: homepage` note, also declared by assets/content/Landing.md; a package has one front page, at /sohl/, and each of these publishes at an address of its own — so nothing here can say which one that address should redirect to. Keep one, and make the rest ordinary notes
724
729
  ```
725
730
 
726
731
  ### Frontmatter, against the schema its type declares
@@ -805,54 +810,154 @@ does not exist. Removing it was verified output-neutral first: across 1,735
805
810
  stripped notes, `package compile` produced byte-identical `build/packs-json` and
806
811
  the site build byte-identical `site/content`.
807
812
 
808
- ### The homepage carries no address of its own
809
-
810
- A note's URL derives from `name.full` and its identity from
811
- `(type, shortcode)`. The homepage is the one page for which neither holds: it
812
- publishes at `/<package>/`, fixed by the package id. So `content-build lint`
813
- **refuses** `name`, `shortcode` and `id` on a `type: homepage` note (#53) rather
814
- than ignoring them — an author fluent in the conventions writes them here
815
- expecting exactly what they do everywhere else, and gets none of it.
816
-
817
- They were never inert, which is why ignoring them was the wrong answer. A
818
- `shortcode` puts the note in the address index and in the `dataview` link
819
- universe, so `[[homepage-<shortcode>]]` resolves _green_ to `homepage/<slug>/`,
820
- an address derived from `name.full` that the site build never writes, because a
821
- homepage goes to `_index.md` at the package root. A build reporting a live link
822
- to a 404 is worse than one saying nothing. It also inflates this command's own
823
- address tally, so the lint and the link manifest disagree about what the package
824
- publishes: SoHL's tree reports `1607 address(es) across 1607 note(s)` with a
825
- `shortcode` on its landing and `1606 address(es) across 1607 note(s)` without
826
- one. That is one defect, not two — the tally is only ever printed on a clean run,
827
- so refusing the field is what makes the count honest.
828
-
829
- Each finding is located at the offending key and says what the field would have
830
- decided, not merely that it does not belong:
813
+ **And the top-level field itself is now retired (#180).** `aliases:` fed the
814
+ alias index, which is what a bare `[[Alias]]` was looked up in. That form
815
+ resolved to nothing anywhere in the corpus, while the collision rule guarding it
816
+ folded in every note's `name.full` and so decided what a note could be named
817
+ (#179). Both are gone: every wikilink is an address, written
818
+ `[[type-shortcode|Text]]`, and declaring `aliases:` is refused naming the file
819
+ and the line.
820
+
821
+ **`name.aliases` is kept, and is read by nothing.** It fed the same index and
822
+ lost the same reader, but unlike the top-level list it is **reserved** held
823
+ for a use that does not exist yet. So it is the one field in the format that is
824
+ neither retired nor consulted: no index folds it in, no rule validates it,
825
+ nothing derives a name, address or URL from it, and no build branches on it. A
826
+ note carrying one compiles, resolves and addresses exactly as the same note
827
+ without it; it rides through into a page's emitted front matter untouched,
828
+ because the emitter spreads a note's frontmatter wholesale and stripping it
829
+ there would mean referencing it. Write it if you have a use for it later —
830
+ nothing today will read it.
831
+
832
+ ### The homepage is addressed like every other note
833
+
834
+ A homepage declares a `shortcode` conventionally `root` and publishes at its
835
+ address, `/<package>/homepage-root/`, written by the same rule as everything
836
+ else (#182). So `[[homepage-root|Read the introduction]]` is an ordinary
837
+ wikilink, resolving to the page the build actually writes.
838
+
839
+ It did not use to be. A page's URL derived from `name.full` while a homepage's
840
+ destination was fixed at `_index.md`, so `content-build lint` **refused** `name`
841
+ and `shortcode` on one (#53) — not out of tidiness, but because they were not
842
+ inert. A `shortcode` put the note in the address index, so
843
+ `[[homepage-<shortcode>]]` resolved _green_ to a page the site build never
844
+ wrote, and a build reporting a live link to a 404 is worse than one saying
845
+ nothing.
846
+
847
+ [A page's URL is its address](#a-pages-url-is-its-address) removed the premise:
848
+ the address a `shortcode` computes is now the address the build publishes. Both
849
+ fields are therefore permitted, and `shortcode` is **required**, like every
850
+ other note's. A homepage that declares none is refused, located at the `type:`
851
+ value that makes it necessary:
831
852
 
832
853
  ```text
833
- assets/content/homepage.md:26:1: error: `shortcode` decides nothing on a `type: homepage` note: this page's address is the package's own, `/<package>/`, fixed by the package id. It is not ignored either it puts the note in the address index, so `[[homepage-<shortcode>]]` resolves to a page the site build never writes. Delete it
834
- assets/content/homepage.md:28:1: error: `name` decides nothing on a `type: homepage` note: a page's slug derives from `name.full`, and a homepage's destination is fixed — it is written to `_index.md` at the package's own address, `/<package>/`. Write `title:` for what the page is called, and delete `name`
854
+ assets/content/homepage.md:3:7: error: a `type: homepage` note declares a `shortcode`, like every other note: it is addressed as `homepage-<shortcode>` and published at `/<package>/homepage-<shortcode>/`, which is where `[[homepage-<shortcode>|Text]]` lands. Write `shortcode: root` the package landing is `homepage-root` in every package
835
855
  ```
836
856
 
837
- **A named class, not an allow-list.** The documented envelope is `type` plus an
838
- optional `title`, with `landing`, `description` and `banner` legitimate beside
839
- them but an unknown top-level key is **not** refused, and that boundary is the
840
- decision rather than an omission. A homepage's frontmatter is emitted into the
841
- published page, so an unrecognised key is a Hugo or theme parameter this build
842
- has never heard of and has no standing to reject; a closed list would make every
843
- new theme parameter wait on a package-build release. What is refused is the
844
- specific class that makes a false claim about _where this page is_. `aliases` is
845
- not in that class either: it is already dropped from every emitted page, so
846
- authoring one here is the same no-op it is anywhere else.
857
+ `root` is a **convention, not a rule**: the address only has to be unique within
858
+ the package, which `(type, shortcode)` already guarantees, and nothing here
859
+ knows better than an author what their landing is called. What the convention
860
+ buys is one spelling shared by all six trees, so `[[homepage-root|…]]` is the
861
+ same link in every package.
862
+
863
+ **Not a bare `[[homepage]]`.** With no hyphen it does not parse as an address,
864
+ so it would need a hardcoded single-token exception in the grammar the one
865
+ thing the addressing work removes.
866
+
867
+ #### `id` is still refused
868
+
869
+ One field is left in the class, and on ground the change does not touch: `id` is
870
+ the Foundry document id a compendium UUID is built from, and a homepage compiles
871
+ into **no document**.
872
+
873
+ ```text
874
+ assets/content/homepage.md:4:1: error: `id` decides nothing on a `type: homepage` note: it is the Foundry document id a compendium UUID is built from, and a homepage compiles into no document — it appears in no pack and in no link manifest. Delete it
875
+ ```
876
+
877
+ That is also why a homepage stays **out of the link manifest**, now that a
878
+ shortcode alone would put it in. A manifest entry is how another package
879
+ resolves a _document_; a cross-package link to a package's front page is its
880
+ bare `/<package>/` address, which needs no index.
881
+
882
+ **A named class, not an allow-list.** The documented envelope is `type` and
883
+ `shortcode`, with `name`, `title`, `landing`, `description` and `banner`
884
+ legitimate beside them — but an unknown top-level key is **not** refused, and
885
+ that boundary is the decision rather than an omission. A homepage's frontmatter
886
+ is emitted into the published page, so an unrecognised key is a Hugo or theme
887
+ parameter this build has never heard of and has no standing to reject; a closed
888
+ list would make every new theme parameter wait on a package-build release.
889
+ `aliases` is not in the class either — it is a retired field now, refused on
890
+ every note whatever its type (#180).
847
891
 
848
892
  **Where it fires: `content-build lint` only.** Unlike a rule about the shape of
849
893
  the _tree_, which the site build has its own reason to gate on, this is a
850
- _frontmatter-schema_ rule and `content-build site` runs none of them — wiring in one type's field
851
- rule would have the site build refuse `shortcode` on a homepage while accepting
852
- `weight: heavy` on a weapon. The gap that leaves is `HarnMaster-3-FoundryVTT`,
853
- which runs no `content-build lint` at all and so receives no frontmatter finding
854
- of any kind; that is a missing script in that repository, not a rule to duplicate
855
- one at a time.
894
+ _frontmatter-schema_ rule and `content-build site` runs none of them — wiring in
895
+ one type's field rule would have the site build refuse `id` on a homepage while
896
+ accepting `weight: heavy` on a weapon. The site build does refuse a homepage it
897
+ cannot address, because it cannot write the page otherwise, and it reports that
898
+ beside the count so the finding reaches `publish.site: homepage` mode as well.
899
+ The remaining gap is `HarnMaster-3-FoundryVTT`, which runs no `content-build
900
+ lint` at all and so receives no frontmatter finding of any kind; that is a
901
+ missing script in that repository, not a rule to duplicate one at a time.
902
+
903
+ ### `/<package>/` is a redirect the package authors
904
+
905
+ Nothing is written at `/<package>/` any more. The package's own address is a
906
+ **routing fact**, and it belongs in the package's own `_redirects`:
907
+
908
+ ```text
909
+ # _redirects
910
+ /sohl/ /sohl/homepage-root/ 301
911
+ /sohl /sohl/homepage-root/ 301
912
+ ```
913
+
914
+ Both forms, because Cloudflare Pages matches the raw path: redirect matching
915
+ runs before any trailing-slash or `index.html` handling, so `/sohl` and `/sohl/`
916
+ are distinct keys and a rule on one does not catch the other. A redirect also
917
+ **wins over a static asset at the same path** — _"Redirects are always followed,
918
+ regardless of whether or not an asset matches the incoming request"_ — so the
919
+ rule fires whatever else happens to be published there.
920
+
921
+ **The 301 carries a pinned lifetime**, and that is the part worth being
922
+ deliberate about:
923
+
924
+ ```text
925
+ # _headers
926
+ /sohl/
927
+ Cache-Control: max-age=3600
928
+ /sohl
929
+ Cache-Control: max-age=3600
930
+ ```
931
+
932
+ Cloudflare Pages sets **no** `Cache-Control` on a redirect it generates — its
933
+ redirect responses carry `location` and nothing else — and a 301 with no
934
+ `Cache-Control` is cacheable indefinitely by default under RFC 9111. Browsers
935
+ persist one to disk and stop asking the server, so a scheme that later changed
936
+ would strand every returning visitor on the package's most-linked URL. An
937
+ explicit `Cache-Control` overrides that heuristic and keeps the 301's canonical
938
+ signal without the permanence.
939
+
940
+ **`_headers` does apply to a `_redirects` response, and this is verified rather
941
+ than documented.** Cloudflare's docs say only that _"redirects are applied
942
+ before headers, so when a request matches both a redirect and a header, the
943
+ redirect takes priority"_ — a sentence routinely misread as "headers are skipped
944
+ on a redirect". Its open-source asset server settles it: the redirect response
945
+ returns from `generateResponse()` and then flows through `attachHeaders()`, and
946
+ the only short-circuit past that is `status >= 500`. The one documented "headers
947
+ are not applied" carve-out is Pages Functions, not redirects.
948
+
949
+ Because that is observed behaviour rather than a documented guarantee, **verify
950
+ it once after deploying** and treat a regression as a Cloudflare change rather
951
+ than a content bug:
952
+
953
+ ```bash
954
+ curl -sSI https://www.heroiclands.org/sohl/ | grep -i 'location\|cache-control'
955
+ ```
956
+
957
+ If it ever stops holding, the documented alternative is a zone-level **Response
958
+ Header Transform Rule** (or Bulk Redirects, which sets both), not a Pages
959
+ Function — `_redirects` and `_headers` both stop applying to a route a Function
960
+ serves.
856
961
 
857
962
  ### The homepage's own links
858
963
 
@@ -1077,6 +1182,40 @@ vendored by other repositories and read as authoritative, so emitting one is a
1077
1182
  statement about this package. With the switch off the command fails rather than
1078
1183
  writing.
1079
1184
 
1185
+ ### A page's URL is its address
1186
+
1187
+ ```text
1188
+ /<package>/<type>-<shortcode>/
1189
+ ```
1190
+
1191
+ `(type, shortcode)` names one note within a package — that is the rule
1192
+ `content-build lint` enforces — so the URL is **unique by construction**. There
1193
+ is no collision check behind it, there never can be one to fail, and renaming a
1194
+ note changes nothing: no part of the address comes from a display string.
1195
+
1196
+ It used to come from `name.full`. That made a display name load-bearing three
1197
+ ways at once — a rename silently 404'd every inbound link, two notes in one
1198
+ section could derive the same URL so a uniqueness gate had to run, and long names
1199
+ had to be shortened through a table of 200 abbreviations. The header of the
1200
+ module doing it justified the cost by promising redirects "every change appends
1201
+ to the legacy-URL map" — and no such map was ever written, here or in any
1202
+ consumer. All of it is gone (#181).
1203
+
1204
+ The `type-` half earns its place: it keeps every content address clear of the
1205
+ package's fixed mounts (`/<package>/` for the landing page, `/<package>/api/` for
1206
+ generated API docs), neither of which contains a hyphen or names a type. So the
1207
+ namespace is provably disjoint rather than conventionally so.
1208
+
1209
+ **Sections stay, as directories.** Hugo derives a page's section from where the
1210
+ file is written, not from its URL, and that section is what gives a landing page,
1211
+ `.CurrentSection` and the per-section layout lookup. So a page is still written
1212
+ into `<section>/`, and carries a front-matter `url:` publishing it at its
1213
+ address. The two are free to differ, and do.
1214
+
1215
+ **A landing page is the one exception**, because it is not addressed by
1216
+ `(type, shortcode)` at all: it _is_ its section, so it addresses the section —
1217
+ under the content mount, where the section directories live (`kb/rules/`).
1218
+
1080
1219
  ### The address scheme
1081
1220
 
1082
1221
  Where the content tree mounts _inside the package_, and which note addresses a
@@ -1095,14 +1234,15 @@ publish:
1095
1234
  landing: readme # default: readme
1096
1235
  ```
1097
1236
 
1098
- - **`prefix`** — the content tree's mount within the package. `sohl` publishes a
1099
- knowledgebase alongside generated API docs, so its notes sit under `kb/`
1100
- (`kb/affliction/aconite/`); `thalorna`'s site is nothing but its content, so it
1101
- has no prefix (`affiliation/the-aerarium-imperii/`). It must end in a slash and
1237
+ - **`prefix`** — the content tree's mount within the package: where its section
1238
+ directories and their landing pages live. `sohl` publishes a knowledgebase
1239
+ alongside generated API docs, so its sections sit under `kb/` (`kb/affliction/`)
1240
+ while its pages address the package root (`affliction-aconite/`); `thalorna`'s
1241
+ site is nothing but its content, so it has no prefix. It must end in a slash and
1102
1242
  must not begin with one — where the _package_ is mounted is the consuming
1103
1243
  build's knowledge and is never recorded here.
1104
- - **`landing`** — which note is a section's landing page, and so has no slug of
1105
- its own:
1244
+ - **`landing`** — which note is a section's landing page, and so is addressed by
1245
+ the section rather than by `(type, shortcode)`:
1106
1246
  - `readme` — a `README.md` addresses its section. A `doc` note then routes by
1107
1247
  its `category` like any other, so a `category: collection` note publishes
1108
1248
  under a literal `collection/` section.
@@ -1112,8 +1252,25 @@ publish:
1112
1252
  The two are alternatives rather than a pair that could both apply: each live
1113
1253
  content tree holds notes the other rule would move.
1114
1254
 
1115
- A note the scheme yields no address for a `doc` with no category, a collection
1116
- note naming no section is **reported and omitted**, never guessed. The command
1255
+ **Under `readme`, a landing's `subType` is an address rather than a genre.** The
1256
+ segment the `README` lands at is what `sectionOf` reads for a `doc`, its
1257
+ `subType` — so `Weapons/README.md` writes `subType: weapongear` and publishes at
1258
+ `weapongear/`. `content-build lint` checks that value against the sections that
1259
+ can exist, which is three sets: every **content type** the format declares
1260
+ (`sectionOf` returns a non-`doc` note's own type, so `being` and `weapongear` are
1261
+ sections by construction), the **subtypes the type declares** (`rules`,
1262
+ `user-guide`, `reference`), and any section named in
1263
+ [`site.sections` / `site.readmeSections`](#what-a-section-may-declare). A
1264
+ misspelt one is still refused, by name and against all three, with the near miss
1265
+ suggested. This applies to a `README` only: every other note's `subType` stays
1266
+ closed to the values its type declares.
1267
+
1268
+ Configuring the section is **not** a prerequisite — `site.sections` is framing,
1269
+ and a package that renders its own site need declare no `site:` block at all.
1270
+
1271
+ A note the scheme yields no address for — one carrying no `shortcode`, a `doc`
1272
+ with no subtype (so no section to be filed under), a collection note naming no
1273
+ section — is **reported and omitted**, never guessed. The command
1117
1274
  prints one located diagnostic per note and still writes the file, because a note
1118
1275
  with no address is ordinary while a manifest entry pointing at a page that does
1119
1276
  not exist is not.
@@ -1140,6 +1297,7 @@ written by a person:
1140
1297
  ```markdown
1141
1298
  ---
1142
1299
  type: homepage
1300
+ shortcode: root
1143
1301
  title: HârnMaster Kethira Basic # optional; defaults to packageBuild.manifest.title
1144
1302
  ---
1145
1303
 
@@ -1151,9 +1309,13 @@ A package declares **exactly one** of these, and both `content-build lint` and
1151
1309
  [Exactly one homepage](#exactly-one-homepage).
1152
1310
 
1153
1311
  That is the whole envelope. A homepage **compiles into no compendium
1154
- document**, appears in no pack and in no link manifest, and is addressed by the
1155
- _package_ rather than by its own name so `name.full`, `shortcode` and `id`
1156
- decide nothing on it. It is dispatched on `type` like every other note, not on a
1312
+ document**, and so appears in no pack and in no link manifest which is why it
1313
+ still refuses `id`. Everything else about its address is ordinary: it declares a
1314
+ `shortcode`, publishes at `/<contentPackage>/homepage-root/`, and is cited as
1315
+ `[[homepage-root|Text]]` (#182 — see
1316
+ [The homepage is addressed like every other note](#the-homepage-is-addressed-like-every-other-note)).
1317
+ `/<contentPackage>/` itself is a redirect the package authors, not a page this
1318
+ build writes. It is dispatched on `type` like every other note, not on a
1157
1319
  filename: `README.md` is already a section landing under `landing: readme`, and
1158
1320
  in `sohl-thalorna` it is a developer explainer about the source tree.
1159
1321
 
@@ -1189,9 +1351,11 @@ That is separate from `publish.manifests.publish`, which stays off for both for
1189
1351
  an unrelated reason: a link manifest is the dependency edge that would stop the
1190
1352
  module being withdrawable, and a homepage is one row in a routing table.
1191
1353
 
1192
- The homepage is written at the root of `site.out` — the package's own address —
1193
- one level above the content mount, which is where `publish.address.prefix` puts
1194
- everything else.
1354
+ The homepage's file is written at the root of `site.out` — the package's own
1355
+ site root, one level above the content mount, which is where
1356
+ `publish.address.prefix` puts everything else — under the name its address gives
1357
+ it, `homepage-root.md`. As with every other page, the file's location decides
1358
+ the Hugo section and the front matter's `url` decides where it publishes.
1195
1359
 
1196
1360
  **What it does not do is decide addresses.** Those come from `publish.address`,
1197
1361
  the same setting the link manifest reads, so a page and its manifest entry cannot
@@ -1320,8 +1484,7 @@ after the links that failed because of it reads as a pile of broken notes.
1320
1484
  | Gate | What it catches |
1321
1485
  | ---------------------- | ---------------------------------------------------------------------------------- |
1322
1486
  | Frontmatter wikilinks | A link in frontmatter, which is copied verbatim and reaches the reader as `[[…]]`. |
1323
- | Slugs | A name that yields no URL. |
1324
- | Collisions | Two notes claiming one page URL. |
1487
+ | Addresses | A note with no shortcode to be addressed by, or no section to be filed under. |
1325
1488
  | Unusable manifest | A vendored manifest this build cannot read. |
1326
1489
  | Unaddressable manifest | One it can read but cannot look anything up in. |
1327
1490
  | Package conflicts | One address claimed by two packages. |
@@ -1404,7 +1567,7 @@ form every C-family compiler, `tsc` and ESLint already use, so an editor, a CI
1404
1567
  annotator or a `grep` parses it with no knowledge of this build:
1405
1568
 
1406
1569
  ```text
1407
- assets/content/Regions/Capital_Nome.md:43:635: warning: unresolved wikilink [[Kenbet_Pat|Kenbet'Pat]] (unknown) in "The Capital Nome"
1570
+ assets/content/Regions/Capital_Nome.md:43:635: error: address [[place-kenbetpat]] resolves to no note — no package publishes it. Fix the shortcode, or vendor the link manifest of the package that does — in "The Capital Nome".
1408
1571
  ```
1409
1572
 
1410
1573
  `file:line:column: severity: message`. The path is relative to the working
package/MIGRATING.md CHANGED
@@ -1,3 +1,67 @@
1
+ # Migrating to `@heroiclands/package-build` 11.0.0
2
+
3
+ **Two edits, and the second is one line per repository.** Every published page
4
+ moves to its address (#181), and the package homepage becomes an ordinary
5
+ addressed note (#182).
6
+
7
+ ## 1. A page's URL is its address
8
+
9
+ Every content page now serves at `/<package>/<type>-<shortcode>/` rather than at
10
+ a slug derived from `name.full`. No content edit is required for it — the
11
+ address is computed from fields every note already declares — but **every
12
+ published URL moves**, so anything holding one (an external link, a bookmark, a
13
+ citation in another repository) has to be re-derived.
14
+
15
+ ## 2. Give the homepage a `shortcode`
16
+
17
+ The homepage used to **refuse** `shortcode` and `name`, because a URL derived
18
+ from a display name while a homepage's destination was fixed. That premise is
19
+ gone, so both fields are permitted and `shortcode` is **required**:
20
+
21
+ ```markdown
22
+ ---
23
+ type: homepage
24
+ shortcode: root
25
+ title: HârnMaster Kethira Basic
26
+ ---
27
+ ```
28
+
29
+ `root` is the convention, not a rule. Without one, `content-build lint` and
30
+ `content-build site` both refuse the note:
31
+
32
+ ```text
33
+ assets/content/homepage.md:3:7: error: a `type: homepage` note declares a `shortcode`, like every other note: it is addressed as `homepage-<shortcode>` and published at `/<package>/homepage-<shortcode>/`, which is where `[[homepage-<shortcode>|Text]]` lands. Write `shortcode: root` — the package landing is `homepage-root` in every package
34
+ ```
35
+
36
+ `id` is still refused, on ground this does not touch: a homepage compiles into
37
+ no compendium document.
38
+
39
+ ## 3. Author the `/<package>/` redirect
40
+
41
+ The landing is published at `/<package>/homepage-root/`, and nothing is written
42
+ at `/<package>/` any more. Add the redirect to the repository's own
43
+ `_redirects`, and pin its lifetime in `_headers` — Cloudflare Pages sets no
44
+ `Cache-Control` on a redirect it generates, and an unpinned 301 is cacheable
45
+ indefinitely:
46
+
47
+ ```text
48
+ # _redirects
49
+ /sohl/ /sohl/homepage-root/ 301
50
+ /sohl /sohl/homepage-root/ 301
51
+ ```
52
+
53
+ ```text
54
+ # _headers
55
+ /sohl/
56
+ Cache-Control: max-age=3600
57
+ /sohl
58
+ Cache-Control: max-age=3600
59
+ ```
60
+
61
+ Both path forms, because Pages matches the raw path and `/sohl` and `/sohl/` are
62
+ distinct keys. See `CONTENT.md` for why the pairing works and how to verify it
63
+ after a deploy.
64
+
1
65
  # Migrating to `@heroiclands/package-build` 6.0.0
2
66
 
3
67
  **No configuration change to make, and one build check that may now fail.**