@heroiclands/package-build 22.3.0 → 22.4.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 +59 -0
- package/CONTENT.md +182 -306
- package/bin/content-build.mjs +40 -23
- package/bin/package-build.mjs +4 -3
- package/content-config.mjs +82 -180
- package/docs/api.md +27 -32
- package/docs/commands.md +54 -19
- package/docs/configuration.md +102 -132
- package/docs/content-format.md +25 -15
- package/docs/getting-started.md +5 -4
- package/docs/project-setup.md +1 -1
- package/engine/content-links.mjs +25 -60
- package/engine/diagnostics.mjs +2 -2
- package/engine/field-reference.mjs +141 -0
- package/engine/homepage.mjs +91 -172
- package/engine/metadata-index.mjs +7 -3
- package/engine/note-vocabulary.mjs +0 -20
- package/engine/pdf-build.mjs +2 -2
- package/engine/site-build.mjs +64 -225
- package/engine/site-config.mjs +15 -53
- package/engine/site-root.mjs +26 -70
- package/package.json +1 -1
- package/types/content-config.d.mts +14 -8
- package/types/engine/content-links.d.mts +12 -23
- package/types/engine/diagnostics.d.mts +2 -2
- package/types/engine/field-reference.d.mts +78 -0
- package/types/engine/homepage.d.mts +51 -91
- package/types/engine/note-vocabulary.d.mts +0 -12
- package/types/engine/site-build.d.mts +28 -129
- package/types/engine/site-config.d.mts +10 -19
- package/types/engine/site-root.d.mts +11 -36
package/bin/content-build.mjs
CHANGED
|
@@ -80,7 +80,7 @@ import {
|
|
|
80
80
|
formatUnaddressableFinding,
|
|
81
81
|
} from "../engine/metadata-index.mjs";
|
|
82
82
|
import { fetchNavigation, generateHugoConfig, writeHugoConfig } from "../engine/site-config.mjs";
|
|
83
|
-
import { renderItemFieldReference } from "../engine/field-reference.mjs";
|
|
83
|
+
import { renderItemFieldReference, renderItemFieldsPage } from "../engine/field-reference.mjs";
|
|
84
84
|
import { lintContentTree } from "../engine/content-lint.mjs";
|
|
85
85
|
import { lintContentCharset } from "../engine/content-charset.mjs";
|
|
86
86
|
import { lintContentHtml } from "../engine/content-html.mjs";
|
|
@@ -284,6 +284,14 @@ const argv = yargs(hideBin(process.argv))
|
|
|
284
284
|
* second implementation of the comparison. Staleness is a property of the whole
|
|
285
285
|
* generated file, so there is no line to name.
|
|
286
286
|
*
|
|
287
|
+
* **A destination under the content tree gets a note envelope**, so the walk
|
|
288
|
+
* that collects every note by its `type:` picks this one up too, rather than
|
|
289
|
+
* silently dropping it — `type: doc`, `subType: reference`, a `shortcode`
|
|
290
|
+
* derived from the destination's basename, `name.full` from the title, and
|
|
291
|
+
* `pack: none`; `docs.itemFields.frontmatter` deep-merges over it. `--check`
|
|
292
|
+
* compares the whole file, envelope included. A destination outside the
|
|
293
|
+
* content tree gets the page body alone, as before.
|
|
294
|
+
*
|
|
287
295
|
* `--out` and `--title` still override, for a one-off render.
|
|
288
296
|
*
|
|
289
297
|
* @returns {object} The yargs command module.
|
|
@@ -332,13 +340,27 @@ function docsCommand() {
|
|
|
332
340
|
const spec = config.docs?.itemFields ?? {};
|
|
333
341
|
const destination =
|
|
334
342
|
argv.out ?? (spec.out ? path.resolve(config.rootDir, spec.out) : null);
|
|
343
|
+
const pageTitle = title ?? spec.title ?? "Item Note Frontmatter";
|
|
335
344
|
|
|
336
|
-
const
|
|
337
|
-
|
|
345
|
+
const body = `${renderItemFieldReference({
|
|
346
|
+
title: pageTitle,
|
|
338
347
|
...(spec.preamble ? { preamble: spec.preamble } : {}),
|
|
339
348
|
generatedBy: "`content-build docs item-fields`",
|
|
340
349
|
config,
|
|
341
350
|
})}\n`;
|
|
351
|
+
// A page filed under the content tree is walked for its
|
|
352
|
+
// `type:` like any other note, so it needs the envelope; one
|
|
353
|
+
// filed anywhere else — a repository's own `docs/` — is not,
|
|
354
|
+
// and gets exactly the body it always did.
|
|
355
|
+
const page =
|
|
356
|
+
destination ?
|
|
357
|
+
renderItemFieldsPage(body, {
|
|
358
|
+
title: pageTitle,
|
|
359
|
+
destination,
|
|
360
|
+
contentRoot: config.paths.content,
|
|
361
|
+
frontmatter: spec.frontmatter,
|
|
362
|
+
})
|
|
363
|
+
: body;
|
|
342
364
|
|
|
343
365
|
if (check) {
|
|
344
366
|
if (!destination) {
|
|
@@ -1484,18 +1506,17 @@ function pdfCommand() {
|
|
|
1484
1506
|
* The sibling of `package compile`: the same tree, rendered as pages instead of
|
|
1485
1507
|
* compiled into packs. Everything a consumer would otherwise write for itself —
|
|
1486
1508
|
* the walk, the address derivation, the address index, table expansion,
|
|
1487
|
-
* wikilink resolution, code-fence protection
|
|
1488
|
-
*
|
|
1489
|
-
*
|
|
1490
|
-
*
|
|
1509
|
+
* wikilink resolution, code-fence protection and the foreign-manifest merge —
|
|
1510
|
+
* happens here, from configuration. So does the Hugo configuration: the whole
|
|
1511
|
+
* source tree Hugo reads lands under `build/hugo/`, and the consumer's script
|
|
1512
|
+
* runs Hugo over it.
|
|
1491
1513
|
*
|
|
1492
1514
|
* **The Hugo configuration is generated before anything is written.** Its
|
|
1493
1515
|
* sources — `package.json`'s `homepage`, the cached navigation, the installed
|
|
1494
1516
|
* theme — are each a way the build can fail, and failing before the output
|
|
1495
|
-
* tree is cleared leaves the last good site in place to be looked at.
|
|
1496
|
-
*
|
|
1497
|
-
*
|
|
1498
|
-
* `tags:` — and that is not known until then.
|
|
1517
|
+
* tree is cleared leaves the last good site in place to be looked at. Nothing
|
|
1518
|
+
* the walk reads changes it: a site renders its homepage and its pages, and
|
|
1519
|
+
* the kinds Hugo disables are the same on every site.
|
|
1499
1520
|
*
|
|
1500
1521
|
* **Each gate is reported and the run stops at the first that fires.** They are
|
|
1501
1522
|
* ordered so the report names the cause rather than its symptoms: an unusable
|
|
@@ -1513,14 +1534,12 @@ function siteCommand() {
|
|
|
1513
1534
|
handler: async () => {
|
|
1514
1535
|
try {
|
|
1515
1536
|
const config = loadPackConfig();
|
|
1516
|
-
// Generated
|
|
1517
|
-
//
|
|
1518
|
-
//
|
|
1519
|
-
//
|
|
1520
|
-
//
|
|
1521
|
-
|
|
1522
|
-
// so the value actually written is regenerated after it runs.
|
|
1523
|
-
generateHugoConfig(config);
|
|
1537
|
+
// Generated before the walk, to fail fast on a missing or
|
|
1538
|
+
// mismatched source — `homepage`, the manifest title, the
|
|
1539
|
+
// cached navigation, the installed theme — while the last
|
|
1540
|
+
// good site is still in place to be looked at. Written only
|
|
1541
|
+
// once the walk has succeeded.
|
|
1542
|
+
const hugo = generateHugoConfig(config);
|
|
1524
1543
|
const result = buildSite({
|
|
1525
1544
|
config,
|
|
1526
1545
|
sqlTables: await prepareTreeSqlTables(config.paths.content, {
|
|
@@ -1631,11 +1650,9 @@ function siteCommand() {
|
|
|
1631
1650
|
const s = result.stats;
|
|
1632
1651
|
log.info(
|
|
1633
1652
|
`wrote ${s.homepages ?? 0} homepage(s) + ` +
|
|
1634
|
-
`${s.content ?? 0} content page(s)
|
|
1635
|
-
`${
|
|
1636
|
-
`landing(s) to ${path.relative(process.cwd(), s.out)}`,
|
|
1653
|
+
`${s.content ?? 0} content page(s) to ` +
|
|
1654
|
+
`${path.relative(process.cwd(), s.out)}`,
|
|
1637
1655
|
);
|
|
1638
|
-
const hugo = generateHugoConfig(config, { hasTags: result.hasTags });
|
|
1639
1656
|
const { file } = writeHugoConfig(config, hugo);
|
|
1640
1657
|
log.info(`wrote ${path.relative(process.cwd(), file)}`);
|
|
1641
1658
|
} catch (err) {
|
package/bin/package-build.mjs
CHANGED
|
@@ -455,15 +455,16 @@ function manifestCommand() {
|
|
|
455
455
|
* `package-build site-root` — the deployment's root files.
|
|
456
456
|
*
|
|
457
457
|
* Hugo owns everything under the `/<package>/` prefix; this owns what sits
|
|
458
|
-
* beside it, which is
|
|
459
|
-
*
|
|
458
|
+
* beside it, which is what Cloudflare Pages reads from the uploaded directory
|
|
459
|
+
* and nowhere else: `_headers`, and no `_redirects`, since the prefix root is
|
|
460
|
+
* the homepage.
|
|
460
461
|
*
|
|
461
462
|
* @returns {object} The yargs command.
|
|
462
463
|
*/
|
|
463
464
|
function siteRootCommand() {
|
|
464
465
|
return {
|
|
465
466
|
command: "site-root",
|
|
466
|
-
describe: "Write the deployment's _headers
|
|
467
|
+
describe: "Write the deployment's _headers",
|
|
467
468
|
builder: (y) =>
|
|
468
469
|
y.option("out", {
|
|
469
470
|
type: "string",
|
package/content-config.mjs
CHANGED
|
@@ -256,10 +256,9 @@ export const DEFAULT_ADDRESS_SCHEME = Object.freeze({
|
|
|
256
256
|
* and the default.
|
|
257
257
|
*
|
|
258
258
|
* - `homepage` — the authored homepage, and **no other page**. The content tree
|
|
259
|
-
* is not walked for pages,
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
* knowledgebase and the section landings.
|
|
259
|
+
* is not walked for pages, and nothing serves a page for its addresses.
|
|
260
|
+
* - `content` — the homepage *plus* every page the content tree publishes, one
|
|
261
|
+
* per note.
|
|
263
262
|
*
|
|
264
263
|
* **Homepage-only is a first-class mode, not an accommodation.**
|
|
265
264
|
* `sohl-kethira-basic` (unofficial Hârn fan material under Keléstia Productions'
|
|
@@ -528,6 +527,11 @@ export function publishesContentPages(config) {
|
|
|
528
527
|
* Without it the page goes to stdout.
|
|
529
528
|
* @property {string[]} [preamble] Lines between the generated banner and the
|
|
530
529
|
* first table. Markdown, emitted verbatim.
|
|
530
|
+
* @property {Record<string, unknown>} [frontmatter] Further note frontmatter,
|
|
531
|
+
* deep-merged over the generated envelope
|
|
532
|
+
* (`type: doc`, `subType: reference`,
|
|
533
|
+
* `shortcode`, `name.full`, `pack: none`)
|
|
534
|
+
* when `out` is under the content tree.
|
|
531
535
|
*/
|
|
532
536
|
|
|
533
537
|
/**
|
|
@@ -761,23 +765,17 @@ const SITE_KEYS = [
|
|
|
761
765
|
"assets",
|
|
762
766
|
"description",
|
|
763
767
|
"packages",
|
|
764
|
-
"sections",
|
|
765
|
-
"landing",
|
|
766
768
|
"pass",
|
|
767
769
|
"passOptions",
|
|
768
|
-
"backfillSections",
|
|
769
|
-
"list",
|
|
770
770
|
"notfound",
|
|
771
771
|
"hugo",
|
|
772
772
|
];
|
|
773
|
-
const SITE_LIST_KEYS = ["shortcodes"];
|
|
774
773
|
const SITE_NOTFOUND_KEYS = ["tagline", "sitenoun", "heroimage", "links"];
|
|
775
774
|
const SITE_NOTFOUND_LINK_KEYS = ["title", "url", "text"];
|
|
776
775
|
const PDF_KEYS = ["title", "subtitle", "document", "out", "front", "fonts", "iconFonts", "binary"];
|
|
777
776
|
const PDF_FONT_KEYS = ["serif", "sans", "mono", "path"];
|
|
778
777
|
const EMPTY_PDF_FONTS = Object.freeze({ serif: "", sans: "", mono: "", path: "" });
|
|
779
|
-
const
|
|
780
|
-
const DOC_PAGE_KEYS = ["title", "out", "preamble"];
|
|
778
|
+
const DOC_PAGE_KEYS = ["title", "out", "preamble", "frontmatter"];
|
|
781
779
|
const RELATIONSHIP_KINDS = ["systems", "requires", "recommends", "conflicts"];
|
|
782
780
|
const RELATIONSHIP_KEYS = [
|
|
783
781
|
"id",
|
|
@@ -1435,6 +1433,12 @@ function normalizeDocPage(value, where) {
|
|
|
1435
1433
|
}),
|
|
1436
1434
|
);
|
|
1437
1435
|
}
|
|
1436
|
+
if (input.frontmatter !== undefined) {
|
|
1437
|
+
if (!isPlainObject(input.frontmatter)) {
|
|
1438
|
+
fail(`${where}.frontmatter`, "must be a mapping");
|
|
1439
|
+
}
|
|
1440
|
+
out.frontmatter = deepFreeze({ .../** @type {object} */ (input.frontmatter) });
|
|
1441
|
+
}
|
|
1438
1442
|
return Object.freeze(out);
|
|
1439
1443
|
}
|
|
1440
1444
|
|
|
@@ -1454,102 +1458,6 @@ function normalizeDocs(value) {
|
|
|
1454
1458
|
});
|
|
1455
1459
|
}
|
|
1456
1460
|
|
|
1457
|
-
/**
|
|
1458
|
-
* One section's landing metadata — what a section says about itself on the
|
|
1459
|
-
* `_index.md` this build generates for it.
|
|
1460
|
-
*
|
|
1461
|
-
* A generated landing is the *only* place a section can speak, and it
|
|
1462
|
-
* is the only place a section **exists**: a content page is addressed
|
|
1463
|
-
* `(type, shortcode)` and written flat under the mount, so no page creates a
|
|
1464
|
-
* directory and nothing else makes `<prefix><section>/` answer. This is
|
|
1465
|
-
* therefore the whole vocabulary, and it is deliberately a **closed** one.
|
|
1466
|
-
*
|
|
1467
|
-
* The alternative — passing whatever a section declared straight through, as
|
|
1468
|
-
* `site.landing` does — was weighed and refused. `landing` is written once, for
|
|
1469
|
-
* the mount, and its keys are one landing template's own; a section entry is
|
|
1470
|
-
* written fourteen to twenty times per build against a contract every package
|
|
1471
|
-
* and every section shares. Unbounded there, a mistyped `descrption:` publishes
|
|
1472
|
-
* into front matter, is read by nobody, and says nothing to anyone — which is
|
|
1473
|
-
* the same failure, moved one step downstream where no build can
|
|
1474
|
-
* see it. So the keys are named here, and the writers emit what this produced
|
|
1475
|
-
* rather than transcribing a second list of their own.
|
|
1476
|
-
*
|
|
1477
|
-
* `banner` and `description` are optional — the hero images are external assets
|
|
1478
|
-
* and not every section has one, and a section may reasonably have nothing to
|
|
1479
|
-
* add to its title. Each is left off entirely rather than written as
|
|
1480
|
-
* `undefined`, which is not a value YAML can carry.
|
|
1481
|
-
*
|
|
1482
|
-
* **`listType` / `listSubType` say what the section lists.** A section's
|
|
1483
|
-
* directory holds nothing but the `_index.md` written here, so a layout
|
|
1484
|
-
* reading Hugo's `.Pages` finds
|
|
1485
|
-
* no members and renders an empty landing. The membership survives in this map
|
|
1486
|
-
* and nowhere a theme can reach it, so the landing states it and a layout
|
|
1487
|
-
* substitutes the equivalent `site.RegularPages` query — the same one `sohl`'s
|
|
1488
|
-
* catalog layouts already run, which is why `sohl`'s landings never broke.
|
|
1489
|
-
*
|
|
1490
|
-
* They are two keys of their own rather than `type` / `subType` because `type`
|
|
1491
|
-
* on an `_index.md` is **Hugo's own layout selector**: verified against Hugo
|
|
1492
|
-
* 0.165, a section landing carrying `type: doc` renders through
|
|
1493
|
-
* `layouts/doc/list.html` rather than the default list template, so spelling
|
|
1494
|
-
* the content type there would silently change which template serves the
|
|
1495
|
-
* landing. (This build already uses that behaviour deliberately, for the
|
|
1496
|
-
* mount's own landing.)
|
|
1497
|
-
*
|
|
1498
|
-
* Both are checked as **address segments**, which is the trap this came from:
|
|
1499
|
-
* a section is named for the URL a consumer chose and a subType is an address
|
|
1500
|
-
* segment, and the two need not agree — `/sohl/kb/user-guide/` is the section,
|
|
1501
|
-
* `userguide` the subType. Copying the section's name into the
|
|
1502
|
-
* declaration would select no page at all, and an empty landing reported by
|
|
1503
|
-
* nobody is the failure being fixed. A `listSubType` with no `listType` is
|
|
1504
|
-
* refused for the same reason: a subType is only distinguishing *within* a
|
|
1505
|
-
* type — `rules`, `userguide` and `reference` are all `doc` — so alone it names
|
|
1506
|
-
* no query.
|
|
1507
|
-
*
|
|
1508
|
-
* @param {unknown} value - The declared entry.
|
|
1509
|
-
* @param {string} where - Dotted path, for the error.
|
|
1510
|
-
* @returns {Readonly<{title: string, banner?: string, description?: string,
|
|
1511
|
-
* listType?: string, listSubType?: string}>}
|
|
1512
|
-
*/
|
|
1513
|
-
function normalizeSectionMeta(value, where) {
|
|
1514
|
-
if (!isPlainObject(value)) fail(where, "must be a mapping");
|
|
1515
|
-
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1516
|
-
rejectUnknownKeys(input, SECTION_META_KEYS, `${where}.`);
|
|
1517
|
-
const out = { title: requireNonEmptyString(input.title, `${where}.title`) };
|
|
1518
|
-
if (input.banner !== undefined) {
|
|
1519
|
-
out.banner = requireNonEmptyString(input.banner, `${where}.banner`);
|
|
1520
|
-
}
|
|
1521
|
-
if (input.description !== undefined) {
|
|
1522
|
-
out.description = requireNonEmptyString(input.description, `${where}.description`);
|
|
1523
|
-
}
|
|
1524
|
-
for (const key of ["listType", "listSubType"]) {
|
|
1525
|
-
if (input[key] === undefined) continue;
|
|
1526
|
-
const segment = requireNonEmptyString(input[key], `${where}.${key}`);
|
|
1527
|
-
if (!isAddressSegment(segment)) {
|
|
1528
|
-
fail(
|
|
1529
|
-
`${where}.${key}`,
|
|
1530
|
-
`is \`${segment}\`, which is not lowercase alphanumeric. It names a ` +
|
|
1531
|
-
"content type or subType, and those are address segments " +
|
|
1532
|
-
`(${ADDRESS_SEGMENT_PATTERN.source}) — not the section's ` +
|
|
1533
|
-
"own name, which is a URL this site chose and need not " +
|
|
1534
|
-
"match (`user-guide` is the section, `userguide` the " +
|
|
1535
|
-
"subType). A value no page carries selects nothing and " +
|
|
1536
|
-
"leaves the landing empty",
|
|
1537
|
-
);
|
|
1538
|
-
}
|
|
1539
|
-
out[key] = segment;
|
|
1540
|
-
}
|
|
1541
|
-
if (out.listSubType !== undefined && out.listType === undefined) {
|
|
1542
|
-
fail(
|
|
1543
|
-
`${where}.listSubType`,
|
|
1544
|
-
"is declared without a `listType`. A subType tells pages apart " +
|
|
1545
|
-
"only within a type — `rules`, `userguide` and `reference` " +
|
|
1546
|
-
"are all `doc` — so on its own it names no query for a layout " +
|
|
1547
|
-
"to run",
|
|
1548
|
-
);
|
|
1549
|
-
}
|
|
1550
|
-
return Object.freeze(out);
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
1461
|
/**
|
|
1554
1462
|
* The asset host the website resolves a pathname against.
|
|
1555
1463
|
*
|
|
@@ -1609,23 +1517,6 @@ function normalizeSiteDescription(value) {
|
|
|
1609
1517
|
return description;
|
|
1610
1518
|
}
|
|
1611
1519
|
|
|
1612
|
-
/**
|
|
1613
|
-
* A map of section name → landing metadata.
|
|
1614
|
-
*
|
|
1615
|
-
* @param {unknown} value - The declared mapping.
|
|
1616
|
-
* @param {string} where - Dotted path, for the error.
|
|
1617
|
-
* @returns {Readonly<Record<string, object>>}
|
|
1618
|
-
*/
|
|
1619
|
-
function normalizeSectionMap(value, where) {
|
|
1620
|
-
if (value === undefined) return Object.freeze({});
|
|
1621
|
-
if (!isPlainObject(value)) fail(where, "must be a mapping");
|
|
1622
|
-
const out = {};
|
|
1623
|
-
for (const [name, meta] of Object.entries(/** @type {Record<string, unknown>} */ (value))) {
|
|
1624
|
-
out[name] = normalizeSectionMeta(meta, `${where}.${name}`);
|
|
1625
|
-
}
|
|
1626
|
-
return Object.freeze(out);
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
1520
|
/**
|
|
1630
1521
|
* Hugo keys a repository may **not** declare under `site.hugo`, because the
|
|
1631
1522
|
* site build generates them and would only overwrite what was written.
|
|
@@ -1647,35 +1538,24 @@ export const DERIVED_HUGO_KEYS = Object.freeze({
|
|
|
1647
1538
|
contentDir: "the fixed content mount, `build/hugo/content`",
|
|
1648
1539
|
themesDir: "where `@heroiclands/hugo-theme` is installed",
|
|
1649
1540
|
theme: "the installed `@heroiclands/hugo-theme`",
|
|
1650
|
-
disableKinds:
|
|
1651
|
-
|
|
1652
|
-
|
|
1541
|
+
disableKinds:
|
|
1542
|
+
"the toolchain, which renders a site as its homepage and its pages: " +
|
|
1543
|
+
"`section`, `taxonomy`, `term` and `RSS` are disabled on every site",
|
|
1544
|
+
taxonomies:
|
|
1545
|
+
"the toolchain, which renders a site as its homepage and its pages: no " +
|
|
1546
|
+
"taxonomy is declared, because `taxonomy` and `term` are disabled kinds",
|
|
1547
|
+
outputs:
|
|
1548
|
+
"the toolchain, which renders a site as its homepage and its pages: no " +
|
|
1549
|
+
"output format is declared, because every listing kind is disabled",
|
|
1653
1550
|
"params.description": "`site.description`",
|
|
1654
1551
|
"params.author": "package.json `author`",
|
|
1655
1552
|
"params.cdnBaseURL": "`site.assets`",
|
|
1656
1553
|
"params.brand": "the organisation's brand links, in `engine/site-config.mjs`",
|
|
1657
|
-
"params.list": "`site.list`",
|
|
1658
1554
|
"params.notfound": "`site.notfound`",
|
|
1659
1555
|
"markup.goldmark.renderer.unsafe": "the toolchain, whose pages carry raw HTML",
|
|
1660
1556
|
menu: "the navigation `content-build deps fetch` caches from heroiclands.org",
|
|
1661
1557
|
});
|
|
1662
1558
|
|
|
1663
|
-
/**
|
|
1664
|
-
* The `site.list` block — how a listing page renders.
|
|
1665
|
-
*
|
|
1666
|
-
* @param {unknown} value - The block, or `undefined`.
|
|
1667
|
-
* @returns {Readonly<{shortcodes: boolean}>} It, frozen, with every default filled.
|
|
1668
|
-
*/
|
|
1669
|
-
function normalizeSiteList(value) {
|
|
1670
|
-
if (value === undefined) return Object.freeze({ shortcodes: false });
|
|
1671
|
-
if (!isPlainObject(value)) fail("site.list", "must be a mapping");
|
|
1672
|
-
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1673
|
-
rejectUnknownKeys(input, SITE_LIST_KEYS, "site.list.");
|
|
1674
|
-
return Object.freeze({
|
|
1675
|
-
shortcodes: optionalBoolean(input.shortcodes, "site.list.shortcodes", false),
|
|
1676
|
-
});
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
1559
|
/**
|
|
1680
1560
|
* The `site.notfound` block — the wording of the "page not found" page.
|
|
1681
1561
|
*
|
|
@@ -1753,25 +1633,59 @@ function normalizeSiteHugo(value) {
|
|
|
1753
1633
|
return Object.freeze(structuredClone(input));
|
|
1754
1634
|
}
|
|
1755
1635
|
|
|
1636
|
+
/**
|
|
1637
|
+
* The keys that asked the site build to write an index between the homepage
|
|
1638
|
+
* and the pages, each refused with the one message.
|
|
1639
|
+
*
|
|
1640
|
+
* A site is its homepage and its pages. Each of these keys asks for a
|
|
1641
|
+
* generated listing between them — `sections` (with `listType` and
|
|
1642
|
+
* `listSubType` inside an entry) a type-wide alphabetical one per entry,
|
|
1643
|
+
* `backfillSections` one for every other directory under the mount, `landing`
|
|
1644
|
+
* the mount's own, `list` how such a listing renders. None of those is a page
|
|
1645
|
+
* anyone chose the contents of, and every one of them is authored instead: a
|
|
1646
|
+
* `doc` note, addressed by its shortcode, carrying a content table over the
|
|
1647
|
+
* notes it introduces, with the columns it chooses.
|
|
1648
|
+
*
|
|
1649
|
+
* Refused rather than ignored, for the reason {@link RETIRED_ADDRESS_KEYS}
|
|
1650
|
+
* gives: a key left ignored reads to its author as though it still works.
|
|
1651
|
+
*
|
|
1652
|
+
* @type {readonly string[]}
|
|
1653
|
+
*/
|
|
1654
|
+
const SITE_INDEX_KEYS = Object.freeze(["sections", "landing", "backfillSections", "list"]);
|
|
1655
|
+
|
|
1656
|
+
/**
|
|
1657
|
+
* The message every key in {@link SITE_INDEX_KEYS} is refused with.
|
|
1658
|
+
*
|
|
1659
|
+
* @type {string}
|
|
1660
|
+
*/
|
|
1661
|
+
const SITE_INDEX_MESSAGE =
|
|
1662
|
+
"is retired — a site is its homepage and its pages, and any index between " +
|
|
1663
|
+
"them is a `doc` note: write one with `type: doc`, a `shortcode` and " +
|
|
1664
|
+
"`pack: none`, carrying a content table over the notes it lists, and link " +
|
|
1665
|
+
"it from the homepage. Nothing is generated between the homepage and the " +
|
|
1666
|
+
"pages, so delete the key";
|
|
1667
|
+
|
|
1756
1668
|
/**
|
|
1757
1669
|
* The `site` section — how this repository frames the website it publishes.
|
|
1758
1670
|
*
|
|
1759
|
-
* Everything here is *framing*:
|
|
1760
|
-
*
|
|
1761
|
-
*
|
|
1762
|
-
*
|
|
1763
|
-
*
|
|
1764
|
-
*
|
|
1765
|
-
*
|
|
1766
|
-
*
|
|
1767
|
-
*
|
|
1768
|
-
* **What the site publishes is the content tree, and nothing
|
|
1769
|
-
* page of documentation is a note — `type: doc`, addressed
|
|
1770
|
-
* compiling into no document where it says `pack: none` —
|
|
1771
|
-
* second mechanism for mounting a directory of markdown, and a
|
|
1772
|
-
* that names one (`site.trees`, and the `site.readmeSections`
|
|
1773
|
-
* such a tree's landing) is refused with a message saying where
|
|
1774
|
-
* goes instead.
|
|
1671
|
+
* Everything here is *framing*: which named pass bundle supplies the
|
|
1672
|
+
* repository's own body rewrites, and the residue of the generated Hugo
|
|
1673
|
+
* configuration that is genuinely this repository's own. Where the Hugo tree
|
|
1674
|
+
* is written is not a choice: `content-build site` writes it under
|
|
1675
|
+
* `build/hugo/`, and a `site.out` is refused by name. How a page gets its
|
|
1676
|
+
* **address** is deliberately not here either — that is `publish.address`,
|
|
1677
|
+
* shared with the link manifest so the two cannot disagree about where a
|
|
1678
|
+
* page is.
|
|
1679
|
+
*
|
|
1680
|
+
* **What the site publishes is the homepage and the content tree, and nothing
|
|
1681
|
+
* beside them.** A page of documentation is a note — `type: doc`, addressed
|
|
1682
|
+
* by its shortcode, compiling into no document where it says `pack: none` —
|
|
1683
|
+
* so there is no second mechanism for mounting a directory of markdown, and a
|
|
1684
|
+
* configuration that names one (`site.trees`, and the `site.readmeSections`
|
|
1685
|
+
* that titled such a tree's landing) is refused with a message saying where
|
|
1686
|
+
* the page goes instead. An index between the homepage and the pages is a
|
|
1687
|
+
* `doc` note too, so the keys that asked the build to generate one
|
|
1688
|
+
* ({@link SITE_INDEX_KEYS}) are refused the same way.
|
|
1775
1689
|
*
|
|
1776
1690
|
* @param {unknown} value - The `site` block, or `undefined`.
|
|
1777
1691
|
* @returns {Readonly<object>} It, frozen, with every default filled.
|
|
@@ -1782,12 +1696,8 @@ function normalizeSite(value) {
|
|
|
1782
1696
|
assets: "",
|
|
1783
1697
|
description: "",
|
|
1784
1698
|
packages: Object.freeze([]),
|
|
1785
|
-
sections: Object.freeze({}),
|
|
1786
|
-
landing: null,
|
|
1787
1699
|
pass: "",
|
|
1788
1700
|
passOptions: Object.freeze({}),
|
|
1789
|
-
backfillSections: false,
|
|
1790
|
-
list: Object.freeze({ shortcodes: false }),
|
|
1791
1701
|
notfound: null,
|
|
1792
1702
|
hugo: Object.freeze({}),
|
|
1793
1703
|
});
|
|
@@ -1813,10 +1723,16 @@ function normalizeSite(value) {
|
|
|
1813
1723
|
`site.${key}`,
|
|
1814
1724
|
"is retired — a page is a note in the content tree. Give each page " +
|
|
1815
1725
|
"`type: doc`, a `shortcode` and `pack: none`, file it under " +
|
|
1816
|
-
"`assets/content/`, and
|
|
1817
|
-
"`
|
|
1726
|
+
"`assets/content/`, and link it from the homepage or from a " +
|
|
1727
|
+
"`doc` note that indexes it",
|
|
1818
1728
|
);
|
|
1819
1729
|
}
|
|
1730
|
+
// And by name, for the same reason: an index between the homepage and
|
|
1731
|
+
// the pages is a `doc` note, and the useful thing to say is that.
|
|
1732
|
+
for (const key of SITE_INDEX_KEYS) {
|
|
1733
|
+
if (input[key] === undefined) continue;
|
|
1734
|
+
fail(`site.${key}`, SITE_INDEX_MESSAGE);
|
|
1735
|
+
}
|
|
1820
1736
|
rejectUnknownKeys(input, SITE_KEYS, "site.");
|
|
1821
1737
|
|
|
1822
1738
|
let packages = [];
|
|
@@ -1827,30 +1743,16 @@ function normalizeSite(value) {
|
|
|
1827
1743
|
packages = input.packages.map((p, i) => requireNonEmptyString(p, `site.packages[${i}]`));
|
|
1828
1744
|
}
|
|
1829
1745
|
|
|
1830
|
-
let landing = null;
|
|
1831
|
-
if (input.landing !== undefined) {
|
|
1832
|
-
if (!isPlainObject(input.landing)) {
|
|
1833
|
-
fail("site.landing", "must be a mapping");
|
|
1834
|
-
}
|
|
1835
|
-
// Passed through rather than validated field by field: it is Hugo
|
|
1836
|
-
// frontmatter, whose vocabulary is the theme's and not this package's.
|
|
1837
|
-
landing = Object.freeze({ ...input.landing });
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
1746
|
return Object.freeze({
|
|
1841
1747
|
base: input.base === undefined ? "" : requireNonEmptyString(input.base, "site.base"),
|
|
1842
1748
|
assets: normalizeSiteAssets(input.assets),
|
|
1843
1749
|
description: normalizeSiteDescription(input.description),
|
|
1844
1750
|
packages: Object.freeze(packages),
|
|
1845
|
-
sections: normalizeSectionMap(input.sections, "site.sections"),
|
|
1846
|
-
landing,
|
|
1847
1751
|
pass: input.pass === undefined ? "" : requireNonEmptyString(input.pass, "site.pass"),
|
|
1848
1752
|
passOptions:
|
|
1849
1753
|
input.passOptions === undefined ?
|
|
1850
1754
|
Object.freeze({})
|
|
1851
1755
|
: Object.freeze({ ...input.passOptions }),
|
|
1852
|
-
backfillSections: optionalBoolean(input.backfillSections, "site.backfillSections", false),
|
|
1853
|
-
list: normalizeSiteList(input.list),
|
|
1854
1756
|
notfound: normalizeSiteNotfound(input.notfound),
|
|
1855
1757
|
hugo: normalizeSiteHugo(input.hugo),
|
|
1856
1758
|
});
|
package/docs/api.md
CHANGED
|
@@ -308,20 +308,19 @@ Schema fields a note may never author, because the document writes them during p
|
|
|
308
308
|
|
|
309
309
|
The package homepage: a note that compiles to a page rather than a compendium document, hand-authored at the conventional shortcode `root`, checked for its address fields and for uniqueness across the tree.
|
|
310
310
|
|
|
311
|
-
| Export | Signature | Returns
|
|
312
|
-
| ---------------------------- | ------------------------------------- |
|
|
313
|
-
| `HOMEPAGE_TYPE` | `const HOMEPAGE_TYPE` | —
|
|
314
|
-
| `HOMEPAGE_FIELDS` | `const HOMEPAGE_FIELDS` | —
|
|
315
|
-
| `HOMEPAGE_SHORTCODE` | `const HOMEPAGE_SHORTCODE` | —
|
|
316
|
-
| `
|
|
317
|
-
| `isHomepage` | `isHomepage(fm)` | `boolean`
|
|
318
|
-
| `HOMEPAGE_REFUSED_FIELDS` | `const HOMEPAGE_REFUSED_FIELDS` | —
|
|
319
|
-
| `checkHomepageAddressFields` | `checkHomepageAddressFields(fm, ...)` | one finding per issue
|
|
320
|
-
| `checkHomepageCount` | `checkHomepageCount(found, ...)` | one finding per offending note
|
|
321
|
-
| `homepageTitle` | `homepageTitle(fm, config)` | `string`
|
|
322
|
-
| `homepageFrontmatter` | `homepageFrontmatter(fm, ...)` | `object`
|
|
323
|
-
| `
|
|
324
|
-
| `homepageAddresses` | `homepageAddresses(fm, body, ...)` | `Array<{field, url, kind}>` | finding every address a homepage carries, in frontmatter and body both |
|
|
311
|
+
| Export | Signature | Returns | Use it when |
|
|
312
|
+
| ---------------------------- | ------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------ |
|
|
313
|
+
| `HOMEPAGE_TYPE` | `const HOMEPAGE_TYPE` | — | naming the note type that compiles to the package homepage |
|
|
314
|
+
| `HOMEPAGE_FIELDS` | `const HOMEPAGE_FIELDS` | — | declaring what a homepage note may write under `sohl:` — nothing |
|
|
315
|
+
| `HOMEPAGE_SHORTCODE` | `const HOMEPAGE_SHORTCODE` | — | naming the shortcode a package front page conventionally takes |
|
|
316
|
+
| `HOMEPAGE_DESTINATION` | `const HOMEPAGE_DESTINATION` | — | naming the file a homepage is written to, `_index.md` at the site root |
|
|
317
|
+
| `isHomepage` | `isHomepage(fm)` | `boolean` | checking whether a note's frontmatter declares the homepage type |
|
|
318
|
+
| `HOMEPAGE_REFUSED_FIELDS` | `const HOMEPAGE_REFUSED_FIELDS` | — | naming the top-level fields a homepage refuses, and what each would decide |
|
|
319
|
+
| `checkHomepageAddressFields` | `checkHomepageAddressFields(fm, ...)` | one finding per issue | checking what the address rule says about one homepage note's top-level fields |
|
|
320
|
+
| `checkHomepageCount` | `checkHomepageCount(found, ...)` | one finding per offending note | requiring exactly one homepage note in a content tree |
|
|
321
|
+
| `homepageTitle` | `homepageTitle(fm, config)` | `string` | resolving the title a homepage publishes under, defaulting to the package's manifest title |
|
|
322
|
+
| `homepageFrontmatter` | `homepageFrontmatter(fm, ...)` | `object` | assembling the frontmatter a homepage publishes with, note plus derived values |
|
|
323
|
+
| `homepageAddresses` | `homepageAddresses(body)` | `Array<{field, url, kind}>` | finding every address a homepage carries — the markdown links in its body |
|
|
325
324
|
|
|
326
325
|
### `engine.noteSchemas`
|
|
327
326
|
|
|
@@ -345,7 +344,6 @@ The closed half of a note's frontmatter: the `data:` container and each type's `
|
|
|
345
344
|
| `applicableTagGroups` | `applicableTagGroups(type, groups)` | `object[]` | reading the declared tag groups that apply to a note type |
|
|
346
345
|
| `exclusiveTagGroups` | `exclusiveTagGroups(type, groups)` | `Array<{slot, tags}>` | reading the single-valued tag slots a note type has, such as a being's kind |
|
|
347
346
|
| `hasTag` | `hasTag(fm, tag)` | `boolean` | checking whether a note carries a given tag, whatever scalar-or-list form it was authored in |
|
|
348
|
-
| `hasAnyTag` | `hasAnyTag(fm)` | `boolean` | checking whether a note carries any `tags:` at all, for the site build's taxonomy decision |
|
|
349
347
|
| `isDraftNote` | `isDraftNote(fm)` | `boolean` | checking whether a note is tagged as an unfinished draft |
|
|
350
348
|
| `subTypeCharsetMessage` | `subTypeCharsetMessage(value)` | `string` | building the message for a `subType` outside the address charset |
|
|
351
349
|
| `typeCharsetMessage` | `typeCharsetMessage(type)` | `string` | building the message for a `type` outside the address charset |
|
|
@@ -608,23 +606,20 @@ The toolchain's own content index — the files it ships, addressed. Every other
|
|
|
608
606
|
|
|
609
607
|
Publishing a content tree as a website. Compiling a content tree into compendium packs is `content-build package compile`. Publishing the _same tree_ as a website was a script each consumer wrote for itself — 473 code lines in `sohl` and 462 in `sohl-thalorna`, 87 of them identical — and the copies drifted in ways neither repository could see. `sohl-thalorna` reimplemented four things this package already exported, not because it needed different behaviour but because its script predates the extraction. That is the failure a command removes: a consumer cannot accidentally reimplement one.
|
|
610
608
|
|
|
611
|
-
| Export
|
|
612
|
-
|
|
|
613
|
-
| `collectContentPages`
|
|
614
|
-
| `collectHomepages`
|
|
615
|
-
| `writeHomepages`
|
|
616
|
-
| `siteGates`
|
|
617
|
-
| `emptyGates`
|
|
618
|
-
| `gatesFailed`
|
|
619
|
-
| `tableUniverse`
|
|
620
|
-
| `
|
|
621
|
-
| `
|
|
622
|
-
| `
|
|
623
|
-
| `
|
|
624
|
-
| `
|
|
625
|
-
| `pluralTitle` | `function pluralTitle(name)` | {string} The display title. | A section landing's title, from its directory name — `macro` → `Macros`. |
|
|
626
|
-
| `resolveSitePass` | `function resolveSitePass(name, options)` | {{beforeLinks?: Function}} The bundle. | Resolves `site.pass` to its bundle. |
|
|
627
|
-
| `buildSite` | `buildSite({ config, sqlTables })` | {{gates: object, stats: object\|null, tableErrors: object[], wikiErrors: object[], manifests: object\|null}} | Builds a Hugo content tree from a content tree, and reports what it found. |
|
|
609
|
+
| Export | Signature | Returns | Use it when |
|
|
610
|
+
| --------------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
611
|
+
| `collectContentPages` | `function collectContentPages(contentBase, ctx)` | {{pages: object[], addressFindings: object[], fmLinkFindings: object[]}} | The content tree's pages, and what could not be addressed. |
|
|
612
|
+
| `collectHomepages` | `function collectHomepages(contentBase, ctx)` | {{pages: object[], addressFindings: object[]}} The homepage notes, in walk order, and the ones among them that could not be addressed. | The package's homepage notes — the authored page at `/<contentPackage>/`. |
|
|
613
|
+
| `writeHomepages` | `function writeHomepages(outRoot, pages, config)` | {number} How many pages were written. | Writes each homepage as the package root's `_index.md`. |
|
|
614
|
+
| `siteGates` | `function siteGates(pages, findings,` | {object} The gate results and, when they pass, the built index. | The integrity gates a site build runs before it writes anything. |
|
|
615
|
+
| `emptyGates` | `function emptyGates()` | {object} An all-clear gate result. | The gate result of a build that ran none of them. |
|
|
616
|
+
| `gatesFailed` | `function gatesFailed(gates)` | — | Whether any gate produced a finding. |
|
|
617
|
+
| `tableUniverse` | `function tableUniverse(pages)` | {Map<string, object[]>} Package → the notes it may tabulate. | The universe a generated table searches, grouped by package. |
|
|
618
|
+
| `pageFrontmatter` | `function pageFrontmatter(page,` | {object} The frontmatter to write. | The frontmatter a page publishes with. |
|
|
619
|
+
| `pageDestination` | `function pageDestination(page)` | — | Where a page is written, relative to the output root. |
|
|
620
|
+
| `renderPages` | `function renderPages(pages, options)` | {{written: number, byKind: Record<string, number>, tableErrors: object[], wikiErrors: object[]}} | Renders and writes every page. |
|
|
621
|
+
| `resolveSitePass` | `function resolveSitePass(name, options)` | {{beforeLinks?: Function}} The bundle. | Resolves `site.pass` to its bundle. |
|
|
622
|
+
| `buildSite` | `buildSite({ config, sqlTables })` | {{gates: object, stats: object\|null, tableErrors: object[], wikiErrors: object[], manifests: object\|null}} | Builds a Hugo content tree from a content tree, and reports what it found. |
|
|
628
623
|
|
|
629
624
|
### `engine.contentLint`
|
|
630
625
|
|