@heroiclands/package-build 14.0.0 → 16.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 +327 -0
- package/CONTENT.md +331 -19
- package/MIGRATING.md +185 -0
- package/bin/content-build.mjs +54 -0
- package/content-config.mjs +10 -0
- package/docs/content-format.md +21 -0
- package/engine/content-address.mjs +21 -36
- package/engine/content-index.mjs +439 -0
- package/engine/field-reference.mjs +29 -0
- package/engine/field-spec.mjs +25 -0
- package/engine/frontmatter-lint.mjs +171 -1
- package/engine/helpers.mjs +38 -12
- package/engine/homepage.mjs +9 -4
- package/engine/index.mjs +3 -0
- package/engine/item-registry.mjs +4 -1
- package/engine/macros.mjs +3 -1
- 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 +9 -0
- package/types/engine/content-address.d.mts +22 -34
- package/types/engine/content-index.d.mts +194 -0
- 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/index.d.mts +1 -0
- package/types/engine/site-build.d.mts +11 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,332 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 16.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- d734ac9: **Two embedded items on one actor may no longer share `(type, shortcode)`.**
|
|
8
|
+
`content-build lint` reports each collision as an error, at the later entry.
|
|
9
|
+
|
|
10
|
+
SoHL treats `(type, shortcode)` as a **logical identity** rather than a lookup
|
|
11
|
+
convenience: two documents of one type bearing one shortcode denote _the same
|
|
12
|
+
entity_, whatever their `_id`s or field values. It is unique within four scopes,
|
|
13
|
+
one of which is an actor's own embedded items — and the invariant exists to keep
|
|
14
|
+
that identity well-defined. Two colliding entries make "the same thing"
|
|
15
|
+
ambiguous, and every match that resolves by it — compendium↔world
|
|
16
|
+
reconciliation, archetype shadowing, `fvttFindItemByShortcode`, cohort
|
|
17
|
+
membership, expression and effect references — becomes unsound.
|
|
18
|
+
|
|
19
|
+
Nothing caught it. The compiler resolves each entry independently and
|
|
20
|
+
distinguishes the two only when seeding `_id`, so a collision compiled to two
|
|
21
|
+
documents with distinct ids and shipped unremarked:
|
|
22
|
+
|
|
23
|
+
```yaml
|
|
24
|
+
items:
|
|
25
|
+
- { shortcode: swim, type: skill, system: { masteryLevelBase: 30 } }
|
|
26
|
+
- { shortcode: swim, type: skill, initSkillMult: 1 }
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Neither entry overrides `system.shortcode`, so both inherit `swim` from the
|
|
30
|
+
template and compile to two `skill` items keyed `swim` on one actor.
|
|
31
|
+
|
|
32
|
+
**The rule is decidable from frontmatter alone**, which is why it is a lint and
|
|
33
|
+
not a compile step. An entry's effective key is `system.shortcode ?? shortcode`:
|
|
34
|
+
a top-level `shortcode` merely selects the template the entry is written from
|
|
35
|
+
and never reaches the document, while a template's own `system.shortcode` is its
|
|
36
|
+
address by construction. Neither the catalogue nor a compile is needed to know
|
|
37
|
+
what an entry will carry.
|
|
38
|
+
|
|
39
|
+
Two entries written from one template are still fine when the second says which
|
|
40
|
+
entity it is — `{ shortcode: Dgr, type: weapongear, name: "Dagger 2", system: { shortcode: Dgr2 } }`.
|
|
41
|
+
The key is the _pair_, so two different types may share a shortcode, and an
|
|
42
|
+
entry naming no key at all is left to the compiler, which already reports it.
|
|
43
|
+
|
|
44
|
+
**Major**, because a tree carrying a collision goes red on adoption. Known at
|
|
45
|
+
the time of writing: `Song-of-Heroic-Lands-FoundryVTT` 1 actor,
|
|
46
|
+
`sohl-thalorna` 15, `sohl-kethira-basic` 0.
|
|
47
|
+
- 2e0d32e: **A note's address has one name: `packageAddress`.** `engine/content-address.mjs`
|
|
48
|
+
exported it twice — as `contentAddress` and as `packageAddress` — and the two
|
|
49
|
+
bodies had become byte-identical:
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
// before // after
|
|
53
|
+
contentAddress(fm); // "doc-gear/" packageAddress(fm); // "doc-gear/"
|
|
54
|
+
packageAddress(fm); // "doc-gear/"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`contentAddress` is removed. Import `packageAddress` from
|
|
58
|
+
`@heroiclands/package-build/engine/content-address` instead; the two returned
|
|
59
|
+
the same string, so nothing else changes.
|
|
60
|
+
|
|
61
|
+
The names once meant different things — a note's address _within the content
|
|
62
|
+
tree_, and its address _relative to the package_. They could differ while a
|
|
63
|
+
`README.md` addressed its section rather than itself and while a page's URL was
|
|
64
|
+
derived from `name.full`; the first went with the landing rules (#204, #208) and
|
|
65
|
+
the second when a page's URL became its address (#181). What was left was two
|
|
66
|
+
exported names for one notion, on a public subpath, with `contentAddress`'s
|
|
67
|
+
documentation still describing an address "below the knowledgebase mount" that
|
|
68
|
+
it no longer returned.
|
|
69
|
+
|
|
70
|
+
`packageAddress` is the name that survives because it still says something true
|
|
71
|
+
and load-bearing: the string is measured **from the package**, so a caller
|
|
72
|
+
composing a URL or a manifest `path` prepends where the package is served. That
|
|
73
|
+
is the one qualification a reader of the call site needs, and it is exactly the
|
|
74
|
+
distinction the `url:` change in 15.0.0 turned on. Closes #226.
|
|
75
|
+
|
|
76
|
+
### Minor Changes
|
|
77
|
+
|
|
78
|
+
- e131f7b: Publish the note tree as a queryable index, instead of throwing every build's walk away.
|
|
79
|
+
|
|
80
|
+
Every content build parses every note's frontmatter — the pack compilers, the site
|
|
81
|
+
build, and the content-table expander each do it — and every one of them discards
|
|
82
|
+
the result. Nothing outside a build could therefore ask a question about the
|
|
83
|
+
content: _which beings carry no `kbcat`_, _what does this table actually select_,
|
|
84
|
+
_did that type rename leave anything behind_ each needed a throwaway script that
|
|
85
|
+
re-walked the tree. Eight dead tables shipped for weeks behind exactly that gap.
|
|
86
|
+
|
|
87
|
+
`content-build content-index` emits one JSON Lines record per note — the whole
|
|
88
|
+
frontmatter, plus a derived `file` (`path`, `folder`, `name`) and the configured
|
|
89
|
+
`package` — so a question is one line of `jq`, and so an editor, a CI check, or
|
|
90
|
+
another package's build can read the content without re-deriving it.
|
|
91
|
+
|
|
92
|
+
**The record is the note, not a projection of it.** Nothing is selected, flattened,
|
|
93
|
+
or renamed; a reader addresses `sohl.body.weight.base` because that is what the note
|
|
94
|
+
says, which is also exactly what a `dataview` query writes. The refusal to impose a
|
|
95
|
+
schema is deliberate: `sohl`'s frontmatter spreads 242 distinct leaf paths unevenly
|
|
96
|
+
over 15 types, from 9 on a `macro` to 72 on a `being`, so a fixed column set would
|
|
97
|
+
turn ordinary authoring into a schema migration. `package` and `file` are the two
|
|
98
|
+
derived keys, and a note carrying either is an error rather than a silent overwrite.
|
|
99
|
+
|
|
100
|
+
**Derived, disposable, byte-stable.** It writes to the new `paths.contentIndex`
|
|
101
|
+
(`build/content-index/<package>.jsonl`) — never `paths.stage`, which is mirrored
|
|
102
|
+
into a Foundry data root — so nothing may be authored against it and it need never
|
|
103
|
+
be committed. Regenerating costs a frontmatter parse rather than a build, which is
|
|
104
|
+
why it is a command of its own; and because rebuilding is the intended use, records
|
|
105
|
+
are ordered by content path with the note id breaking ties and every object's keys
|
|
106
|
+
are sorted at every depth, so a rebuild over an unchanged tree is a no-op. A tree
|
|
107
|
+
that yields no note is an error, not an empty index: a reader takes the file as
|
|
108
|
+
authoritative, and "this package has no content" is indistinguishable from a
|
|
109
|
+
mis-pointed tree.
|
|
110
|
+
|
|
111
|
+
**Every note's address, and every anchor it defines.** A record states
|
|
112
|
+
`address.slug` (what goes inside `[[…]]` locally) and `address.canonical` (the
|
|
113
|
+
package-qualified key the manifest files it under), plus every `{#slug}` anchor the
|
|
114
|
+
body declares — each with its heading name, level, **line in the file**, and the
|
|
115
|
+
`slug#anchor` link that reaches it. Neither address is new information, since both
|
|
116
|
+
derive from `type` and `shortcode`; what the fields add is the rule, derived through
|
|
117
|
+
the same `addressSlug` and `canonicalKey` the manifest and site build use, so an
|
|
118
|
+
index cannot disagree with either about where a note lives. The payoff is that a
|
|
119
|
+
wikilink — anchor and all — becomes checkable by lookup rather than by re-parsing
|
|
120
|
+
the tree, and an editor can jump to a section instead of searching for it. What
|
|
121
|
+
counts as an anchor is kept identical to what `splitPages` matches, and a test
|
|
122
|
+
asserts the two agree.
|
|
123
|
+
|
|
124
|
+
**A keyboard-typeable form of every name.** `nameAscii` states `name.full`
|
|
125
|
+
reduced to printable 7-bit ASCII — `Kûrbúl ¾-Helm` → `Kurbul 3/4-Helm`, `Kèthîra`
|
|
126
|
+
→ `Kethira`, `Ærling` → `AErling`, `Þorn` → `Thorn`, `Straße` → `Strasse`. Names
|
|
127
|
+
carry the setting's orthography and nobody types them, so anything searching or
|
|
128
|
+
completing over the index needs a form a keyboard produces; stating one means
|
|
129
|
+
every consumer folds the same way instead of each inventing its own and two
|
|
130
|
+
searches over the same data disagreeing. It transliterates rather than strips,
|
|
131
|
+
through the same `unidecode` table `slugify` already runs — so an ASCII name and
|
|
132
|
+
a slug cannot disagree about a character, and `Kûrbúl` does not become `Krbl`.
|
|
133
|
+
Emitted even when it equals the name, so a consumer never branches on whether a
|
|
134
|
+
name happened to be ASCII; `null` only when the note has no name. `aliasesAscii`
|
|
135
|
+
does the same for `name.aliases` in the authored order — an alias is the name a
|
|
136
|
+
reader is at least as likely to reach for (`Killer Whale` for an orca), so a
|
|
137
|
+
search has to match it too — and is an empty array rather than null when a note
|
|
138
|
+
has none.
|
|
139
|
+
|
|
140
|
+
`file.path` stays relative to the content root and is deliberately never absolute:
|
|
141
|
+
an absolute path is a fact about the machine that built the index, so it would break
|
|
142
|
+
byte-stability between checkouts and publish someone's home directory.
|
|
143
|
+
|
|
144
|
+
Closes #224.
|
|
145
|
+
|
|
146
|
+
## 15.0.0
|
|
147
|
+
|
|
148
|
+
### Major Changes
|
|
149
|
+
|
|
150
|
+
- 5c16c02: Let a note's art path say "unset" and "blank on purpose" with two different values.
|
|
151
|
+
|
|
152
|
+
`resolveImg` opened with `if (!raw) return ""`, and every caller then applied its
|
|
153
|
+
own default to the result with `||` — `resolveImg(fm.img) || itemArt(type)`. So
|
|
154
|
+
`""`, `null` and an absent key were one case: all three compiled to the type's
|
|
155
|
+
default art, and a note had no way to say _ship no image_ at all.
|
|
156
|
+
|
|
157
|
+
They are now three values with two meanings, the convention the project already
|
|
158
|
+
holds for an optional "not specified" DataModel string (`nullable, initial: null`,
|
|
159
|
+
so "unset" is one honest value rather than two):
|
|
160
|
+
|
|
161
|
+
| a note writes | it means | it compiles with |
|
|
162
|
+
| -------------- | ------------------------------------ | ---------------- |
|
|
163
|
+
| nothing at all | _unset_ — name me no art | the type default |
|
|
164
|
+
| `img: null` | the same thing, said out loud | the type default |
|
|
165
|
+
| `img: ""` | _blank on purpose_ — I want no image | no image |
|
|
166
|
+
|
|
167
|
+
**`resolveImg` returns `string | null`.** `null` for an unset path, `""` for a
|
|
168
|
+
deliberate blank, the translation half unchanged. Every caller pairs its default
|
|
169
|
+
with **nullish** coalescing: `sohl/items.mjs`, three in `sohl/actors.mjs` (`img`,
|
|
170
|
+
`portrait`, and the prototype token's `texture.src`), and `engine/macros.mjs`.
|
|
171
|
+
Not `||` — that collapses a deliberate blank back into the default and takes the
|
|
172
|
+
distinction away again, and it does so silently, because `""` is falsy.
|
|
173
|
+
`itemArt()` is unaffected: a registry entry with no art throws before the
|
|
174
|
+
translation, so its result is never the unset case.
|
|
175
|
+
|
|
176
|
+
**The default-art seam is a documented extension point, so this is the substance
|
|
177
|
+
of the change, not a detail.** A consuming repository that pairs art with its own
|
|
178
|
+
`itemBuilders` entry, or calls `resolveImg` from a builder of its own, gets the
|
|
179
|
+
new reading of `""` whether or not it asked for it — which is why the sweeps have
|
|
180
|
+
to come first. `sohl-thalorna` swept forty-five `img: ""` notes ahead of this
|
|
181
|
+
release (HeroicLands/sohl-thalorna#134) and `sohl-kethira-basic` eleven
|
|
182
|
+
`portrait: ""` beings (HeroicLands/sohl-kethira-basic#81); `sohl` authors neither,
|
|
183
|
+
and its 3,125 compiled documents are byte-identical across the change.
|
|
184
|
+
|
|
185
|
+
**Both art fields, because both go through `resolveImg`.** A being carries `img`
|
|
186
|
+
(its token art) and `portrait` (its sheet portrait) independently; `portrait` is
|
|
187
|
+
not a variant spelling of `img`, and the rule belongs to the translator rather
|
|
188
|
+
than to one of the keys reaching it. This is not theoretical: `sohl-kethira-basic`
|
|
189
|
+
writes `portrait: ""` on eleven beings and `img: ""` on none, so a change — or a
|
|
190
|
+
guard — keyed on `img` alone would have called that tree clean and dropped every
|
|
191
|
+
one of those portraits.
|
|
192
|
+
|
|
193
|
+
**A warning for the old spelling.** The frontmatter lint reports `img: ""` and
|
|
194
|
+
`portrait: ""` — in either authoring position — as the meaning-change they are, on
|
|
195
|
+
the pattern the `package:` and retired-alias sweeps set: a warning, because the
|
|
196
|
+
note still compiles, to a document that is merely iconless.
|
|
197
|
+
|
|
198
|
+
**`title` is deliberately not on this rule.** It reads as a general rule about
|
|
199
|
+
optional strings and it is not: on a `type: affiliation` note `title` is
|
|
200
|
+
_simultaneously_ a declared item field whose default is `""`
|
|
201
|
+
(`sohl/item-fields.mjs`), resolved from the very same shared top-level key the
|
|
202
|
+
site emitter reads as the page title. `title: null` therefore does not fall back —
|
|
203
|
+
it stringifies, and the compiled document ships the literal `"null"`. A `title` a
|
|
204
|
+
note does not want is written by omitting the key; the emitter's `fm.title ?? name`
|
|
205
|
+
is already correct and is untouched. The lint guard is `img`'s alone for the same
|
|
206
|
+
reason.
|
|
207
|
+
|
|
208
|
+
Delivers the `img` half of #218. The `title` half — the collision above — stays
|
|
209
|
+
open.
|
|
210
|
+
- 63dfcae: **A note's own `title` no longer fills an affiliation's `system.title`** (#218).
|
|
211
|
+
|
|
212
|
+
The two were never the same quantity. A note's top-level `title` is _the title
|
|
213
|
+
of the note_ — the heading its page is published under, which the site emitter
|
|
214
|
+
reads. An `affiliation` item's `system.title` is _the style of address the office
|
|
215
|
+
carries_ — Ajaw, Warden, a person's style within the body. They collided only in
|
|
216
|
+
spelling, and field resolution's third step, the shared top-level property, fed
|
|
217
|
+
the second from the first.
|
|
218
|
+
|
|
219
|
+
That step also answers **without** applying the field's default — only the
|
|
220
|
+
in-block step does — so an authored `title: null` reached the `String()` coercion
|
|
221
|
+
unguarded and compiled to the literal string `"null"`. Fifteen `sohl-thalorna`
|
|
222
|
+
notes shipped `"system": { "title": "null" }` that way.
|
|
223
|
+
|
|
224
|
+
**Nothing in any content tree relied on the fallback.** Across all three
|
|
225
|
+
consumers — 295 `affiliation` notes in `sohl-thalorna`, 28 in
|
|
226
|
+
`sohl-kethira-basic`, none in `sohl` — not one carries a non-empty top-level
|
|
227
|
+
`title`, and `content-build package compile` emits byte-identical
|
|
228
|
+
`build/packs-json` for all three. It is a major because the rule that decides a
|
|
229
|
+
consumer's compiled documents changed with no configuration to restore it, and
|
|
230
|
+
because the generated item-field reference moves (below).
|
|
231
|
+
|
|
232
|
+
**The field is still authorable**, at the two positions that describe the
|
|
233
|
+
document rather than the note: `sohl.system.title`, and the legacy in-block
|
|
234
|
+
`sohl.title` that most trees already write. A membership's title belongs on the
|
|
235
|
+
entry in a being's `sohl.items`, as its `system.title`. `data.title` is neither —
|
|
236
|
+
`title` is not a `data:` property any note type declares, so `content-build lint`
|
|
237
|
+
refuses it.
|
|
238
|
+
|
|
239
|
+
**Declaring the exemption:** a field in an `itemBuilders` `fields:` declaration
|
|
240
|
+
may now carry `topLevelMeans`, whose value is _what the note's top-level key of
|
|
241
|
+
that name means instead_. Declaring it removes the shared top-level position from
|
|
242
|
+
that field's resolution order, and the generated item-field reference prints the
|
|
243
|
+
reason beneath the type's table, so an author reading it learns that the
|
|
244
|
+
top-level key will not fill the field. A repository that commits that page should
|
|
245
|
+
regenerate it.
|
|
246
|
+
|
|
247
|
+
The value is the reason rather than a bare flag deliberately: a boolean would
|
|
248
|
+
record the decision and lose the case for it, and the next person adding a field
|
|
249
|
+
needs to know the question exists.
|
|
250
|
+
- a41b066: **A page states its address relative to the site root.** `site.base` is no
|
|
251
|
+
longer written into a page's Hugo `url:` front matter — only into the `href`s
|
|
252
|
+
this build renders and the base a link-manifest `path` is measured against:
|
|
253
|
+
|
|
254
|
+
```yaml
|
|
255
|
+
# emitted page
|
|
256
|
+
url: /doc-rulesintro/ # was: /sohl/doc-rulesintro/
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`site.base` was two quantities wearing one name. It is correctly _where the
|
|
260
|
+
package is served_ — the prefix on every rendered `href`, and the base a
|
|
261
|
+
manifest `path` is stripped against — and `/<contentPackage>/` is the right
|
|
262
|
+
default for that. It was also written verbatim into each page's `url:`, and that
|
|
263
|
+
is a different quantity: Hugo resolves `url` against `baseURL`, whose path for
|
|
264
|
+
every consumer that exists **already is** the package base. So the prefix was
|
|
265
|
+
written twice, and every content page — plus the homepage — published one
|
|
266
|
+
package segment too deep:
|
|
267
|
+
|
|
268
|
+
```text
|
|
269
|
+
/sohl/doc-rulesintro/ 404 /sohl/sohl/doc-rulesintro/ 200
|
|
270
|
+
/thalorna/being-afzndhprnzr/ 404 /thalorna/thalorna/being-afzndhprnzr/ 200
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Not one address a link manifest advertised resolved: **0 of `sohl`'s 2,988
|
|
274
|
+
entries**, and 0 of `thalorna`'s 2,585.
|
|
275
|
+
|
|
276
|
+
**One value fed two readers that need opposite framings**, which is why no
|
|
277
|
+
setting could fix it from a consumer: `site.base: "/"` bought the addresses and
|
|
278
|
+
short-changed the hrefs, and the default did the reverse. They are now separate,
|
|
279
|
+
and each reader gets the form it needs:
|
|
280
|
+
|
|
281
|
+
| Reader | Gets | Because |
|
|
282
|
+
| --------------------------------------------------- | --------------------------- | ------------------------------------------------------------------- |
|
|
283
|
+
| A page's own `url:` front matter | `/<type>-<shortcode>/` | Hugo prefixes the site's `baseURL` path to it |
|
|
284
|
+
| The address index a `[[wikilink]]` resolves through | `<base><type>-<shortcode>/` | A browser resolves the rendered `href` against nothing |
|
|
285
|
+
| A link-manifest `path` | `<type>-<shortcode>/` | Measured against `site.base` and stripped; unchanged, byte for byte |
|
|
286
|
+
|
|
287
|
+
The homepage moves with them: `homepageFrontmatter` states `/homepage-root/`,
|
|
288
|
+
and no longer takes a `base` (nor does `writeHomepages`' fourth argument, which
|
|
289
|
+
existed only to supply it). `trees` pages and section landings never stated a
|
|
290
|
+
`url:` and are untouched — they take their address from their path.
|
|
291
|
+
|
|
292
|
+
**Take this release and drop your `site.base`.** Both publishing consumers set
|
|
293
|
+
`site.base: "/"` as a stopgap
|
|
294
|
+
(HeroicLands/Song-of-Heroic-Lands-FoundryVTT#1813, HeroicLands/sohl-thalorna#131),
|
|
295
|
+
which bought the correct addresses at the cost of same-package body links
|
|
296
|
+
rendering `/doc-x/` rather than `/sohl/doc-x/` — dead either way, so nothing
|
|
297
|
+
regressed. With this release the stopgap is no longer needed and no longer
|
|
298
|
+
harmless: keeping it leaves those hrefs short. Delete the `base:` line and the
|
|
299
|
+
default is right for both halves.
|
|
300
|
+
|
|
301
|
+
**Verified against pristine `git archive origin/main` extractions of all three
|
|
302
|
+
consumers**, in both configurations, through `content-build site` **and** Hugo
|
|
303
|
+
0.165:
|
|
304
|
+
|
|
305
|
+
| Consumer | `site.base` | Page `url:` | Rendered path | Manifest entries resolving |
|
|
306
|
+
| ---------- | ----------- | --------------------- | ------------------------------ | -------------------------- |
|
|
307
|
+
| `sohl` | unset | `/doc-rulesintro/` | `/sohl/doc-rulesintro/` | 2,988 of 2,988 (was 0) |
|
|
308
|
+
| `sohl` | `"/"` | `/doc-rulesintro/` | `/sohl/doc-rulesintro/` | 2,988 of 2,988 |
|
|
309
|
+
| `thalorna` | unset | `/being-afzndhprnzr/` | `/thalorna/being-afzndhprnzr/` | 2,585 of 2,585 (was 0) |
|
|
310
|
+
| `thalorna` | `"/"` | `/being-afzndhprnzr/` | `/thalorna/being-afzndhprnzr/` | 2,585 of 2,585 |
|
|
311
|
+
|
|
312
|
+
No `/sohl/sohl/` or `/thalorna/thalorna/` path exists in either built tree, the
|
|
313
|
+
section landings, the content mount and the `dev-docs` tree pages all still
|
|
314
|
+
resolve, and with the stopgap dropped a same-package body link renders
|
|
315
|
+
`<a href=/sohl/doc-skills/>` again. `lint` and `links` are identical line for
|
|
316
|
+
line in every combination — `sohl` green, `sohl-thalorna` exactly as red as its
|
|
317
|
+
own content gap leaves it (1,983 lint findings, 122 link findings).
|
|
318
|
+
|
|
319
|
+
**Exactly one line per page changes, and nothing else does.** Of `sohl`'s 1,671
|
|
320
|
+
emitted files, 1,606 differ — the 1,605 content pages and the homepage — each by
|
|
321
|
+
its `url:` alone; the 46 tree pages and 19 landings are byte-identical, and
|
|
322
|
+
`build/manifests/sohl.json` is byte-identical. `sohl-kethira-basic` publishes a
|
|
323
|
+
homepage and nothing else (`publish.site: homepage`) and declares no
|
|
324
|
+
`site.base`: its console output is identical, its one emitted page moves from
|
|
325
|
+
`/kethira/kethira/homepage-root/` to `/kethira/homepage-root/`, and its landing
|
|
326
|
+
at `/kethira/` is unaffected.
|
|
327
|
+
|
|
328
|
+
Closes #217
|
|
329
|
+
|
|
3
330
|
## 14.0.0
|
|
4
331
|
|
|
5
332
|
### Major Changes
|