@heroiclands/package-build 22.3.1 → 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 +47 -0
- package/CONTENT.md +182 -306
- package/bin/content-build.mjs +15 -20
- package/bin/package-build.mjs +4 -3
- package/content-config.mjs +70 -179
- package/docs/api.md +27 -32
- package/docs/commands.md +31 -19
- package/docs/configuration.md +90 -126
- 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 +7 -1
- 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 +6 -8
- package/types/engine/content-links.d.mts +12 -23
- package/types/engine/diagnostics.d.mts +2 -2
- 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
|
@@ -1506,18 +1506,17 @@ function pdfCommand() {
|
|
|
1506
1506
|
* The sibling of `package compile`: the same tree, rendered as pages instead of
|
|
1507
1507
|
* compiled into packs. Everything a consumer would otherwise write for itself —
|
|
1508
1508
|
* the walk, the address derivation, the address index, table expansion,
|
|
1509
|
-
* wikilink resolution, code-fence protection
|
|
1510
|
-
*
|
|
1511
|
-
*
|
|
1512
|
-
*
|
|
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.
|
|
1513
1513
|
*
|
|
1514
1514
|
* **The Hugo configuration is generated before anything is written.** Its
|
|
1515
1515
|
* sources — `package.json`'s `homepage`, the cached navigation, the installed
|
|
1516
1516
|
* theme — are each a way the build can fail, and failing before the output
|
|
1517
|
-
* tree is cleared leaves the last good site in place to be looked at.
|
|
1518
|
-
*
|
|
1519
|
-
*
|
|
1520
|
-
* `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.
|
|
1521
1520
|
*
|
|
1522
1521
|
* **Each gate is reported and the run stops at the first that fires.** They are
|
|
1523
1522
|
* ordered so the report names the cause rather than its symptoms: an unusable
|
|
@@ -1535,14 +1534,12 @@ function siteCommand() {
|
|
|
1535
1534
|
handler: async () => {
|
|
1536
1535
|
try {
|
|
1537
1536
|
const config = loadPackConfig();
|
|
1538
|
-
// Generated
|
|
1539
|
-
//
|
|
1540
|
-
//
|
|
1541
|
-
//
|
|
1542
|
-
//
|
|
1543
|
-
|
|
1544
|
-
// so the value actually written is regenerated after it runs.
|
|
1545
|
-
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);
|
|
1546
1543
|
const result = buildSite({
|
|
1547
1544
|
config,
|
|
1548
1545
|
sqlTables: await prepareTreeSqlTables(config.paths.content, {
|
|
@@ -1653,11 +1650,9 @@ function siteCommand() {
|
|
|
1653
1650
|
const s = result.stats;
|
|
1654
1651
|
log.info(
|
|
1655
1652
|
`wrote ${s.homepages ?? 0} homepage(s) + ` +
|
|
1656
|
-
`${s.content ?? 0} content page(s)
|
|
1657
|
-
`${
|
|
1658
|
-
`landing(s) to ${path.relative(process.cwd(), s.out)}`,
|
|
1653
|
+
`${s.content ?? 0} content page(s) to ` +
|
|
1654
|
+
`${path.relative(process.cwd(), s.out)}`,
|
|
1659
1655
|
);
|
|
1660
|
-
const hugo = generateHugoConfig(config, { hasTags: result.hasTags });
|
|
1661
1656
|
const { file } = writeHugoConfig(config, hugo);
|
|
1662
1657
|
log.info(`wrote ${path.relative(process.cwd(), file)}`);
|
|
1663
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'
|
|
@@ -766,22 +765,16 @@ const SITE_KEYS = [
|
|
|
766
765
|
"assets",
|
|
767
766
|
"description",
|
|
768
767
|
"packages",
|
|
769
|
-
"sections",
|
|
770
|
-
"landing",
|
|
771
768
|
"pass",
|
|
772
769
|
"passOptions",
|
|
773
|
-
"backfillSections",
|
|
774
|
-
"list",
|
|
775
770
|
"notfound",
|
|
776
771
|
"hugo",
|
|
777
772
|
];
|
|
778
|
-
const SITE_LIST_KEYS = ["shortcodes"];
|
|
779
773
|
const SITE_NOTFOUND_KEYS = ["tagline", "sitenoun", "heroimage", "links"];
|
|
780
774
|
const SITE_NOTFOUND_LINK_KEYS = ["title", "url", "text"];
|
|
781
775
|
const PDF_KEYS = ["title", "subtitle", "document", "out", "front", "fonts", "iconFonts", "binary"];
|
|
782
776
|
const PDF_FONT_KEYS = ["serif", "sans", "mono", "path"];
|
|
783
777
|
const EMPTY_PDF_FONTS = Object.freeze({ serif: "", sans: "", mono: "", path: "" });
|
|
784
|
-
const SECTION_META_KEYS = ["title", "banner", "description", "listType", "listSubType"];
|
|
785
778
|
const DOC_PAGE_KEYS = ["title", "out", "preamble", "frontmatter"];
|
|
786
779
|
const RELATIONSHIP_KINDS = ["systems", "requires", "recommends", "conflicts"];
|
|
787
780
|
const RELATIONSHIP_KEYS = [
|
|
@@ -1465,102 +1458,6 @@ function normalizeDocs(value) {
|
|
|
1465
1458
|
});
|
|
1466
1459
|
}
|
|
1467
1460
|
|
|
1468
|
-
/**
|
|
1469
|
-
* One section's landing metadata — what a section says about itself on the
|
|
1470
|
-
* `_index.md` this build generates for it.
|
|
1471
|
-
*
|
|
1472
|
-
* A generated landing is the *only* place a section can speak, and it
|
|
1473
|
-
* is the only place a section **exists**: a content page is addressed
|
|
1474
|
-
* `(type, shortcode)` and written flat under the mount, so no page creates a
|
|
1475
|
-
* directory and nothing else makes `<prefix><section>/` answer. This is
|
|
1476
|
-
* therefore the whole vocabulary, and it is deliberately a **closed** one.
|
|
1477
|
-
*
|
|
1478
|
-
* The alternative — passing whatever a section declared straight through, as
|
|
1479
|
-
* `site.landing` does — was weighed and refused. `landing` is written once, for
|
|
1480
|
-
* the mount, and its keys are one landing template's own; a section entry is
|
|
1481
|
-
* written fourteen to twenty times per build against a contract every package
|
|
1482
|
-
* and every section shares. Unbounded there, a mistyped `descrption:` publishes
|
|
1483
|
-
* into front matter, is read by nobody, and says nothing to anyone — which is
|
|
1484
|
-
* the same failure, moved one step downstream where no build can
|
|
1485
|
-
* see it. So the keys are named here, and the writers emit what this produced
|
|
1486
|
-
* rather than transcribing a second list of their own.
|
|
1487
|
-
*
|
|
1488
|
-
* `banner` and `description` are optional — the hero images are external assets
|
|
1489
|
-
* and not every section has one, and a section may reasonably have nothing to
|
|
1490
|
-
* add to its title. Each is left off entirely rather than written as
|
|
1491
|
-
* `undefined`, which is not a value YAML can carry.
|
|
1492
|
-
*
|
|
1493
|
-
* **`listType` / `listSubType` say what the section lists.** A section's
|
|
1494
|
-
* directory holds nothing but the `_index.md` written here, so a layout
|
|
1495
|
-
* reading Hugo's `.Pages` finds
|
|
1496
|
-
* no members and renders an empty landing. The membership survives in this map
|
|
1497
|
-
* and nowhere a theme can reach it, so the landing states it and a layout
|
|
1498
|
-
* substitutes the equivalent `site.RegularPages` query — the same one `sohl`'s
|
|
1499
|
-
* catalog layouts already run, which is why `sohl`'s landings never broke.
|
|
1500
|
-
*
|
|
1501
|
-
* They are two keys of their own rather than `type` / `subType` because `type`
|
|
1502
|
-
* on an `_index.md` is **Hugo's own layout selector**: verified against Hugo
|
|
1503
|
-
* 0.165, a section landing carrying `type: doc` renders through
|
|
1504
|
-
* `layouts/doc/list.html` rather than the default list template, so spelling
|
|
1505
|
-
* the content type there would silently change which template serves the
|
|
1506
|
-
* landing. (This build already uses that behaviour deliberately, for the
|
|
1507
|
-
* mount's own landing.)
|
|
1508
|
-
*
|
|
1509
|
-
* Both are checked as **address segments**, which is the trap this came from:
|
|
1510
|
-
* a section is named for the URL a consumer chose and a subType is an address
|
|
1511
|
-
* segment, and the two need not agree — `/sohl/kb/user-guide/` is the section,
|
|
1512
|
-
* `userguide` the subType. Copying the section's name into the
|
|
1513
|
-
* declaration would select no page at all, and an empty landing reported by
|
|
1514
|
-
* nobody is the failure being fixed. A `listSubType` with no `listType` is
|
|
1515
|
-
* refused for the same reason: a subType is only distinguishing *within* a
|
|
1516
|
-
* type — `rules`, `userguide` and `reference` are all `doc` — so alone it names
|
|
1517
|
-
* no query.
|
|
1518
|
-
*
|
|
1519
|
-
* @param {unknown} value - The declared entry.
|
|
1520
|
-
* @param {string} where - Dotted path, for the error.
|
|
1521
|
-
* @returns {Readonly<{title: string, banner?: string, description?: string,
|
|
1522
|
-
* listType?: string, listSubType?: string}>}
|
|
1523
|
-
*/
|
|
1524
|
-
function normalizeSectionMeta(value, where) {
|
|
1525
|
-
if (!isPlainObject(value)) fail(where, "must be a mapping");
|
|
1526
|
-
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1527
|
-
rejectUnknownKeys(input, SECTION_META_KEYS, `${where}.`);
|
|
1528
|
-
const out = { title: requireNonEmptyString(input.title, `${where}.title`) };
|
|
1529
|
-
if (input.banner !== undefined) {
|
|
1530
|
-
out.banner = requireNonEmptyString(input.banner, `${where}.banner`);
|
|
1531
|
-
}
|
|
1532
|
-
if (input.description !== undefined) {
|
|
1533
|
-
out.description = requireNonEmptyString(input.description, `${where}.description`);
|
|
1534
|
-
}
|
|
1535
|
-
for (const key of ["listType", "listSubType"]) {
|
|
1536
|
-
if (input[key] === undefined) continue;
|
|
1537
|
-
const segment = requireNonEmptyString(input[key], `${where}.${key}`);
|
|
1538
|
-
if (!isAddressSegment(segment)) {
|
|
1539
|
-
fail(
|
|
1540
|
-
`${where}.${key}`,
|
|
1541
|
-
`is \`${segment}\`, which is not lowercase alphanumeric. It names a ` +
|
|
1542
|
-
"content type or subType, and those are address segments " +
|
|
1543
|
-
`(${ADDRESS_SEGMENT_PATTERN.source}) — not the section's ` +
|
|
1544
|
-
"own name, which is a URL this site chose and need not " +
|
|
1545
|
-
"match (`user-guide` is the section, `userguide` the " +
|
|
1546
|
-
"subType). A value no page carries selects nothing and " +
|
|
1547
|
-
"leaves the landing empty",
|
|
1548
|
-
);
|
|
1549
|
-
}
|
|
1550
|
-
out[key] = segment;
|
|
1551
|
-
}
|
|
1552
|
-
if (out.listSubType !== undefined && out.listType === undefined) {
|
|
1553
|
-
fail(
|
|
1554
|
-
`${where}.listSubType`,
|
|
1555
|
-
"is declared without a `listType`. A subType tells pages apart " +
|
|
1556
|
-
"only within a type — `rules`, `userguide` and `reference` " +
|
|
1557
|
-
"are all `doc` — so on its own it names no query for a layout " +
|
|
1558
|
-
"to run",
|
|
1559
|
-
);
|
|
1560
|
-
}
|
|
1561
|
-
return Object.freeze(out);
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
1461
|
/**
|
|
1565
1462
|
* The asset host the website resolves a pathname against.
|
|
1566
1463
|
*
|
|
@@ -1620,23 +1517,6 @@ function normalizeSiteDescription(value) {
|
|
|
1620
1517
|
return description;
|
|
1621
1518
|
}
|
|
1622
1519
|
|
|
1623
|
-
/**
|
|
1624
|
-
* A map of section name → landing metadata.
|
|
1625
|
-
*
|
|
1626
|
-
* @param {unknown} value - The declared mapping.
|
|
1627
|
-
* @param {string} where - Dotted path, for the error.
|
|
1628
|
-
* @returns {Readonly<Record<string, object>>}
|
|
1629
|
-
*/
|
|
1630
|
-
function normalizeSectionMap(value, where) {
|
|
1631
|
-
if (value === undefined) return Object.freeze({});
|
|
1632
|
-
if (!isPlainObject(value)) fail(where, "must be a mapping");
|
|
1633
|
-
const out = {};
|
|
1634
|
-
for (const [name, meta] of Object.entries(/** @type {Record<string, unknown>} */ (value))) {
|
|
1635
|
-
out[name] = normalizeSectionMeta(meta, `${where}.${name}`);
|
|
1636
|
-
}
|
|
1637
|
-
return Object.freeze(out);
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
1520
|
/**
|
|
1641
1521
|
* Hugo keys a repository may **not** declare under `site.hugo`, because the
|
|
1642
1522
|
* site build generates them and would only overwrite what was written.
|
|
@@ -1658,35 +1538,24 @@ export const DERIVED_HUGO_KEYS = Object.freeze({
|
|
|
1658
1538
|
contentDir: "the fixed content mount, `build/hugo/content`",
|
|
1659
1539
|
themesDir: "where `@heroiclands/hugo-theme` is installed",
|
|
1660
1540
|
theme: "the installed `@heroiclands/hugo-theme`",
|
|
1661
|
-
disableKinds:
|
|
1662
|
-
|
|
1663
|
-
|
|
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",
|
|
1664
1550
|
"params.description": "`site.description`",
|
|
1665
1551
|
"params.author": "package.json `author`",
|
|
1666
1552
|
"params.cdnBaseURL": "`site.assets`",
|
|
1667
1553
|
"params.brand": "the organisation's brand links, in `engine/site-config.mjs`",
|
|
1668
|
-
"params.list": "`site.list`",
|
|
1669
1554
|
"params.notfound": "`site.notfound`",
|
|
1670
1555
|
"markup.goldmark.renderer.unsafe": "the toolchain, whose pages carry raw HTML",
|
|
1671
1556
|
menu: "the navigation `content-build deps fetch` caches from heroiclands.org",
|
|
1672
1557
|
});
|
|
1673
1558
|
|
|
1674
|
-
/**
|
|
1675
|
-
* The `site.list` block — how a listing page renders.
|
|
1676
|
-
*
|
|
1677
|
-
* @param {unknown} value - The block, or `undefined`.
|
|
1678
|
-
* @returns {Readonly<{shortcodes: boolean}>} It, frozen, with every default filled.
|
|
1679
|
-
*/
|
|
1680
|
-
function normalizeSiteList(value) {
|
|
1681
|
-
if (value === undefined) return Object.freeze({ shortcodes: false });
|
|
1682
|
-
if (!isPlainObject(value)) fail("site.list", "must be a mapping");
|
|
1683
|
-
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1684
|
-
rejectUnknownKeys(input, SITE_LIST_KEYS, "site.list.");
|
|
1685
|
-
return Object.freeze({
|
|
1686
|
-
shortcodes: optionalBoolean(input.shortcodes, "site.list.shortcodes", false),
|
|
1687
|
-
});
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
1559
|
/**
|
|
1691
1560
|
* The `site.notfound` block — the wording of the "page not found" page.
|
|
1692
1561
|
*
|
|
@@ -1764,25 +1633,59 @@ function normalizeSiteHugo(value) {
|
|
|
1764
1633
|
return Object.freeze(structuredClone(input));
|
|
1765
1634
|
}
|
|
1766
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
|
+
|
|
1767
1668
|
/**
|
|
1768
1669
|
* The `site` section — how this repository frames the website it publishes.
|
|
1769
1670
|
*
|
|
1770
|
-
* Everything here is *framing*:
|
|
1771
|
-
*
|
|
1772
|
-
*
|
|
1773
|
-
*
|
|
1774
|
-
*
|
|
1775
|
-
*
|
|
1776
|
-
*
|
|
1777
|
-
*
|
|
1778
|
-
*
|
|
1779
|
-
* **What the site publishes is the content tree, and nothing
|
|
1780
|
-
* page of documentation is a note — `type: doc`, addressed
|
|
1781
|
-
* compiling into no document where it says `pack: none` —
|
|
1782
|
-
* second mechanism for mounting a directory of markdown, and a
|
|
1783
|
-
* that names one (`site.trees`, and the `site.readmeSections`
|
|
1784
|
-
* such a tree's landing) is refused with a message saying where
|
|
1785
|
-
* 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.
|
|
1786
1689
|
*
|
|
1787
1690
|
* @param {unknown} value - The `site` block, or `undefined`.
|
|
1788
1691
|
* @returns {Readonly<object>} It, frozen, with every default filled.
|
|
@@ -1793,12 +1696,8 @@ function normalizeSite(value) {
|
|
|
1793
1696
|
assets: "",
|
|
1794
1697
|
description: "",
|
|
1795
1698
|
packages: Object.freeze([]),
|
|
1796
|
-
sections: Object.freeze({}),
|
|
1797
|
-
landing: null,
|
|
1798
1699
|
pass: "",
|
|
1799
1700
|
passOptions: Object.freeze({}),
|
|
1800
|
-
backfillSections: false,
|
|
1801
|
-
list: Object.freeze({ shortcodes: false }),
|
|
1802
1701
|
notfound: null,
|
|
1803
1702
|
hugo: Object.freeze({}),
|
|
1804
1703
|
});
|
|
@@ -1824,10 +1723,16 @@ function normalizeSite(value) {
|
|
|
1824
1723
|
`site.${key}`,
|
|
1825
1724
|
"is retired — a page is a note in the content tree. Give each page " +
|
|
1826
1725
|
"`type: doc`, a `shortcode` and `pack: none`, file it under " +
|
|
1827
|
-
"`assets/content/`, and
|
|
1828
|
-
"`
|
|
1726
|
+
"`assets/content/`, and link it from the homepage or from a " +
|
|
1727
|
+
"`doc` note that indexes it",
|
|
1829
1728
|
);
|
|
1830
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
|
+
}
|
|
1831
1736
|
rejectUnknownKeys(input, SITE_KEYS, "site.");
|
|
1832
1737
|
|
|
1833
1738
|
let packages = [];
|
|
@@ -1838,30 +1743,16 @@ function normalizeSite(value) {
|
|
|
1838
1743
|
packages = input.packages.map((p, i) => requireNonEmptyString(p, `site.packages[${i}]`));
|
|
1839
1744
|
}
|
|
1840
1745
|
|
|
1841
|
-
let landing = null;
|
|
1842
|
-
if (input.landing !== undefined) {
|
|
1843
|
-
if (!isPlainObject(input.landing)) {
|
|
1844
|
-
fail("site.landing", "must be a mapping");
|
|
1845
|
-
}
|
|
1846
|
-
// Passed through rather than validated field by field: it is Hugo
|
|
1847
|
-
// frontmatter, whose vocabulary is the theme's and not this package's.
|
|
1848
|
-
landing = Object.freeze({ ...input.landing });
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
1746
|
return Object.freeze({
|
|
1852
1747
|
base: input.base === undefined ? "" : requireNonEmptyString(input.base, "site.base"),
|
|
1853
1748
|
assets: normalizeSiteAssets(input.assets),
|
|
1854
1749
|
description: normalizeSiteDescription(input.description),
|
|
1855
1750
|
packages: Object.freeze(packages),
|
|
1856
|
-
sections: normalizeSectionMap(input.sections, "site.sections"),
|
|
1857
|
-
landing,
|
|
1858
1751
|
pass: input.pass === undefined ? "" : requireNonEmptyString(input.pass, "site.pass"),
|
|
1859
1752
|
passOptions:
|
|
1860
1753
|
input.passOptions === undefined ?
|
|
1861
1754
|
Object.freeze({})
|
|
1862
1755
|
: Object.freeze({ ...input.passOptions }),
|
|
1863
|
-
backfillSections: optionalBoolean(input.backfillSections, "site.backfillSections", false),
|
|
1864
|
-
list: normalizeSiteList(input.list),
|
|
1865
1756
|
notfound: normalizeSiteNotfound(input.notfound),
|
|
1866
1757
|
hugo: normalizeSiteHugo(input.hugo),
|
|
1867
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
|
|
package/docs/commands.md
CHANGED
|
@@ -261,7 +261,7 @@ package-build: `packageKind: documentation` ships no Foundry package, so there i
|
|
|
261
261
|
|
|
262
262
|
**NAME**
|
|
263
263
|
|
|
264
|
-
`package-build site-root` — write the deployment's `_headers
|
|
264
|
+
`package-build site-root` — write the deployment's `_headers`.
|
|
265
265
|
|
|
266
266
|
**SYNOPSIS**
|
|
267
267
|
|
|
@@ -278,13 +278,18 @@ and Cloudflare Pages reads `_headers` and `_redirects` from there and nowhere
|
|
|
278
278
|
else — a copy inside the prefix is published as a text file and never applied.
|
|
279
279
|
Hugo owns everything under the prefix; this owns what sits beside it.
|
|
280
280
|
|
|
281
|
-
|
|
282
|
-
answers on but nobody advertises —
|
|
283
|
-
`pages.dev`,
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
281
|
+
One file is written, `_headers`, and it says one thing: indexing is
|
|
282
|
+
suppressed on every address a deployment answers on but nobody advertises —
|
|
283
|
+
the project's `pages.dev`, the per-deployment `pages.dev`, and the custom
|
|
284
|
+
domain the routing layer fetches — each of which would otherwise compete with
|
|
285
|
+
the canonical URL in search results. No `Cache-Control` is pinned on the
|
|
286
|
+
prefix root: it is the homepage, and a lifetime on it would hold a stale copy
|
|
287
|
+
at the most-linked address after a deploy.
|
|
288
|
+
|
|
289
|
+
No `_redirects` is written: the prefix root _is_ the homepage, which the site
|
|
290
|
+
build writes as the mount's `_index.md`, so nothing redirects. A `_redirects`
|
|
291
|
+
left beside the site by an earlier build is removed, since Pages would apply
|
|
292
|
+
it.
|
|
288
293
|
|
|
289
294
|
The rules are scoped to those hostnames, so a site deployed under a domain of
|
|
290
295
|
its own stays indexable.
|
|
@@ -304,7 +309,6 @@ under the prefix. Otherwise 0.
|
|
|
304
309
|
```
|
|
305
310
|
$ package-build site-root
|
|
306
311
|
✅ Wrote build/site/_headers.
|
|
307
|
-
✅ Wrote build/site/_redirects.
|
|
308
312
|
```
|
|
309
313
|
|
|
310
314
|
**SEE ALSO**
|
|
@@ -1564,23 +1568,31 @@ Writes the whole Hugo source tree under `build/hugo/` — the sibling of
|
|
|
1564
1568
|
`package compile`: the same tree, rendered as pages instead of compiled
|
|
1565
1569
|
into packs. Everything a consumer would otherwise write for itself happens
|
|
1566
1570
|
here: the walk, address derivation, the address index, table expansion,
|
|
1567
|
-
wikilink resolution, code-fence protection, the foreign-manifest merge,
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
+
wikilink resolution, code-fence protection, the foreign-manifest merge, and
|
|
1572
|
+
the Hugo configuration itself. The consumer's script then runs Hugo over the
|
|
1573
|
+
tree — `hugo --source build/hugo` — and this command never does.
|
|
1574
|
+
|
|
1575
|
+
**A site is its homepage and its pages.** The `type: homepage` note is
|
|
1576
|
+
written as the mount's `_index.md`, so Hugo renders it at
|
|
1577
|
+
`/<contentPackage>/`; every other note is one page at
|
|
1578
|
+
`/<contentPackage>/<type>-<shortcode>/`. Nothing is generated between them —
|
|
1579
|
+
no section directory, no listing, no tag page — and the generated
|
|
1580
|
+
configuration disables the `section`, `taxonomy`, `term` and `RSS` kinds on
|
|
1581
|
+
every site. An index of what the package publishes is a `doc` note carrying a
|
|
1582
|
+
content table, authored where every other page is.
|
|
1571
1583
|
|
|
1572
1584
|
Three things are written, and nothing outside `build/`:
|
|
1573
1585
|
|
|
1574
1586
|
- `build/hugo/hugo.toml`, generated on every run from `package.json`
|
|
1575
1587
|
(`homepage`, `description`, `author`), `package-build.config.yaml`
|
|
1576
|
-
(`packageBuild.manifest.title`, `site.assets`, `site.
|
|
1577
|
-
`site.
|
|
1588
|
+
(`packageBuild.manifest.title`, `site.assets`, `site.notfound`,
|
|
1589
|
+
`site.hugo`), the organisation's constants, the installed
|
|
1578
1590
|
`@heroiclands/hugo-theme`'s location, and the navigation `deps fetch`
|
|
1579
1591
|
cached. Every value's source is listed under
|
|
1580
1592
|
[the generated Hugo configuration](configuration.md#the-generated-hugo-configuration).
|
|
1581
|
-
- `build/hugo/content/`, the content mount — the homepage
|
|
1582
|
-
the content tree's pages below `publish.address.prefix`. Wiped on
|
|
1583
|
-
run.
|
|
1593
|
+
- `build/hugo/content/`, the content mount — the homepage as its `_index.md`,
|
|
1594
|
+
and the content tree's pages flat below `publish.address.prefix`. Wiped on
|
|
1595
|
+
every run.
|
|
1584
1596
|
- `publishDir` pointing Hugo at `build/site/<contentPackage>/`, the
|
|
1585
1597
|
deployment root `package-build site-root` writes beside. Nothing Hugo
|
|
1586
1598
|
reads lands in what is published.
|
|
@@ -1612,7 +1624,7 @@ failed to expand, or a dead wikilink. Otherwise 0.
|
|
|
1612
1624
|
|
|
1613
1625
|
```
|
|
1614
1626
|
$ content-build site
|
|
1615
|
-
[…] wrote 1 homepage(s) + 1 content page(s)
|
|
1627
|
+
[…] wrote 1 homepage(s) + 1 content page(s) to build/hugo/content
|
|
1616
1628
|
[…] wrote build/hugo/hugo.toml
|
|
1617
1629
|
|
|
1618
1630
|
$ content-build site
|