@heroiclands/package-build 13.0.0 → 15.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.
- package/CHANGELOG.md +361 -0
- package/CONTENT.md +193 -43
- package/MIGRATING.md +196 -5
- package/content-config.mjs +99 -61
- package/docs/content-format.md +28 -8
- package/engine/content-address.mjs +11 -19
- package/engine/field-reference.mjs +29 -0
- package/engine/field-spec.mjs +25 -0
- package/engine/frontmatter-lint.mjs +99 -32
- package/engine/helpers.mjs +38 -12
- package/engine/homepage.mjs +9 -4
- package/engine/item-registry.mjs +4 -1
- package/engine/macros.mjs +3 -1
- package/engine/manifest-emit.mjs +4 -7
- package/engine/note-vocabulary.mjs +34 -76
- package/engine/site-build.mjs +20 -14
- package/engine/system-block.mjs +29 -3
- package/package.json +1 -1
- package/sohl/actors.mjs +6 -3
- package/sohl/item-fields.mjs +6 -0
- package/sohl/items.mjs +4 -1
- package/types/content-config.d.mts +25 -35
- package/types/engine/content-address.d.mts +7 -14
- package/types/engine/field-spec.d.mts +53 -0
- package/types/engine/helpers.d.mts +34 -12
- package/types/engine/homepage.d.mts +8 -4
- package/types/engine/manifest-emit.d.mts +3 -9
- package/types/engine/note-vocabulary.d.mts +17 -47
- package/types/engine/site-build.d.mts +11 -6
package/CONTENT.md
CHANGED
|
@@ -138,7 +138,6 @@ publish:
|
|
|
138
138
|
manifests: { publish: true, consume: true }
|
|
139
139
|
address:
|
|
140
140
|
prefix: kb/
|
|
141
|
-
landing: readme
|
|
142
141
|
|
|
143
142
|
# How this repository frames the website `content-build site` publishes.
|
|
144
143
|
# Framing only: addresses come from `publish.address` above.
|
|
@@ -456,9 +455,9 @@ error: pack "characters" (Actor) reads the compiled output of the Item pack
|
|
|
456
455
|
|
|
457
456
|
### An item type's default art
|
|
458
457
|
|
|
459
|
-
A note that
|
|
460
|
-
|
|
461
|
-
|
|
458
|
+
A note that names no art gets its type's **default art**, and a type declares
|
|
459
|
+
that art in the same place it declares its builder. An `itemBuilders` entry may
|
|
460
|
+
be written two ways:
|
|
462
461
|
|
|
463
462
|
```js
|
|
464
463
|
itemBuilders: {
|
|
@@ -479,8 +478,60 @@ same `resolveImg` rule as a note's `img:`, so `icons/relic.svg` means _this_
|
|
|
479
478
|
repository's asset root — `modules/sohl-relics/assets/icons/relic.svg` — and an
|
|
480
479
|
already-served path (`systems/sohl/assets/icons/…`) passes through untouched.
|
|
481
480
|
|
|
482
|
-
|
|
483
|
-
|
|
481
|
+
#### "Names no art" and "wants no art" are different (#218)
|
|
482
|
+
|
|
483
|
+
A note has two ways to leave `img:` empty, and they mean opposite things:
|
|
484
|
+
|
|
485
|
+
| a note writes | it means | it compiles with |
|
|
486
|
+
| -------------- | ------------------------------------ | ---------------- |
|
|
487
|
+
| nothing at all | _unset_ — name me no art | the type default |
|
|
488
|
+
| `img: null` | the same thing, said out loud | the type default |
|
|
489
|
+
| `img: ""` | _blank on purpose_ — I want no image | no image |
|
|
490
|
+
| `img: <path>` | this art | that path |
|
|
491
|
+
|
|
492
|
+
`resolveImg` returns `null` for the first two and `""` for the third, and every
|
|
493
|
+
caller pairs its default with **nullish** coalescing — `resolveImg(fm.img) ?? itemArt(type)`.
|
|
494
|
+
Never `||`: that collapses a deliberate blank back into the default and takes the
|
|
495
|
+
distinction away again, which is exactly what the function used to do.
|
|
496
|
+
|
|
497
|
+
**`portrait` is the same field twice over.** A being carries `img` (its token
|
|
498
|
+
art) and `portrait` (its sheet portrait) independently, and both resolve through
|
|
499
|
+
`resolveImg`, so the rule above is the rule for both.
|
|
500
|
+
|
|
501
|
+
This is the convention the project already holds for an optional "not specified"
|
|
502
|
+
DataModel string — `nullable, initial: null`, so "unset" is one honest value
|
|
503
|
+
rather than two.
|
|
504
|
+
|
|
505
|
+
> **The rule is `img`'s, and does not extend to `title`.** `title` is not art
|
|
506
|
+
> and never reaches `resolveImg`, so nothing here applies to it.
|
|
507
|
+
>
|
|
508
|
+
> The reason used to be sharper, and is no longer true: a note's top-level
|
|
509
|
+
> `title` was _also_ the shared source for an `affiliation` item's `system.title`
|
|
510
|
+
> (`sohl/item-fields.mjs`, "the style of address the office carries"), so one
|
|
511
|
+
> authored key fed two unrelated destinations that disagreed about what empty
|
|
512
|
+
> means — and `title: null` stringified into the compiled document as the literal
|
|
513
|
+
> `"null"`. That collision is gone: the field declares `topLevelMeans`, and the
|
|
514
|
+
> top-level key is no longer a source for it (#218).
|
|
515
|
+
>
|
|
516
|
+
> So `title: null` is now a note declining to state a heading, and the site
|
|
517
|
+
> emitter's `fm.title ?? name` falls back to `name.full`. `title: ""` still
|
|
518
|
+
> publishes a deliberately blank heading, and nothing warns about that yet.
|
|
519
|
+
|
|
520
|
+
Because `""` used to mean "unset", a note still carrying that spelling has
|
|
521
|
+
quietly changed meaning, and the frontmatter lint says so — for either art
|
|
522
|
+
field:
|
|
523
|
+
|
|
524
|
+
```
|
|
525
|
+
Note.md:9:1: warning: `img: ""` means "ship no art at all" — it no longer falls
|
|
526
|
+
back to this type's default. Write `img: null` for a note that simply names
|
|
527
|
+
none; keep `""` only where the document is meant to have no image
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
A warning, not an error: the note still compiles, to a document that is merely
|
|
531
|
+
iconless.
|
|
532
|
+
|
|
533
|
+
**A type with neither is a build error, deliberately.** When a note names no
|
|
534
|
+
art and its type pairs none, the pack build aborts rather than shipping an
|
|
484
535
|
item with a mismatched icon:
|
|
485
536
|
|
|
486
537
|
```
|
|
@@ -571,6 +622,47 @@ system `S` is:
|
|
|
571
622
|
`sohl:`", which is the degenerate case where source and destination happen to
|
|
572
623
|
share a name.
|
|
573
624
|
|
|
625
|
+
**A spelling that means two different things skips step 3.** Because a field's
|
|
626
|
+
`name` doubles as its identity and as the shared property it draws from, the two
|
|
627
|
+
coincide only while the note vocabulary and the system vocabulary agree about
|
|
628
|
+
what a spelling means. `title` is where they do not. A note's top-level `title`
|
|
629
|
+
is _the title of the note_ — the heading its page publishes under, which the
|
|
630
|
+
site emitter reads. An `affiliation` item's `system.title` is _the style of
|
|
631
|
+
address the office carries_ — Ajaw, Warden, a person's style within the body.
|
|
632
|
+
They are unrelated quantities, and step 3 used to feed the second from the first
|
|
633
|
+
(#218).
|
|
634
|
+
|
|
635
|
+
That was not merely untidy, because **step 3 answers without applying
|
|
636
|
+
`field.default`** — only step 2 does — so an authored `title: null` reached the
|
|
637
|
+
field's `String()` coercion unguarded and shipped as the literal string `"null"`.
|
|
638
|
+
|
|
639
|
+
So a field may declare `topLevelMeans`: what the top-level key of that name means
|
|
640
|
+
_instead_. Declaring it removes step 3 for that field, and the value is the
|
|
641
|
+
reason rather than a bare flag, so the collision is legible where the field is
|
|
642
|
+
declared and the generated field reference can print it. It is a per-field
|
|
643
|
+
opt-out, not a change to the order — step 3 is right wherever the two levels
|
|
644
|
+
state the same quantity, which is nearly everywhere: `subType` is the other
|
|
645
|
+
declared item field spelled like a note-level key, and there the two agree by
|
|
646
|
+
design.
|
|
647
|
+
|
|
648
|
+
**An exempted field is still authorable**, at the two positions that describe the
|
|
649
|
+
document rather than the note:
|
|
650
|
+
|
|
651
|
+
```yaml
|
|
652
|
+
title: The Order of the Silver Hand # the note's own heading — reaches the page
|
|
653
|
+
type: affiliation
|
|
654
|
+
subType: order
|
|
655
|
+
sohl:
|
|
656
|
+
system:
|
|
657
|
+
title: Warden # → document.system.title, the style of address
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
`sohl.title`, the legacy in-block position, works the same way. A membership — a
|
|
661
|
+
`title` a particular being holds — is authored on the entry in that being's
|
|
662
|
+
`sohl.items`, whose `system` is overlaid on the catalogue document directly.
|
|
663
|
+
`data.title` is neither position: `title` is not a `data:` property any note type
|
|
664
|
+
declares, so the frontmatter lint refuses it.
|
|
665
|
+
|
|
574
666
|
**`<system>.system` is written through verbatim**, at the DataModel's own paths,
|
|
575
667
|
with no renaming layer. A key the system's published `schema.json` does not
|
|
576
668
|
declare for the subtype the note compiles into is an **error naming the note**,
|
|
@@ -806,15 +898,15 @@ rule is checked ahead of the closed-set check, which is what makes it reach a
|
|
|
806
898
|
type whose values are declared but not yet enumerated:
|
|
807
899
|
|
|
808
900
|
```text
|
|
809
|
-
assets/content/Beings/Folk.md:3:1: error: `subType` "common-folk" is not
|
|
901
|
+
assets/content/Beings/Folk.md:3:1: error: `subType` "common-folk" is not a well-formed subType — a subType is letters and digits only (^[A-Za-z0-9]+$), the same charset a type, a shortcode and a contentPackage are held to. …
|
|
810
902
|
```
|
|
811
903
|
|
|
812
904
|
One declared value broke that rule: a `doc`'s `user-guide`, now **`userguide`**.
|
|
813
|
-
The old spelling
|
|
814
|
-
|
|
815
|
-
release before it had a chance to sweep
|
|
816
|
-
|
|
817
|
-
|
|
905
|
+
The old spelling was accepted for one transitional release, as a warning naming
|
|
906
|
+
the replacement, because an error would have redded every tree that took the
|
|
907
|
+
release before it had a chance to sweep. Every tree has swept, so the acceptance
|
|
908
|
+
is gone (#210): `user-guide` is refused by the charset check like any other
|
|
909
|
+
hyphenated value, and nothing retirement-specific was left to remove.
|
|
818
910
|
|
|
819
911
|
The vocabulary lives in `engine/note-vocabulary.mjs`, one entry per note type,
|
|
820
912
|
taken from the content-format specification. It is note-format knowledge rather
|
|
@@ -1235,6 +1327,21 @@ order to satisfy a rendering engine's directory semantics. The file is now
|
|
|
1235
1327
|
`<mount>/<type>-<shortcode>.md` and the front-matter `url:` still publishes it at
|
|
1236
1328
|
the package root, one level above.
|
|
1237
1329
|
|
|
1330
|
+
**A page states its address without the package base; everything pointing _at_
|
|
1331
|
+
it composes one** (#217). They read as one quantity and are two:
|
|
1332
|
+
|
|
1333
|
+
| Written | Form | Because |
|
|
1334
|
+
| ------------------------------------------- | --------------------------- | --------------------------------------------------------------------------------------- |
|
|
1335
|
+
| A page's own `url:` front matter | `/<type>-<shortcode>/` | Hugo resolves it against `baseURL`, whose path already _is_ where the package is served |
|
|
1336
|
+
| Every `href` this build renders into a body | `<base><type>-<shortcode>/` | A browser resolves it against nothing |
|
|
1337
|
+
| A link-manifest `path` | `<type>-<shortcode>/` | Measured against `site.base` and stripped; a consumer prefixes its own |
|
|
1338
|
+
|
|
1339
|
+
`site.base` is the second and third of those and reaches the first not at all.
|
|
1340
|
+
It used to be written into the `url:` as well, so every consumer's Hugo prefixed
|
|
1341
|
+
its own base to a value that already carried one and published every content
|
|
1342
|
+
page a segment too deep — `/sohl/sohl/doc-rulesintro/`, 404 at the address the
|
|
1343
|
+
manifest, the sitemap and every inbound link named.
|
|
1344
|
+
|
|
1238
1345
|
**There is no landing page.** A `README.md` was its section's landing and
|
|
1239
1346
|
addressed the section itself; that is retired with the section. A page that
|
|
1240
1347
|
introduces the notes of a type is an ordinary note addressed `doc-<type>`, with
|
|
@@ -1247,9 +1354,8 @@ per section, and that is now the _only_ thing that makes one exist — see
|
|
|
1247
1354
|
|
|
1248
1355
|
### The address scheme
|
|
1249
1356
|
|
|
1250
|
-
Where the content tree mounts _inside the package_
|
|
1251
|
-
|
|
1252
|
-
both load-bearing. They are one setting, read by this command **and** by the page
|
|
1357
|
+
Where the content tree mounts _inside the package_ differs between repositories
|
|
1358
|
+
and is load-bearing. It is one setting, read by this command **and** by the page
|
|
1253
1359
|
emitter, so the address a manifest publishes is the address a page is emitted at
|
|
1254
1360
|
— stating it twice is how a manifest comes to assert a URL that resolves at build
|
|
1255
1361
|
time and 404s for the reader.
|
|
@@ -1260,7 +1366,6 @@ publish:
|
|
|
1260
1366
|
manifests: { publish: true, consume: true }
|
|
1261
1367
|
address:
|
|
1262
1368
|
prefix: kb/ # default: "" — the package root
|
|
1263
|
-
landing: readme # inert since #204; still accepted
|
|
1264
1369
|
```
|
|
1265
1370
|
|
|
1266
1371
|
- **`prefix`** — the content tree's mount within the package: the Hugo directory
|
|
@@ -1270,14 +1375,9 @@ publish:
|
|
|
1270
1375
|
content, so it has no prefix. It must end in a slash and must not begin with
|
|
1271
1376
|
one — where the _package_ is mounted is the consuming build's knowledge and is
|
|
1272
1377
|
never recorded here.
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
nothing; it is still accepted because both publishing consumers declare
|
|
1277
|
-
`landing: readme` and refusing a correct statement would break them. It is
|
|
1278
|
-
removed once no configuration writes it. The retired second rule, `collection`,
|
|
1279
|
-
is still refused by name (#202), along with the `collection` subtype and the
|
|
1280
|
-
top-level `section:` key.
|
|
1378
|
+
|
|
1379
|
+
`prefix` is the whole scheme. The `collection` subtype and the top-level
|
|
1380
|
+
`section:` key are refused by name (#202).
|
|
1281
1381
|
|
|
1282
1382
|
A note's `subType` is checked against the values its type declares, and only
|
|
1283
1383
|
those. It briefly had a second reading — a `README` landing's `subType` was the
|
|
@@ -1371,7 +1471,8 @@ The homepage's file is written at the root of `site.out` — the package's own
|
|
|
1371
1471
|
site root, one level above the content mount, which is where
|
|
1372
1472
|
`publish.address.prefix` puts everything else — under the name its address gives
|
|
1373
1473
|
it, `homepage-root.md`. As with every other page, the front matter's `url`
|
|
1374
|
-
decides where it publishes.
|
|
1474
|
+
decides where it publishes, and states it relative to the site root — `site.base`
|
|
1475
|
+
does not reach it (#217).
|
|
1375
1476
|
|
|
1376
1477
|
**What it does not do is decide addresses.** Those come from `publish.address`,
|
|
1377
1478
|
the same setting the link manifest reads, so a page and its manifest entry cannot
|
|
@@ -1382,7 +1483,7 @@ published beside the content:
|
|
|
1382
1483
|
```yaml
|
|
1383
1484
|
site:
|
|
1384
1485
|
out: kb/content # required; wiped on every run
|
|
1385
|
-
base: /sohl/ # default: /<contentPackage>/
|
|
1486
|
+
base: /sohl/ # default: /<contentPackage>/ — hrefs only, never a page's `url:`
|
|
1386
1487
|
packages: [sohl, thalorna] # default: just contentPackage
|
|
1387
1488
|
backfillSections: true
|
|
1388
1489
|
landing: { title: Knowledgebase, type: knowledgebase }
|
|
@@ -1402,18 +1503,18 @@ site:
|
|
|
1402
1503
|
dev-docs: { title: Developer Documentation, banner: banners/dev-docs.webp }
|
|
1403
1504
|
```
|
|
1404
1505
|
|
|
1405
|
-
| Key | What it decides
|
|
1406
|
-
| ------------------ |
|
|
1407
|
-
| `out` | The Hugo content root. **Required** in both modes, and wiped on every run — see below.
|
|
1408
|
-
| `base` | Where the package is served. Defaults to `/<contentPackage>/`.
|
|
1409
|
-
| `packages` | Which content packages this site renders. Defaults to its own.
|
|
1410
|
-
| `sections` | The Hugo sections this site declares, and what each says about itself — see below.
|
|
1411
|
-
| `readmeSections` | The same, for a `trees` entry, whose landing comes from its own `README`.
|
|
1412
|
-
| `landing` | Frontmatter for the mount's own `_index.md`. Passed through — the vocabulary is the theme's.
|
|
1413
|
-
| `backfillSections` | Write a bare `_index.md` for any other directory directly under the mount.
|
|
1414
|
-
| `trees` | Extra source trees published beside the content, preserving their source layout below a section.
|
|
1415
|
-
| `pass` | A named bundle of this repository's own body rewrites.
|
|
1416
|
-
| `passOptions` | That bundle's options.
|
|
1506
|
+
| Key | What it decides |
|
|
1507
|
+
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1508
|
+
| `out` | The Hugo content root. **Required** in both modes, and wiped on every run — see below. |
|
|
1509
|
+
| `base` | Where the package is served: the prefix on every rendered `href`, and what a manifest `path` is measured against. It reaches no page's own `url:` — see [A page's URL is its address](#a-pages-url-is-its-address). Defaults to `/<contentPackage>/`. |
|
|
1510
|
+
| `packages` | Which content packages this site renders. Defaults to its own. |
|
|
1511
|
+
| `sections` | The Hugo sections this site declares, and what each says about itself — see below. |
|
|
1512
|
+
| `readmeSections` | The same, for a `trees` entry, whose landing comes from its own `README`. |
|
|
1513
|
+
| `landing` | Frontmatter for the mount's own `_index.md`. Passed through — the vocabulary is the theme's. |
|
|
1514
|
+
| `backfillSections` | Write a bare `_index.md` for any other directory directly under the mount. |
|
|
1515
|
+
| `trees` | Extra source trees published beside the content, preserving their source layout below a section. |
|
|
1516
|
+
| `pass` | A named bundle of this repository's own body rewrites. |
|
|
1517
|
+
| `passOptions` | That bundle's options. |
|
|
1417
1518
|
|
|
1418
1519
|
### What a section may declare
|
|
1419
1520
|
|
|
@@ -1431,7 +1532,8 @@ Two consequences follow, and neither is optional:
|
|
|
1431
1532
|
`_index.md`, so a layout reading `.Pages` finds nothing. A layout that queries
|
|
1432
1533
|
`site.RegularPages` by `Params.type` is unaffected, and that is the shape a
|
|
1433
1534
|
content catalog wants anyway — it groups by what a page _is_, not by where its
|
|
1434
|
-
file happened to be written.
|
|
1535
|
+
file happened to be written. A section that wants that query run for it says
|
|
1536
|
+
so with `listType` — see below.
|
|
1435
1537
|
|
|
1436
1538
|
Whatever the entry may carry is the whole of what the section can say.
|
|
1437
1539
|
|
|
@@ -1440,18 +1542,63 @@ Whatever the entry may carry is the whole of what the section can say.
|
|
|
1440
1542
|
| `title` | yes | The landing's heading, so it matches the card that links to it. |
|
|
1441
1543
|
| `banner` | no | The hero image, resolved as a CDN asset like any other `banner:`. |
|
|
1442
1544
|
| `description` | no | The hero standfirst under the heading, and the blurb a landing card uses. |
|
|
1545
|
+
| `listType` | no | The content **type** whose pages this section lists. |
|
|
1546
|
+
| `listSubType` | no | Narrows that to one **subType**. Only with a `listType`. |
|
|
1443
1547
|
|
|
1444
|
-
`readmeSections` takes the same
|
|
1548
|
+
`readmeSections` takes the same keys, for a **`trees`** entry: those pages keep
|
|
1445
1549
|
their source layout below a named section, so the tree's own `README.md` is that
|
|
1446
1550
|
section's landing. What the section declares wins over what the `README` happens
|
|
1447
1551
|
to carry — the landing has to match the card that links to it. No content note
|
|
1448
1552
|
reaches this map any more.
|
|
1449
1553
|
|
|
1554
|
+
#### Saying what a section lists
|
|
1555
|
+
|
|
1556
|
+
A generic list layout has nothing to render, because the membership a section
|
|
1557
|
+
landing used to get free from Hugo's page tree no longer exists anywhere the
|
|
1558
|
+
theme can read: not on the page, not on the landing, not in any URL — only here.
|
|
1559
|
+
So a section states its own query, and a layout substitutes it when `.Pages` is
|
|
1560
|
+
empty:
|
|
1561
|
+
|
|
1562
|
+
```yaml
|
|
1563
|
+
sections:
|
|
1564
|
+
being: { title: Beings, listType: being }
|
|
1565
|
+
rules: { title: Rules, listType: doc, listSubType: rules }
|
|
1566
|
+
user-guide: { title: User Guide, listType: doc, listSubType: userguide }
|
|
1567
|
+
```
|
|
1568
|
+
|
|
1569
|
+
emits, for the last of those, `user-guide/_index.md`:
|
|
1570
|
+
|
|
1571
|
+
```yaml
|
|
1572
|
+
---
|
|
1573
|
+
title: User Guide
|
|
1574
|
+
listType: doc
|
|
1575
|
+
listSubType: userguide
|
|
1576
|
+
---
|
|
1577
|
+
```
|
|
1578
|
+
|
|
1579
|
+
Three things about the spelling, each of them load-bearing:
|
|
1580
|
+
|
|
1581
|
+
- **Not `type`.** On an `_index.md` that is Hugo's own layout selector — a
|
|
1582
|
+
landing carrying `type: doc` is rendered by `layouts/doc/list.html` rather
|
|
1583
|
+
than the default list template — so writing the content type there would
|
|
1584
|
+
silently change which template serves the landing. This build already relies
|
|
1585
|
+
on that behaviour for the mount's own `landing`.
|
|
1586
|
+
- **Not inferred from the section's name.** A section is named for a URL the
|
|
1587
|
+
site chose; a type and a subType are addresses. They need not agree, and on
|
|
1588
|
+
`sohl` they do not: the section is `user-guide`, because that is a published
|
|
1589
|
+
URL, while the subType is `userguide` because an address segment is
|
|
1590
|
+
alphanumeric (#207). Both values are checked against that charset here, so
|
|
1591
|
+
copying the section's name in is refused rather than quietly matching nothing.
|
|
1592
|
+
- **`listSubType` needs a `listType`.** A subType only tells pages apart within
|
|
1593
|
+
a type — `rules`, `userguide` and `reference` are all `doc` — so alone it
|
|
1594
|
+
names no query.
|
|
1595
|
+
|
|
1450
1596
|
**The vocabulary is closed, and a key outside it is refused by name:**
|
|
1451
1597
|
|
|
1452
1598
|
```text
|
|
1453
1599
|
package-build config: `site.sections.affliction.descrption` is not a
|
|
1454
|
-
recognized option (expected one of: title, banner, description
|
|
1600
|
+
recognized option (expected one of: title, banner, description, listType,
|
|
1601
|
+
listSubType).
|
|
1455
1602
|
```
|
|
1456
1603
|
|
|
1457
1604
|
That refusal is the point. `landing` is passed through unvalidated because it is
|
|
@@ -1460,7 +1607,10 @@ entry is written fourteen to twenty times per build against a contract every
|
|
|
1460
1607
|
package and every section shares, so an unbounded one would let a mistyped
|
|
1461
1608
|
`descrption:` publish into front matter, be read by nobody, and say nothing to
|
|
1462
1609
|
anyone. Refusing it costs one line here when the vocabulary genuinely grows, and
|
|
1463
|
-
buys a build that cannot quietly emit a key no theme reads.
|
|
1610
|
+
buys a build that cannot quietly emit a key no theme reads. `listType` and
|
|
1611
|
+
`listSubType` are that growth: two named keys, checked, rather than an open
|
|
1612
|
+
passthrough in which `listTpye:` would publish and no landing would list
|
|
1613
|
+
anything.
|
|
1464
1614
|
|
|
1465
1615
|
The **writers** name no keys: a section's `_index.md` is whatever the entry
|
|
1466
1616
|
resolved to, `title` first. So extending the vocabulary is a change to the
|
|
@@ -1624,7 +1774,7 @@ path now rides on the error, and the loader that read the file resolves it
|
|
|
1624
1774
|
against the YAML, so all of them come out located:
|
|
1625
1775
|
|
|
1626
1776
|
```text
|
|
1627
|
-
package-build.config.yaml:382:64: error: package-build config: `site.sections.being.descrption` is not a recognized option (expected one of: title, banner).
|
|
1777
|
+
package-build.config.yaml:382:64: error: package-build config: `site.sections.being.descrption` is not a recognized option (expected one of: title, banner, description, listType, listSubType).
|
|
1628
1778
|
```
|
|
1629
1779
|
|
|
1630
1780
|
The same two rules apply. A key the file never declares — a required one that is
|
package/MIGRATING.md
CHANGED
|
@@ -1,3 +1,188 @@
|
|
|
1
|
+
# Migrating to `@heroiclands/package-build` 15.0.0
|
|
2
|
+
|
|
3
|
+
**Three unrelated changes ship in this major, and they ask different
|
|
4
|
+
repositories for different things.** A page's `url:` front matter is now stated
|
|
5
|
+
relative to the **site root** (#217), which a site-publishing repository answers
|
|
6
|
+
by deleting one line; an empty art path now means the opposite of an absent one
|
|
7
|
+
(#218), which a content tree answers by sweeping `img: ""` and `portrait: ""` to
|
|
8
|
+
`null`; and a note's own `title` no longer reaches an `affiliation` item's
|
|
9
|
+
`system.title` (#218), which asks nothing of any repository that exists today.
|
|
10
|
+
|
|
11
|
+
Route yourself by what you have. A repository that publishes no site skips
|
|
12
|
+
§1–§2. One that authors no empty art path skips §3–§4. One with no
|
|
13
|
+
`type: affiliation` notes skips §6 — and §5, which is only the seam between the
|
|
14
|
+
two halves of #218.
|
|
15
|
+
|
|
16
|
+
## 1. Drop the `site.base: "/"` stopgap (#217)
|
|
17
|
+
|
|
18
|
+
If your repository set it to stop every page publishing at
|
|
19
|
+
`/<package>/<package>/<address>/`, delete the line — the default,
|
|
20
|
+
`/<contentPackage>/`, is now right for both halves:
|
|
21
|
+
|
|
22
|
+
```yaml
|
|
23
|
+
site:
|
|
24
|
+
out: kb/content
|
|
25
|
+
# base: "/" ← delete this
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Keeping it is no longer harmless: the addresses stay correct either way, but
|
|
29
|
+
every same-package link this build renders into a page body stays short
|
|
30
|
+
(`/doc-skills/` rather than `/sohl/doc-skills/`) and 404s.
|
|
31
|
+
|
|
32
|
+
A repository that is genuinely served somewhere other than
|
|
33
|
+
`/<contentPackage>/` still says so here, and that value still reaches every
|
|
34
|
+
`href`.
|
|
35
|
+
|
|
36
|
+
## 2. What the `url:` change does _not_ touch
|
|
37
|
+
|
|
38
|
+
- No note edits, and no configuration key added or removed.
|
|
39
|
+
- Every link-manifest entry, including each entry's `path`, is byte-identical.
|
|
40
|
+
- Every compiled compendium document is unchanged.
|
|
41
|
+
- `trees` pages and section landings state no `url:` and do not move.
|
|
42
|
+
|
|
43
|
+
The published address of every content page **does** move — from
|
|
44
|
+
`/<package>/<package>/<address>/`, where nothing linked, to
|
|
45
|
+
`/<package>/<address>/`, which is what the link manifest, the sitemap and every
|
|
46
|
+
inbound link already named.
|
|
47
|
+
|
|
48
|
+
## 3. Sweep `img: ""` and `portrait: ""` to `null` (#218)
|
|
49
|
+
|
|
50
|
+
`resolveImg` opened with `if (!raw) return ""`, and every caller applied its own
|
|
51
|
+
default to the result with `||`. So `""`, `null` and an absent key were one
|
|
52
|
+
case: all three compiled to the type's default art, and a note had no way to say
|
|
53
|
+
"ship no image" at all.
|
|
54
|
+
|
|
55
|
+
They are now three values with two meanings:
|
|
56
|
+
|
|
57
|
+
| a note writes | it means | it compiles with |
|
|
58
|
+
| -------------- | ------------------------------------ | ---------------- |
|
|
59
|
+
| nothing at all | _unset_ — name me no art | the type default |
|
|
60
|
+
| `img: null` | the same thing, said out loud | the type default |
|
|
61
|
+
| `img: ""` | _blank on purpose_ — I want no image | no image |
|
|
62
|
+
|
|
63
|
+
So a note still carrying `img: ""` **loses its default art**. The same holds for
|
|
64
|
+
`portrait:`, which a being carries independently of `img` and which resolves
|
|
65
|
+
through the same function. Find them both:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
grep -rnE '^[[:space:]]*(img|portrait):[[:space:]]*""[[:space:]]*$' assets/content --include='*.md'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
and write `null` in each — unless the document really is meant to have no image,
|
|
72
|
+
which is what `""` now says. Before this release `sohl-thalorna` swept forty-five
|
|
73
|
+
`img: ""` notes and `sohl-kethira-basic` eleven `portrait: ""` beings; `sohl`
|
|
74
|
+
authors neither.
|
|
75
|
+
|
|
76
|
+
The frontmatter lint reports every one that is left, as a warning:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
Note.md:9:1: warning: `img: ""` means "ship no art at all" — it no longer falls
|
|
80
|
+
back to this type's default. Write `img: null` for a note that simply names
|
|
81
|
+
none; keep `""` only where the document is meant to have no image
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## 4. Custom callers pair their default with `??`, not `||`
|
|
85
|
+
|
|
86
|
+
`resolveImg` returns `string | null` now: `null` for an unset path, `""` for a
|
|
87
|
+
deliberate blank. A consumer that calls it directly — a custom item builder, a
|
|
88
|
+
compiler of its own — must switch:
|
|
89
|
+
|
|
90
|
+
```diff
|
|
91
|
+
-img: resolveImg(fm.img) || MY_DEFAULT,
|
|
92
|
+
+img: resolveImg(fm.img) ?? MY_DEFAULT,
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`||` still compiles and still looks right; it silently reinstates the old
|
|
96
|
+
conflation, because `""` is falsy. Every caller in this package moved:
|
|
97
|
+
`sohl/items.mjs`, three in `sohl/actors.mjs` (`img`, `portrait`, and the
|
|
98
|
+
prototype token's `texture.src`), and `engine/macros.mjs`.
|
|
99
|
+
|
|
100
|
+
`itemArt()` is unaffected — a registry entry with no art throws before the
|
|
101
|
+
translation, so its result is never the unset case.
|
|
102
|
+
|
|
103
|
+
## 5. `title` is **not** on the art rule
|
|
104
|
+
|
|
105
|
+
`resolveImg`'s rule reads as a general one about optional strings, and it is
|
|
106
|
+
not — it belongs to the function, and `title` never goes through it. So do not
|
|
107
|
+
extend §3's sweep to `title`, and do not read the table in §3 as saying anything
|
|
108
|
+
about it.
|
|
109
|
+
|
|
110
|
+
**The reason has changed since this section was first written, and the earlier
|
|
111
|
+
one is no longer true.** It used to be that a note's top-level `title` was
|
|
112
|
+
_simultaneously_ the page's heading and the shared source for an `affiliation`
|
|
113
|
+
item's `system.title`, so `title: null` did not fall back — it stringified, and
|
|
114
|
+
the compiled document shipped the literal string `"null"`. That collision is what
|
|
115
|
+
§6 removes: the top-level key is no longer a source for the item field at all.
|
|
116
|
+
|
|
117
|
+
So `title: null` is now simply a note declining to state a heading, and the site
|
|
118
|
+
emitter's `fm.title ?? name` falls back to `name.full` as it always did. It
|
|
119
|
+
reaches no document field and stringifies nothing.
|
|
120
|
+
|
|
121
|
+
`title: ""` still publishes a **deliberately blank heading** — which is what cost
|
|
122
|
+
fifteen `sohl-thalorna` notes their names (HeroicLands/sohl-thalorna#129) — and
|
|
123
|
+
nothing warns about it yet. Whether the frontmatter lint should is #218's, still
|
|
124
|
+
open, and deliberately not settled by the art-field warning in §3.
|
|
125
|
+
|
|
126
|
+
## 6. A note's own `title` no longer fills `system.title` (#218)
|
|
127
|
+
|
|
128
|
+
**No note edit, no URL change, and no compiled document moves.** An
|
|
129
|
+
`affiliation` item's `system.title` stops falling back to the note's own
|
|
130
|
+
top-level `title`. The two were never the same quantity — a note's `title` is
|
|
131
|
+
the heading its page publishes under, while `system.title` is the style of
|
|
132
|
+
address an office carries, Ajaw or Warden — and no note in any content tree
|
|
133
|
+
relied on the fallback, so `content-build package compile` emits byte-identical
|
|
134
|
+
`build/packs-json` for every consumer.
|
|
135
|
+
|
|
136
|
+
Only a `type: affiliation` note is affected, and only if it carries a top-level
|
|
137
|
+
`title` it meant as the item's field rather than as the page's heading:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
grep -rl '^type: affiliation' assets/content --include='*.md' \
|
|
141
|
+
| xargs grep -l '^title:'
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Anything that turns up wanted one of the two positions that describe the
|
|
145
|
+
_document_ rather than the note — `sohl.system.title`, or `sohl.title`, the
|
|
146
|
+
legacy in-block key most trees already write. A membership's title belongs on
|
|
147
|
+
the entry in the being's `sohl.items`, as `system.title`.
|
|
148
|
+
|
|
149
|
+
`data: { title: ... }` is not a position and never was: `title` is not a `data:`
|
|
150
|
+
property any note type declares, so `content-build lint` refuses it.
|
|
151
|
+
|
|
152
|
+
### Regenerate the item field reference
|
|
153
|
+
|
|
154
|
+
The generated page now prints, under each affected type's table, what the
|
|
155
|
+
top-level key of a non-shared field means instead — so an author reading the
|
|
156
|
+
table learns that writing `title:` at the top of a note will not fill this
|
|
157
|
+
field. Re-run the generator and commit the result, or a repository that checks
|
|
158
|
+
the page for staleness reports it stale:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
npx content-build docs item-fields --out <the path your repo uses>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Declaring your own non-shared field
|
|
165
|
+
|
|
166
|
+
A field in an `itemBuilders` `fields:` declaration may now carry
|
|
167
|
+
`topLevelMeans`, whose value is _what the note's top-level key of that name
|
|
168
|
+
means instead_. Declaring it removes the shared top-level position from that
|
|
169
|
+
field's resolution order:
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
{
|
|
173
|
+
name: "title",
|
|
174
|
+
to: "title",
|
|
175
|
+
...STRING,
|
|
176
|
+
default: "",
|
|
177
|
+
topLevelMeans: "the note's own title — the heading its page is published under",
|
|
178
|
+
describe: "The style of address the office carries.",
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The value is the reason rather than a bare flag on purpose: the next person
|
|
183
|
+
adding a field needs to know the question exists, and a boolean with a comment
|
|
184
|
+
beside it is two statements of one rule.
|
|
185
|
+
|
|
1
186
|
# Migrating to `@heroiclands/package-build` 12.0.0
|
|
2
187
|
|
|
3
188
|
**No note edit, no URL change, and one thing to check in the Hugo layer.** A
|
|
@@ -49,12 +234,18 @@ That is the shape a content catalog wants regardless — it groups by what a pag
|
|
|
49
234
|
_is_ rather than by where its file happened to be written — and it is what
|
|
50
235
|
`sohl`'s catalog layouts already do.
|
|
51
236
|
|
|
52
|
-
|
|
237
|
+
A site rendering through a **shared theme** has no layout of its own to edit. As
|
|
238
|
+
of 13.1.0 the section declares its query instead, and the theme runs it:
|
|
239
|
+
|
|
240
|
+
```yaml
|
|
241
|
+
sections:
|
|
242
|
+
weapongear: { title: Weapons, listType: weapongear }
|
|
243
|
+
user-guide: { title: User Guide, listType: doc, listSubType: userguide }
|
|
244
|
+
```
|
|
53
245
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
`landing: collection` is still refused by name (#202).
|
|
246
|
+
`listType` is the content type, not the section's name — the two need not agree,
|
|
247
|
+
and `listSubType` narrows a type whose genres share it. See
|
|
248
|
+
[What a section may declare](CONTENT.md#what-a-section-may-declare).
|
|
58
249
|
|
|
59
250
|
## What did not change
|
|
60
251
|
|