@heroiclands/package-build 22.3.1 → 22.4.1
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 +65 -0
- package/CONTENT.md +189 -306
- package/bin/content-build.mjs +15 -20
- package/bin/package-build.mjs +87 -3
- package/content-config.mjs +70 -179
- package/docs/api.md +27 -32
- package/docs/commands.md +98 -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/actor-compiler.mjs +53 -14
- package/engine/changelog-lint.mjs +629 -0
- 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/githooks/pre-commit +53 -2
- package/package.json +1 -1
- package/types/content-config.d.mts +6 -8
- package/types/engine/actor-compiler.d.mts +42 -9
- package/types/engine/changelog-lint.d.mts +50 -0
- 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/engine/site-build.mjs
CHANGED
|
@@ -25,12 +25,18 @@
|
|
|
25
25
|
*
|
|
26
26
|
* **What is here is the pass, not the framing.** The walk, the frontmatter read,
|
|
27
27
|
* the address derivation, the address index, table expansion, wikilink
|
|
28
|
-
* resolution, code-fence protection, the foreign-manifest merge
|
|
29
|
-
* emission
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
28
|
+
* resolution, code-fence protection, the foreign-manifest merge and the page
|
|
29
|
+
* emission are the same job everywhere. Where a page's address comes from
|
|
30
|
+
* (`publish.address`) and what a repository's own rewrites are, are not — they
|
|
31
|
+
* arrive as configuration and as a **named** pass bundle, since a configuration
|
|
32
|
+
* is data and cannot hold a function.
|
|
33
|
+
*
|
|
34
|
+
* **A site is its homepage and its pages.** The homepage is the mount's
|
|
35
|
+
* `_index.md`, rendered at `/<package>/`; every other note is one page at
|
|
36
|
+
* `/<package>/<type>-<shortcode>/`. Nothing is generated between them — no
|
|
37
|
+
* section directory, no listing, no `_index.md` but the root's. An index of
|
|
38
|
+
* what the package publishes is a `doc` note carrying a content table, and it
|
|
39
|
+
* is authored where every other page is.
|
|
34
40
|
*
|
|
35
41
|
* **Every gate reports; none exits.** The integrity checks a site build needs —
|
|
36
42
|
* a wikilink authored in frontmatter, a name that yields no slug, an unusable
|
|
@@ -62,14 +68,13 @@ import { deriveBeingInfo, isBeing } from "../sohl/being-info.mjs";
|
|
|
62
68
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
63
69
|
import { routerFor } from "./pack-router.mjs";
|
|
64
70
|
import { searchableFrontmatter } from "./note-package.mjs";
|
|
65
|
-
import { hasAnyTag } from "./note-vocabulary.mjs";
|
|
66
71
|
// The corpus, from the one pass that derives it.
|
|
67
72
|
import { indexRecordsFor } from "./content-index.mjs";
|
|
68
73
|
import { isNoteRecord, noteFile } from "./index-records.mjs";
|
|
69
74
|
import { ART_SLOTS, artPathname, assetAddressIndex } from "./art-fields.mjs";
|
|
70
75
|
import {
|
|
76
|
+
HOMEPAGE_DESTINATION,
|
|
71
77
|
checkHomepageCount,
|
|
72
|
-
homepageDestination,
|
|
73
78
|
homepageFrontmatter,
|
|
74
79
|
homepageTitle,
|
|
75
80
|
isHomepage,
|
|
@@ -87,20 +92,12 @@ const require = createRequire(import.meta.url);
|
|
|
87
92
|
* which files are the content. The note is still read for its `{fm, body}`: the
|
|
88
93
|
* index carries no note text, and a page *is* its text.
|
|
89
94
|
*
|
|
90
|
-
* **
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* content-path order
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* emitted to its own file at an address derived from its frontmatter, and the
|
|
97
|
-
* one place order could still show — a section's page list — is sorted by the
|
|
98
|
-
* theme. Verified rather than argued: over `sohl`'s tree the emitted mount is
|
|
99
|
-
* byte-identical, all 1,749 files.
|
|
100
|
-
*
|
|
101
|
-
* A **content-path** order is also the better of the two. Directory-read order
|
|
102
|
-
* is a fact about the filesystem, not about the content, so it can differ
|
|
103
|
-
* between two checkouts of one tree; this order cannot.
|
|
95
|
+
* **Records are in content-path order**, and nothing downstream depends on
|
|
96
|
+
* it: each page is emitted to its own file at an address derived from its
|
|
97
|
+
* frontmatter, and a content table orders what it lists by its own clause.
|
|
98
|
+
* A content-path order is a fact about the content, where directory-read
|
|
99
|
+
* order is a fact about the filesystem and can differ between two checkouts
|
|
100
|
+
* of one tree.
|
|
104
101
|
*
|
|
105
102
|
* @param {string} contentBase - Root of the content tree.
|
|
106
103
|
* @param {object} ctx - The build context. `ctx.records` is the corpus when the
|
|
@@ -216,8 +213,8 @@ export function collectContentPages(contentBase, ctx) {
|
|
|
216
213
|
// Location below the content root, POSIX-separated — what a
|
|
217
214
|
// generated table reads as `file.path` and scopes on with `FROM`.
|
|
218
215
|
relPath: rel.split(path.sep).join("/"),
|
|
219
|
-
// The immediate source subfolder
|
|
220
|
-
//
|
|
216
|
+
// The immediate source subfolder — what a content table reads as
|
|
217
|
+
// `file.folder`, and the page's `kbfolder`.
|
|
221
218
|
folder: path.basename(path.dirname(file)),
|
|
222
219
|
// Every page is addressed by `(type, shortcode)` at the package
|
|
223
220
|
// root, which takes no content mount. The file is written
|
|
@@ -275,25 +272,24 @@ export function collectHomepages(contentBase, ctx) {
|
|
|
275
272
|
}
|
|
276
273
|
|
|
277
274
|
/**
|
|
278
|
-
* Writes each homepage
|
|
275
|
+
* Writes each homepage as the package root's `_index.md`.
|
|
279
276
|
*
|
|
280
277
|
* Its own writer, deliberately small. A homepage is authored markdown published
|
|
281
|
-
* verbatim — no table expansion
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
* **Verbatim is the answer, not a gap.** A
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
* {@link auditHomepageLinks} reads the
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
* **Its destination is
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
* package's own repository authors, which is a routing fact rather than a page.
|
|
278
|
+
* verbatim — no table expansion and no link resolution — so routing it through
|
|
279
|
+
* {@link renderPages} would buy it a pipeline it has no input for, and would
|
|
280
|
+
* make homepage-only mode depend on the index, the foreign manifests and the
|
|
281
|
+
* table universe that mode exists to not build.
|
|
282
|
+
*
|
|
283
|
+
* **Verbatim is the answer, not a gap.** A homepage's links could not be
|
|
284
|
+
* *resolved* here without giving `homepage` mode the index its licensing fence
|
|
285
|
+
* exists to not build, so they are **checked** instead:
|
|
286
|
+
* {@link auditHomepageLinks} reads the body's markdown links, and reports a
|
|
287
|
+
* wikilink on the page rather than resolving one.
|
|
288
|
+
*
|
|
289
|
+
* **Its destination is fixed**: {@link HOMEPAGE_DESTINATION}, at the package's
|
|
290
|
+
* site root, which Hugo renders as the `home` kind at `/<package>/`. The page
|
|
291
|
+
* states no `url` — the home kind has none to state — and the mount below it
|
|
292
|
+
* holds pages and nothing else, so this is the only `_index.md` in the tree.
|
|
297
293
|
*
|
|
298
294
|
* @param {string} outRoot - The package's site root — the content mount's
|
|
299
295
|
* root, `build/hugo/content`, one level above the mount itself.
|
|
@@ -308,7 +304,7 @@ export function writeHomepages(outRoot, pages, config) {
|
|
|
308
304
|
contentPackage: config.contentPackage,
|
|
309
305
|
title: homepageTitle(page.fm, config),
|
|
310
306
|
});
|
|
311
|
-
const dest = path.join(outRoot,
|
|
307
|
+
const dest = path.join(outRoot, HOMEPAGE_DESTINATION);
|
|
312
308
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
313
309
|
fs.writeFileSync(dest, matter.stringify(page.body, data));
|
|
314
310
|
}
|
|
@@ -325,7 +321,7 @@ export function writeHomepages(outRoot, pages, config) {
|
|
|
325
321
|
* - **Frontmatter wikilinks** first, because frontmatter is copied to the page
|
|
326
322
|
* verbatim and a link written in one reaches the reader as literal `[[…]]`.
|
|
327
323
|
* - **Addresses** next: a note that has no address — no shortcode to be
|
|
328
|
-
* addressed by
|
|
324
|
+
* addressed by — would silently drop a page.
|
|
329
325
|
* There is no collision gate beside it: an address is `(type, shortcode)`,
|
|
330
326
|
* which is unique within a package by rule, so two pages cannot claim one URL.
|
|
331
327
|
* - **Foreign manifests** last, in two steps. *Unusable* is a file this build
|
|
@@ -441,39 +437,6 @@ export function tableUniverse(pages) {
|
|
|
441
437
|
return byPackage;
|
|
442
438
|
}
|
|
443
439
|
|
|
444
|
-
/**
|
|
445
|
-
* The front matter a section's landing states about itself.
|
|
446
|
-
*
|
|
447
|
-
* The section metadata a configuration resolved, ready to be written or merged
|
|
448
|
-
* onto a page. Two things happen here and nothing else does:
|
|
449
|
-
*
|
|
450
|
-
* - **`title` leads.** It is the one key every landing has carried since the
|
|
451
|
-
* first one, and a landing whose block opened with `banner:` would be a
|
|
452
|
-
* gratuitous diff on every consumer's tree.
|
|
453
|
-
* - **An absent value is left off**, not written as `undefined` — which is not
|
|
454
|
-
* a value YAML can carry, and would abort the serializer.
|
|
455
|
-
*
|
|
456
|
-
* Everything else the section declared is passed through. That is the point of
|
|
457
|
-
* the function: two writers transcribing `title` and `banner` by
|
|
458
|
-
* name, so the vocabulary lived in three places — the schema that admits a key
|
|
459
|
-
* and the two writers that copy it — and a key added to the schema alone
|
|
460
|
-
* validated cleanly and then reached no page. The *schema* is the bound worth
|
|
461
|
-
* keeping (see `normalizeSectionMeta`, which refuses a key it does not know and
|
|
462
|
-
* names it); a second, silent bound in the writers is not.
|
|
463
|
-
*
|
|
464
|
-
* @param {object} meta - A resolved `site.sections` / `site.readmeSections`
|
|
465
|
-
* entry.
|
|
466
|
-
* @returns {object} Its front matter, `title` first.
|
|
467
|
-
*/
|
|
468
|
-
export function sectionFrontmatter(meta) {
|
|
469
|
-
const data = { title: meta.title };
|
|
470
|
-
for (const [key, value] of Object.entries(meta)) {
|
|
471
|
-
if (key === "title" || value === undefined) continue;
|
|
472
|
-
data[key] = value;
|
|
473
|
-
}
|
|
474
|
-
return data;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
440
|
/**
|
|
478
441
|
* The frontmatter a page publishes with.
|
|
479
442
|
*
|
|
@@ -504,10 +467,8 @@ export function sectionFrontmatter(meta) {
|
|
|
504
467
|
* A content page carries the package the build **derived**. No note
|
|
505
468
|
* declares one — `package:` is retired — so the note's frontmatter alone
|
|
506
469
|
* would publish a page that does not say which package it belongs to. The
|
|
507
|
-
* emitted page is what a
|
|
508
|
-
*
|
|
509
|
-
* `.Params.package`, so without it that crumb degrades from a linked, labelled
|
|
510
|
-
* section to a bare type slug. Writing the derived value keeps a page
|
|
470
|
+
* emitted page is what a theme reads: `breadcrumbs.html` reads
|
|
471
|
+
* `.Params.package`. Writing the derived value keeps a page
|
|
511
472
|
* self-describing and makes sweeping the field out of a content tree
|
|
512
473
|
* output-preserving for a site as it already is for the packs.
|
|
513
474
|
*
|
|
@@ -592,16 +553,15 @@ function isPlainObject(value) {
|
|
|
592
553
|
* Where a page is written, relative to the output root.
|
|
593
554
|
*
|
|
594
555
|
* **Flat, under the mount, named by its address**. A content page's URL
|
|
595
|
-
* is its address — `/<package>/<type>-<shortcode>/` — and the file is
|
|
596
|
-
* the same way, so the two agree.
|
|
597
|
-
* Hugo
|
|
598
|
-
* a directory chosen only to satisfy a rendering engine's idea
|
|
599
|
-
* section is has no business in the note format.
|
|
556
|
+
* is its address — `/<package>/<type>-<shortcode>/` — and the file is named
|
|
557
|
+
* the same way, so the two agree. No page creates a directory: a section is
|
|
558
|
+
* a Hugo listing over a content directory, the `section` kind is disabled on
|
|
559
|
+
* every site, and a directory chosen only to satisfy a rendering engine's idea
|
|
560
|
+
* of what a section is has no business in the note format.
|
|
600
561
|
*
|
|
601
|
-
* The name is the *whole* address
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
* are distinct whatever the sections.
|
|
562
|
+
* The name is the *whole* address, so two types cannot fight over one file: a
|
|
563
|
+
* `doc` note's `subType` may be spelled the same as another note's `type`, and
|
|
564
|
+
* `doc-gear.md` and `weapongear-gear.md` are distinct.
|
|
605
565
|
*
|
|
606
566
|
* @param {object} page - The page.
|
|
607
567
|
* @returns {string} The file, relative to the mount.
|
|
@@ -699,8 +659,7 @@ export function renderPages(pages, options) {
|
|
|
699
659
|
|
|
700
660
|
for (const page of pages) {
|
|
701
661
|
// The page's path in the tree an author edits, below the content
|
|
702
|
-
// root.
|
|
703
|
-
// directory that was never the note's.
|
|
662
|
+
// root.
|
|
704
663
|
const src = page.relPath ?? page.base;
|
|
705
664
|
const ctx = wikiContext(index, {
|
|
706
665
|
src,
|
|
@@ -749,111 +708,6 @@ export function renderPages(pages, options) {
|
|
|
749
708
|
return { written: pages.length, byKind, tableErrors, wikiErrors, imageErrors };
|
|
750
709
|
}
|
|
751
710
|
|
|
752
|
-
/**
|
|
753
|
-
* Writes the Hugo sections a published tree declares.
|
|
754
|
-
*
|
|
755
|
-
* **This is where a section lives now, and the only place**. A content
|
|
756
|
-
* note carries none: it is addressed by `(type, shortcode)` and emitted flat
|
|
757
|
-
* under the mount, so nothing a page does creates a directory. A site that wants
|
|
758
|
-
* `/<package>/<prefix><section>/` to answer — with a title, a hero, and whatever
|
|
759
|
-
* listing its layout builds — says so here, in configuration, and this writes
|
|
760
|
-
* the `_index.md` that makes Hugo agree it is a section.
|
|
761
|
-
*
|
|
762
|
-
* Three jobs, all of them Hugo's directory semantics rather than the note
|
|
763
|
-
* format's:
|
|
764
|
-
*
|
|
765
|
-
* - **The mount's own landing**, so `/<package>/<prefix>` is a page rather than
|
|
766
|
-
* a directory listing. It carries a `type` of its own: Hugo's template lookup
|
|
767
|
-
* walks up a page's path, so an untyped landing template at the mount would
|
|
768
|
-
* also serve every section below it that has none.
|
|
769
|
-
* - **Declared sections** get a titled `_index.md` with their hero, so a landing
|
|
770
|
-
* matches the card that links to it instead of showing Hugo's auto-humanised
|
|
771
|
-
* directory name. The body is empty, which lets the theme decide what to list.
|
|
772
|
-
* - **Every other directory directly under the mount** gets a bare `_index.md`,
|
|
773
|
-
* or its own address publishes nothing. Hugo generates a section page
|
|
774
|
-
* automatically only for a *top-level* content directory; below that, a
|
|
775
|
-
* directory without an `_index.md` is not a section, so its URL 404s while its
|
|
776
|
-
* children publish normally. With content pages flat, no note creates a
|
|
777
|
-
* directory below the mount, so this reaches only what something else
|
|
778
|
-
* placed there.
|
|
779
|
-
*
|
|
780
|
-
* **A section listing is not a page listing any more.** A layout that reads
|
|
781
|
-
* `.Pages` off a section it declares here will find nothing, because no file is
|
|
782
|
-
* filed into it; one that queries `site.RegularPages` by `Params.type` — which
|
|
783
|
-
* is how `sohl`'s eleven catalog layouts already work — is unaffected. That is a
|
|
784
|
-
* consumer's layout to choose, and it is stated here because the choice is no
|
|
785
|
-
* longer free.
|
|
786
|
-
*
|
|
787
|
-
* Scoped to one level on purpose. A directory further down was not a section
|
|
788
|
-
* before either, and giving it one here would silently re-scope the prev/next
|
|
789
|
-
* navigation of every page inside it.
|
|
790
|
-
*
|
|
791
|
-
* @param {string} outRoot - The mount directory.
|
|
792
|
-
* @param {object} options
|
|
793
|
-
* @param {Record<string, object>} [options.sections] - The declared sections,
|
|
794
|
-
* each written as a titled `_index.md` carrying its own frontmatter.
|
|
795
|
-
* @param {object} [options.landing] - The mount's own landing frontmatter.
|
|
796
|
-
* Omitted, the mount gets no `_index.md` of its own.
|
|
797
|
-
* @param {((name: string) => string)|null} [options.sectionTitle] - Titles a
|
|
798
|
-
* directory below the mount that declared no section. `null` leaves such a
|
|
799
|
-
* directory without an `_index.md`.
|
|
800
|
-
* @returns {number} How many landings were written.
|
|
801
|
-
*/
|
|
802
|
-
export function writeSectionLandings(outRoot, { sections = {}, landing, sectionTitle }) {
|
|
803
|
-
let written = 0;
|
|
804
|
-
|
|
805
|
-
if (landing) {
|
|
806
|
-
fs.mkdirSync(outRoot, { recursive: true });
|
|
807
|
-
fs.writeFileSync(path.join(outRoot, "_index.md"), matter.stringify("", landing));
|
|
808
|
-
written += 1;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
for (const [sec, meta] of Object.entries(sections)) {
|
|
812
|
-
const dir = path.join(outRoot, sec);
|
|
813
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
814
|
-
// Whatever the section declared, not a list of keys named here — see
|
|
815
|
-
// {@link sectionFrontmatter} for why the two lists were one too many.
|
|
816
|
-
fs.writeFileSync(
|
|
817
|
-
path.join(dir, "_index.md"),
|
|
818
|
-
matter.stringify("", sectionFrontmatter(meta)),
|
|
819
|
-
);
|
|
820
|
-
written += 1;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
if (!sectionTitle) return written;
|
|
824
|
-
for (const entry of fs.readdirSync(outRoot, { withFileTypes: true })) {
|
|
825
|
-
if (!entry.isDirectory()) continue;
|
|
826
|
-
const index = path.join(outRoot, entry.name, "_index.md");
|
|
827
|
-
if (fs.existsSync(index)) continue;
|
|
828
|
-
fs.writeFileSync(index, matter.stringify("", { title: sectionTitle(entry.name) }));
|
|
829
|
-
written += 1;
|
|
830
|
-
}
|
|
831
|
-
return written;
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
/**
|
|
835
|
-
* A section landing's title, from its directory name — `macro` → `Macros`.
|
|
836
|
-
*
|
|
837
|
-
* Hugo derives exactly this for a section page it generates itself, but not for
|
|
838
|
-
* one backed by an `_index.md`: an explicit file with no `title` renders a blank
|
|
839
|
-
* heading. So a backfilled landing states its own, in plain English
|
|
840
|
-
* pluralisation rather than Hugo's inflector, which spells that section
|
|
841
|
-
* "Macroes".
|
|
842
|
-
*
|
|
843
|
-
* @param {string} name - The directory name.
|
|
844
|
-
* @returns {string} The display title.
|
|
845
|
-
*/
|
|
846
|
-
export function pluralTitle(name) {
|
|
847
|
-
const plural =
|
|
848
|
-
/(?:s|x|z|ch|sh)$/.test(name) ? `${name}es`
|
|
849
|
-
: /[^aeiou]y$/.test(name) ? `${name.slice(0, -1)}ies`
|
|
850
|
-
: `${name}s`;
|
|
851
|
-
return plural
|
|
852
|
-
.split("-")
|
|
853
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
854
|
-
.join(" ");
|
|
855
|
-
}
|
|
856
|
-
|
|
857
711
|
/**
|
|
858
712
|
* The named pass bundles a configuration may select.
|
|
859
713
|
*
|
|
@@ -905,10 +759,7 @@ export function resolveSitePass(name, options) {
|
|
|
905
759
|
* `sql` directive with none prepared is a table error: nothing here runs a
|
|
906
760
|
* query.
|
|
907
761
|
* @returns {{gates: object, stats: object|null, tableErrors: object[],
|
|
908
|
-
* wikiErrors: object[], imageErrors: object[], manifests: object|null
|
|
909
|
-
* hasTags: boolean}} `hasTags` is whether any note the walk read carries
|
|
910
|
-
* `tags:` — what {@link module:engine/site-config.hugoConfig} reads to
|
|
911
|
-
* decide whether the site emits taxonomy pages.
|
|
762
|
+
* wikiErrors: object[], imageErrors: object[], manifests: object|null}}
|
|
912
763
|
*/
|
|
913
764
|
export function buildSite({ config, sqlTables } = {}) {
|
|
914
765
|
const resolved = config ?? loadPackConfig();
|
|
@@ -927,8 +778,8 @@ export function buildSite({ config, sqlTables } = {}) {
|
|
|
927
778
|
const base = site.base || `/${resolved.contentPackage}/`;
|
|
928
779
|
const mount = `${base}${scheme.prefix}`;
|
|
929
780
|
|
|
930
|
-
// The Hugo content tree mirrors that mount:
|
|
931
|
-
// `<out
|
|
781
|
+
// The Hugo content tree mirrors that mount: the homepage is written at
|
|
782
|
+
// `<out>/_index.md` and every page under `<out>/<prefix>/`.
|
|
932
783
|
// The root is fixed — `build/hugo/content`, beside the generated
|
|
933
784
|
// `hugo.toml` — and resolved against the repository root for the same
|
|
934
785
|
// reason every configured path is, so the build reads and writes the same
|
|
@@ -942,9 +793,9 @@ export function buildSite({ config, sqlTables } = {}) {
|
|
|
942
793
|
// Homepage-only has no content mount, so the package's root *is*
|
|
943
794
|
// the output root.
|
|
944
795
|
: outBase;
|
|
945
|
-
// The homepage publishes at `/<contentPackage
|
|
946
|
-
//
|
|
947
|
-
//
|
|
796
|
+
// The homepage publishes at `/<contentPackage>/`, so its file goes at the
|
|
797
|
+
// package's own root — one level above the content mount, and the same
|
|
798
|
+
// directory in homepage-only mode.
|
|
948
799
|
const homeRoot = publishesContent ? outBase : out;
|
|
949
800
|
|
|
950
801
|
const packages = new Set(site.packages.length ? site.packages : [resolved.contentPackage]);
|
|
@@ -1007,7 +858,6 @@ export function buildSite({ config, sqlTables } = {}) {
|
|
|
1007
858
|
wikiErrors: [],
|
|
1008
859
|
imageErrors: [],
|
|
1009
860
|
stats: null,
|
|
1010
|
-
hasTags: homepages.some((p) => hasAnyTag(p.fm)),
|
|
1011
861
|
};
|
|
1012
862
|
}
|
|
1013
863
|
|
|
@@ -1027,23 +877,21 @@ export function buildSite({ config, sqlTables } = {}) {
|
|
|
1027
877
|
imageErrors: [],
|
|
1028
878
|
stats: {
|
|
1029
879
|
homepages: writeHomepages(homeRoot, homepages, resolved),
|
|
1030
|
-
landings: 0,
|
|
1031
880
|
out: homeRoot,
|
|
1032
881
|
},
|
|
1033
|
-
hasTags: homepages.some((p) => hasAnyTag(p.fm)),
|
|
1034
882
|
};
|
|
1035
883
|
}
|
|
1036
884
|
|
|
1037
885
|
const content = collectContentPages(resolved.paths.content, ctx);
|
|
1038
886
|
const { pages } = content;
|
|
1039
887
|
|
|
1040
|
-
// The homepage is **indexed but not rendered**.
|
|
1041
|
-
//
|
|
1042
|
-
//
|
|
1043
|
-
//
|
|
1044
|
-
// authored markdown published verbatim, with no table
|
|
1045
|
-
// resolution of its own, and routing it through
|
|
1046
|
-
// a pass it has no input for.
|
|
888
|
+
// The homepage is **indexed but not rendered**. `[[homepage-root|Text]]`
|
|
889
|
+
// is an ordinary wikilink and has to resolve to the page the build
|
|
890
|
+
// publishes — which means the address index must hold it, at the address
|
|
891
|
+
// it publishes at: the package root. It takes no part in `renderPages`: a
|
|
892
|
+
// homepage is authored markdown published verbatim, with no table
|
|
893
|
+
// expansion and no link resolution of its own, and routing it through
|
|
894
|
+
// that pipeline would buy it a pass it has no input for.
|
|
1047
895
|
const homepageEntries = homepages.map((page) => ({
|
|
1048
896
|
kind: "content",
|
|
1049
897
|
fm: page.fm,
|
|
@@ -1051,7 +899,7 @@ export function buildSite({ config, sqlTables } = {}) {
|
|
|
1051
899
|
name: page.fm.name?.full ?? homepageTitle(page.fm, resolved),
|
|
1052
900
|
slug: addressSlug(page.fm),
|
|
1053
901
|
base: path.basename(page.file),
|
|
1054
|
-
url:
|
|
902
|
+
url: base,
|
|
1055
903
|
}));
|
|
1056
904
|
|
|
1057
905
|
const gates = siteGates([...pages, ...homepageEntries], content, { config: resolved });
|
|
@@ -1062,7 +910,6 @@ export function buildSite({ config, sqlTables } = {}) {
|
|
|
1062
910
|
tableErrors: [],
|
|
1063
911
|
wikiErrors: [],
|
|
1064
912
|
imageErrors: [],
|
|
1065
|
-
hasTags: [...pages, ...homepageEntries].some((p) => hasAnyTag(p.fm)),
|
|
1066
913
|
};
|
|
1067
914
|
}
|
|
1068
915
|
|
|
@@ -1099,12 +946,6 @@ export function buildSite({ config, sqlTables } = {}) {
|
|
|
1099
946
|
},
|
|
1100
947
|
});
|
|
1101
948
|
|
|
1102
|
-
const landings = writeSectionLandings(out, {
|
|
1103
|
-
sections: site.sections,
|
|
1104
|
-
landing: site.landing,
|
|
1105
|
-
sectionTitle: site.backfillSections ? pluralTitle : null,
|
|
1106
|
-
});
|
|
1107
|
-
|
|
1108
949
|
// Last, and outside the mount: the package's front page is not part of the
|
|
1109
950
|
// content tree it introduces.
|
|
1110
951
|
const homepagesWritten = writeHomepages(homeRoot, homepages, resolved);
|
|
@@ -1117,10 +958,8 @@ export function buildSite({ config, sqlTables } = {}) {
|
|
|
1117
958
|
stats: {
|
|
1118
959
|
...rendered.byKind,
|
|
1119
960
|
homepages: homepagesWritten,
|
|
1120
|
-
landings,
|
|
1121
961
|
out,
|
|
1122
962
|
},
|
|
1123
|
-
hasTags: [...pages, ...homepageEntries].some((p) => hasAnyTag(p.fm)),
|
|
1124
963
|
};
|
|
1125
964
|
}
|
|
1126
965
|
|
package/engine/site-config.mjs
CHANGED
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
* and the site build writes `[menu.main]` from the cache.
|
|
33
33
|
*
|
|
34
34
|
* What a repository still says for itself is the residue that is genuinely
|
|
35
|
-
* its own — the wording of its "page not found" page
|
|
36
|
-
*
|
|
35
|
+
* its own — the wording of its "page not found" page — and, through
|
|
36
|
+
* `site.hugo`, the one key nobody anticipated.
|
|
37
37
|
* {@link module:content-config.DERIVED_HUGO_KEYS} refuses everything the
|
|
38
38
|
* generator writes from being authored there too.
|
|
39
39
|
*
|
|
@@ -83,38 +83,17 @@ export const BRAND = Object.freeze({
|
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
|
-
* The kinds
|
|
87
|
-
*
|
|
88
|
-
* A
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* The kinds a site with at least one tagged note renders — everything but
|
|
98
|
-
* `RSS`.
|
|
99
|
-
*/
|
|
100
|
-
const DISABLE_KINDS_TAGGED = Object.freeze(["RSS"]);
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* The single taxonomy a tagged site declares.
|
|
104
|
-
*
|
|
105
|
-
* Only `tag` — Hugo's default pair also declares `category`, which nothing
|
|
106
|
-
* here authors and which would publish an empty `/categories/`.
|
|
86
|
+
* The kinds no site renders.
|
|
87
|
+
*
|
|
88
|
+
* A site is its homepage and its pages: `home` and `page` are the only kinds,
|
|
89
|
+
* for every package and whatever its notes carry. A `section` is a listing
|
|
90
|
+
* Hugo would generate from a content directory; `taxonomy` and `term` are
|
|
91
|
+
* listings it would generate from `tags:`; `RSS` is a feed. Every structure
|
|
92
|
+
* between the homepage and the pages is authored instead, as a `doc` note
|
|
93
|
+
* carrying a content table — a tag is a field such a table filters on, not a
|
|
94
|
+
* page of its own.
|
|
107
95
|
*/
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* The taxonomy output formats a tagged site declares — `HTML` only, so no
|
|
112
|
-
* feed is produced for `/tags/` or a single tag.
|
|
113
|
-
*/
|
|
114
|
-
const TAXONOMY_OUTPUTS = Object.freeze({
|
|
115
|
-
taxonomy: Object.freeze(["HTML"]),
|
|
116
|
-
term: Object.freeze(["HTML"]),
|
|
117
|
-
});
|
|
96
|
+
export const DISABLE_KINDS = Object.freeze(["section", "taxonomy", "term", "RSS"]);
|
|
118
97
|
|
|
119
98
|
/**
|
|
120
99
|
* The markup settings the toolchain's own output requires.
|
|
@@ -383,15 +362,12 @@ function deepMerge(base, overrides) {
|
|
|
383
362
|
* @param {object} options.config - The resolved build configuration.
|
|
384
363
|
* @param {readonly NavigationEntry[]} options.navigation - The navigation.
|
|
385
364
|
* @param {string} options.themesDir - From {@link resolveThemesDir}.
|
|
386
|
-
* @param {boolean} [options.hasTags] - Whether any note the site build walked
|
|
387
|
-
* carries `tags:`, from {@link module:engine/site-build.buildSite}'s
|
|
388
|
-
* `hasTags`. Defaults to `false` — no tagged note, no taxonomy pages.
|
|
389
365
|
* @returns {Record<string, any>} The configuration Hugo reads.
|
|
390
366
|
* @throws {TypeError} When `homepage` fails `checkHomepage`, or the
|
|
391
367
|
* configuration declares no `packageBuild.manifest.title`, no
|
|
392
368
|
* `site.description`, or no `site.assets`.
|
|
393
369
|
*/
|
|
394
|
-
export function hugoConfig({ config, navigation, themesDir
|
|
370
|
+
export function hugoConfig({ config, navigation, themesDir }) {
|
|
395
371
|
checkHomepage(config.homepage, config.contentPackage);
|
|
396
372
|
|
|
397
373
|
const title = config.packageBuild?.manifest?.title;
|
|
@@ -421,7 +397,6 @@ export function hugoConfig({ config, navigation, themesDir, hasTags = false }) {
|
|
|
421
397
|
if (config.author?.name) params.author = config.author.name;
|
|
422
398
|
if (config.site.assets) params.cdnBaseURL = config.site.assets;
|
|
423
399
|
params.brand = { ...BRAND };
|
|
424
|
-
params.list = { ...config.site.list };
|
|
425
400
|
if (config.site.notfound) params.notfound = structuredClone(config.site.notfound);
|
|
426
401
|
|
|
427
402
|
const generated = {
|
|
@@ -432,18 +407,11 @@ export function hugoConfig({ config, navigation, themesDir, hasTags = false }) {
|
|
|
432
407
|
themesDir,
|
|
433
408
|
theme: THEME,
|
|
434
409
|
contentDir: path.posix.relative(HUGO_SOURCE, HUGO_CONTENT),
|
|
435
|
-
disableKinds:
|
|
410
|
+
disableKinds: [...DISABLE_KINDS],
|
|
436
411
|
params,
|
|
437
412
|
markup: structuredClone(MARKUP),
|
|
438
413
|
menu: { main: menuEntries(navigation) },
|
|
439
414
|
};
|
|
440
|
-
if (hasTags) {
|
|
441
|
-
generated.taxonomies = { ...TAXONOMIES };
|
|
442
|
-
generated.outputs = {
|
|
443
|
-
taxonomy: [...TAXONOMY_OUTPUTS.taxonomy],
|
|
444
|
-
term: [...TAXONOMY_OUTPUTS.term],
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
415
|
return deepMerge(generated, config.site.hugo);
|
|
448
416
|
}
|
|
449
417
|
|
|
@@ -470,20 +438,14 @@ export function hugoToml(generated) {
|
|
|
470
438
|
* run this before touching the output tree and fail with it intact.
|
|
471
439
|
*
|
|
472
440
|
* @param {object} config - The resolved build configuration.
|
|
473
|
-
* @param {object} [options] - Options.
|
|
474
|
-
* @param {boolean} [options.hasTags] - Whether any note the site build walked
|
|
475
|
-
* carries `tags:`. Defaults to `false`, so a caller generating the
|
|
476
|
-
* configuration before the walk (to fail fast on a missing source) gets the
|
|
477
|
-
* untagged shape; pass the site build's own `hasTags` once it is known.
|
|
478
441
|
* @returns {Record<string, any>} The configuration Hugo reads.
|
|
479
442
|
* @throws {Error} When any source is missing or wrong.
|
|
480
443
|
*/
|
|
481
|
-
export function generateHugoConfig(config
|
|
444
|
+
export function generateHugoConfig(config) {
|
|
482
445
|
return hugoConfig({
|
|
483
446
|
config,
|
|
484
447
|
navigation: readCachedNavigation(config),
|
|
485
448
|
themesDir: resolveThemesDir(config.rootDir),
|
|
486
|
-
hasTags,
|
|
487
449
|
});
|
|
488
450
|
}
|
|
489
451
|
|