@heroiclands/package-build 23.12.8 → 24.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +84 -0
- package/README.md +3 -2
- package/assets/preview/gitea.ini +12 -7
- package/assets/styles/content-base.css +293 -24
- package/bin/content-commands.mjs +370 -213
- package/bin/package-build-preview.mjs +2 -2
- package/bin/package-build.mjs +3 -2
- package/ci/check-banners.mjs +7 -7
- package/config.mjs +21 -6
- package/content-config.mjs +199 -80
- package/docs/api.md +110 -89
- package/docs/authoring/links-and-markup.md +469 -97
- package/docs/commands.md +259 -78
- package/docs/configuration.md +211 -103
- package/docs/content-format.md +4 -4
- package/docs/diagnostics.md +44 -2
- package/docs/getting-started.md +1 -1
- package/docs/guides/icons.md +1 -1
- package/docs/guides/module-project.md +5 -8
- package/docs/guides/release-and-deploy.md +2 -2
- package/docs/guides/site.md +112 -23
- package/docs/guides/system-project.md +1 -1
- package/docs/guides/writing-in-invented-scripts.md +1247 -0
- package/docs/index.md +1 -0
- package/docs/project-setup.md +28 -30
- package/docs/reference/format-details.md +677 -319
- package/docs/reference/note-types.md +47 -10
- package/docs/reference/theme-pages.md +178 -108
- package/docs/source-preview.md +16 -2
- package/engine/anchors.mjs +2 -2
- package/engine/asset-shipping.mjs +22 -10
- package/engine/base-compiler.mjs +62 -104
- package/engine/changelog-lint.mjs +1 -1
- package/engine/code-fences.mjs +3 -3
- package/engine/content-address.mjs +1 -1
- package/engine/content-alerts.mjs +8 -4
- package/engine/content-blocks.mjs +29 -14
- package/engine/content-embeds.mjs +1 -1
- package/engine/content-expand.mjs +376 -0
- package/engine/content-figures.mjs +18 -11
- package/engine/content-format.yaml +8 -0
- package/engine/content-glyphs.mjs +213 -0
- package/engine/content-images.mjs +6 -7
- package/engine/content-index.mjs +1 -1
- package/engine/content-links.mjs +3 -3
- package/engine/content-lint.mjs +1 -1
- package/engine/content-render.mjs +453 -0
- package/engine/content-scripts.mjs +376 -43
- package/engine/content-spans.mjs +6 -3
- package/engine/content-styles.mjs +91 -0
- package/engine/content-symbols.mjs +186 -0
- package/engine/content-verse.mjs +738 -0
- package/engine/core-report.mjs +169 -0
- package/engine/core-vocabulary.mjs +208 -0
- package/engine/dictionary-html.mjs +119 -0
- package/engine/dictionary-rows.mjs +15 -4
- package/engine/field-reference.mjs +5 -5
- package/engine/first-use.mjs +17 -27
- package/engine/frontmatter-lint.mjs +1 -1
- package/engine/heading-attributes.mjs +89 -0
- package/engine/helpers.mjs +61 -101
- package/engine/homepage.mjs +17 -29
- package/engine/html-minify.mjs +70 -0
- package/engine/index.mjs +8 -5
- package/engine/key-case-collisions.mjs +136 -0
- package/engine/language-data.mjs +203 -6
- package/engine/language-rows.mjs +8 -2
- package/engine/language-terms.mjs +186 -16
- package/engine/language-words.mjs +12 -0
- package/engine/logographic.mjs +255 -0
- package/engine/map-lexicon.mjs +262 -47
- package/engine/map-places.mjs +5 -4
- package/engine/markdown-expressions.mjs +0 -2
- package/engine/markdownlint-run.mjs +9 -3
- package/engine/markup-validation.mjs +21 -0
- package/engine/metre-declaration.mjs +134 -0
- package/engine/names.mjs +36 -9
- package/engine/note-matter.mjs +66 -0
- package/engine/note-vocabulary.mjs +187 -3
- package/engine/pdf-build.mjs +69 -133
- package/engine/pdf-render.mjs +205 -58
- package/engine/post-fields.mjs +157 -0
- package/engine/preview-index.mjs +130 -0
- package/engine/preview-source.mjs +450 -0
- package/engine/project-init.mjs +5 -12
- package/engine/prose-config.mjs +3 -3
- package/engine/read-stdin.mjs +29 -0
- package/engine/run-rows.mjs +2 -2
- package/engine/run-words.mjs +6 -112
- package/engine/script-emphasis-terms.mjs +38 -0
- package/engine/script-emphasis.mjs +295 -0
- package/engine/script-font-delivery.mjs +4 -4
- package/engine/site-build.mjs +260 -315
- package/engine/site-config.mjs +26 -238
- package/engine/site-files.mjs +175 -0
- package/engine/site-index.mjs +9 -4
- package/engine/site-preview.mjs +52 -525
- package/engine/site-render.mjs +593 -0
- package/engine/site-root.mjs +22 -13
- package/engine/site-search.mjs +161 -0
- package/engine/site-serve.mjs +273 -0
- package/engine/site-theme-banner.mjs +154 -0
- package/engine/site-theme-urls.mjs +125 -0
- package/engine/site-theme.mjs +926 -0
- package/engine/sql-tables.mjs +20 -3
- package/engine/text-maps.mjs +57 -10
- package/engine/typographer.mjs +248 -0
- package/engine/verse-metre.mjs +357 -0
- package/engine/wikilink-syntax.mjs +29 -47
- package/engine/wikilinks.mjs +758 -279
- package/engine/word-forms.mjs +364 -0
- package/engine/word-rows.mjs +12 -2
- package/engine/word-terms.mjs +38 -4
- package/manifest.mjs +3 -4
- package/package.json +11 -8
- package/{hugo-theme → site-theme}/data/banners.yaml +6 -10
- package/site-theme/layouts/base.hbs +31 -0
- package/site-theme/layouts/blog-list.hbs +30 -0
- package/site-theme/layouts/blog-post.hbs +40 -0
- package/site-theme/layouts/home.hbs +35 -0
- package/site-theme/layouts/list.hbs +20 -0
- package/site-theme/layouts/not-found.hbs +31 -0
- package/site-theme/layouts/page.hbs +7 -0
- package/site-theme/partials/copy-buttons.hbs +37 -0
- package/site-theme/partials/draft-notice.hbs +5 -0
- package/site-theme/partials/exturl.hbs +2 -0
- package/{hugo-theme/layouts/partials/footer.html → site-theme/partials/footer.hbs} +10 -7
- package/site-theme/partials/header.hbs +44 -0
- package/site-theme/partials/hero-banner.hbs +11 -0
- package/site-theme/partials/infobox-entry.hbs +2 -0
- package/site-theme/partials/infobox-section.hbs +31 -0
- package/site-theme/partials/infobox-value.hbs +2 -0
- package/site-theme/partials/infobox.hbs +18 -0
- package/site-theme/partials/list-members.hbs +9 -0
- package/site-theme/partials/nav-links.hbs +5 -0
- package/site-theme/partials/page-body.hbs +48 -0
- package/site-theme/partials/page-list.hbs +14 -0
- package/site-theme/partials/related.hbs +24 -0
- package/site-theme/partials/search-meta.hbs +6 -0
- package/site-theme/partials/search-names.hbs +2 -0
- package/site-theme/partials/search.hbs +52 -0
- package/site-theme/partials/tags.hbs +4 -0
- package/site-theme/partials/toc.hbs +7 -0
- package/{hugo-theme → site-theme}/static/css/style.css +106 -14
- package/stage.mjs +1 -2
- package/types/config.d.mts +8 -4
- package/types/content-config.d.mts +15 -15
- package/types/engine/asset-shipping.d.mts +5 -2
- package/types/engine/base-compiler.d.mts +20 -2
- package/types/engine/code-fences.d.mts +3 -3
- package/types/engine/content-address.d.mts +1 -1
- package/types/engine/content-alerts.d.mts +8 -2
- package/types/engine/content-blocks.d.mts +11 -4
- package/types/engine/content-expand.d.mts +288 -0
- package/types/engine/content-figures.d.mts +11 -3
- package/types/engine/content-glyphs.d.mts +85 -0
- package/types/engine/content-images.d.mts +6 -7
- package/types/engine/content-links.d.mts +1 -1
- package/types/engine/content-render.d.mts +199 -0
- package/types/engine/content-scripts.d.mts +70 -4
- package/types/engine/content-spans.d.mts +1 -1
- package/types/engine/content-styles.d.mts +33 -0
- package/types/engine/content-symbols.d.mts +35 -0
- package/types/engine/content-verse.d.mts +240 -0
- package/types/engine/core-report.d.mts +45 -0
- package/types/engine/core-vocabulary.d.mts +53 -0
- package/types/engine/dictionary-html.d.mts +8 -0
- package/types/engine/dictionary-rows.d.mts +10 -0
- package/types/engine/field-reference.d.mts +2 -2
- package/types/engine/heading-attributes.d.mts +20 -0
- package/types/engine/helpers.d.mts +3 -10
- package/types/engine/homepage.d.mts +11 -24
- package/types/engine/html-minify.d.mts +9 -0
- package/types/engine/index.d.mts +2 -1
- package/types/engine/key-case-collisions.d.mts +28 -0
- package/types/engine/language-data.d.mts +0 -2
- package/types/engine/language-terms.d.mts +24 -2
- package/types/engine/logographic.d.mts +45 -0
- package/types/engine/map-lexicon.d.mts +66 -13
- package/types/engine/map-places.d.mts +8 -0
- package/types/engine/markdown-expressions.d.mts +0 -2
- package/types/engine/metre-declaration.d.mts +39 -0
- package/types/engine/names.d.mts +34 -3
- package/types/engine/note-matter.d.mts +48 -0
- package/types/engine/pdf-render.d.mts +6 -2
- package/types/engine/post-fields.d.mts +44 -0
- package/types/engine/preview-index.d.mts +72 -0
- package/types/engine/preview-source.d.mts +51 -0
- package/types/engine/read-stdin.d.mts +11 -0
- package/types/engine/run-rows.d.mts +2 -2
- package/types/engine/run-words.d.mts +0 -15
- package/types/engine/script-emphasis-terms.d.mts +21 -0
- package/types/engine/script-emphasis.d.mts +84 -0
- package/types/engine/script-font-delivery.d.mts +4 -4
- package/types/engine/site-build.d.mts +71 -113
- package/types/engine/site-config.d.mts +14 -134
- package/types/engine/site-files.d.mts +73 -0
- package/types/engine/site-index.d.mts +11 -3
- package/types/engine/site-preview.d.mts +15 -19
- package/types/engine/site-render.d.mts +171 -0
- package/types/engine/site-root.d.mts +12 -4
- package/types/engine/site-search.d.mts +54 -0
- package/types/engine/site-serve.d.mts +44 -0
- package/types/engine/site-theme-banner.d.mts +51 -0
- package/types/engine/site-theme-urls.d.mts +60 -0
- package/types/engine/site-theme.d.mts +127 -0
- package/types/engine/sql-tables.d.mts +4 -1
- package/types/engine/text-maps.d.mts +17 -3
- package/types/engine/typographer.d.mts +11 -0
- package/types/engine/verse-metre.d.mts +131 -0
- package/types/engine/wikilink-syntax.d.mts +30 -48
- package/types/engine/wikilinks.d.mts +330 -14
- package/types/engine/word-forms.d.mts +83 -0
- package/types/manifest.d.mts +3 -4
- package/types/stage.d.mts +1 -2
- package/types/vendor/markdownlint.d.mts +8 -0
- package/vendor/markdownlint.mjs +20388 -0
- package/engine/content-poetry.mjs +0 -240
- package/engine/web-wikilinks.mjs +0 -594
- package/hugo-theme/layouts/404.html +0 -85
- package/hugo-theme/layouts/_default/baseof.html +0 -67
- package/hugo-theme/layouts/_default/list.html +0 -139
- package/hugo-theme/layouts/_default/single.html +0 -4
- package/hugo-theme/layouts/blog/list.html +0 -54
- package/hugo-theme/layouts/blog-post/single.html +0 -36
- package/hugo-theme/layouts/index.html +0 -75
- package/hugo-theme/layouts/partials/banner-url.html +0 -141
- package/hugo-theme/layouts/partials/cdn-url.html +0 -36
- package/hugo-theme/layouts/partials/dictionary-lists.html +0 -58
- package/hugo-theme/layouts/partials/draft-notice.html +0 -41
- package/hugo-theme/layouts/partials/draft-state.html +0 -26
- package/hugo-theme/layouts/partials/header.html +0 -35
- package/hugo-theme/layouts/partials/hero-banner.html +0 -85
- package/hugo-theme/layouts/partials/infobox/entry.html +0 -18
- package/hugo-theme/layouts/partials/infobox/items.html +0 -26
- package/hugo-theme/layouts/partials/infobox/section.html +0 -56
- package/hugo-theme/layouts/partials/infobox/value.html +0 -43
- package/hugo-theme/layouts/partials/infobox.html +0 -57
- package/hugo-theme/layouts/partials/page-body.html +0 -152
- package/hugo-theme/layouts/partials/related.html +0 -138
- package/hugo-theme/layouts/partials/search.html +0 -84
- package/hugo-theme/layouts/partials/site-url.html +0 -32
- package/hugo-theme/layouts/partials/tags.html +0 -30
- package/hugo-theme/layouts/partials/toc.html +0 -28
- package/hugo-theme/layouts/partials/warn-missing-banner.html +0 -3
- package/hugo-theme/layouts/shortcodes/cartouche.html +0 -64
- package/hugo-theme/layouts/shortcodes/exturl.html +0 -12
- package/hugo-theme/layouts/shortcodes/glyph.html +0 -43
- package/hugo-theme/layouts/shortcodes/pagelist.html +0 -63
- package/hugo-theme/theme.toml +0 -11
- package/types/engine/content-poetry.d.mts +0 -58
- package/types/engine/web-wikilinks.d.mts +0 -107
- /package/{hugo-theme → site-theme}/data/banner-kinds.yaml +0 -0
- /package/{hugo-theme → site-theme}/static/css/sohl-icons.css +0 -0
- /package/{hugo-theme → site-theme}/static/fonts/game-icons-kb.woff2 +0 -0
- /package/{hugo-theme → site-theme}/static/img/banners/takheperu-hieroglyphs.svg +0 -0
- /package/{hugo-theme → site-theme}/static/img/cartouches/takheperu.svg +0 -0
- /package/{hugo-theme → site-theme}/static/js/inline-spoilers.js +0 -0
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* **A site, written as HTML.** {@link writeSite} draws every page of a site
|
|
16
|
+
* through the layouts of {@link module:engine/site-theme} and writes the files
|
|
17
|
+
* that are deployed: one `index.html` per page, `404.html`, `sitemap.xml`, the
|
|
18
|
+
* stylesheets, scripts and fonts the layouts link, and, where the site asks
|
|
19
|
+
* for them, its listings, its feed, a sitemap index and `robots.txt`.
|
|
20
|
+
* {@link packageSite} turns a package's site build into the pages
|
|
21
|
+
* `writeSite` is given.
|
|
22
|
+
*
|
|
23
|
+
* ## The pages of a site
|
|
24
|
+
*
|
|
25
|
+
* A site is given its home page, its regular pages and any section pages its
|
|
26
|
+
* author wrote. The rest is derived:
|
|
27
|
+
*
|
|
28
|
+
* | Page | Where | Lists |
|
|
29
|
+
* | ---- | ----- | ----- |
|
|
30
|
+
* | a section | `<section>/` | the section's pages |
|
|
31
|
+
* | the tags | `tags/` | every tag, with how many pages carry it |
|
|
32
|
+
* | a tag | `tags/<tag>/` | the pages that carry it |
|
|
33
|
+
*
|
|
34
|
+
* A section is the `section` a regular page names; a page that names none is
|
|
35
|
+
* in no section. A section page an author wrote keeps its title and its body,
|
|
36
|
+
* and lists the section's pages the body does not already reach. The tag pages
|
|
37
|
+
* are written only with `tagPages`, and a page's tag links to its page only
|
|
38
|
+
* then.
|
|
39
|
+
*
|
|
40
|
+
* The pages of a section are in the order a reader walks them, which their
|
|
41
|
+
* previous and next links follow: by `weight`, lightest first and unweighted
|
|
42
|
+
* last, then newest first by `date`, then by title in English order, then by
|
|
43
|
+
* address.
|
|
44
|
+
*
|
|
45
|
+
* ## The files beside the pages
|
|
46
|
+
*
|
|
47
|
+
* | Site states | Written |
|
|
48
|
+
* | ----------- | ------- |
|
|
49
|
+
* | always | `404.html`, `sitemap.xml`, `css/code.css` and the theme's own files |
|
|
50
|
+
* | `feed: {section, title}` | `<section>/atom.xml`, the section's pages, newest first |
|
|
51
|
+
* | `sitemaps: [...]` | `sitemap-index.xml`, naming this site's sitemap and each one listed, and `robots.txt` naming them all |
|
|
52
|
+
*
|
|
53
|
+
* A sitemap index and `robots.txt` belong at the root of an origin, so a site
|
|
54
|
+
* served under a path refuses `sitemaps`.
|
|
55
|
+
*
|
|
56
|
+
* Every page is minified as it is written; see
|
|
57
|
+
* {@link module:engine/html-minify}.
|
|
58
|
+
*
|
|
59
|
+
* @module
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
import fs from "node:fs";
|
|
63
|
+
import path from "node:path";
|
|
64
|
+
import { fileURLToPath } from "node:url";
|
|
65
|
+
|
|
66
|
+
import { createRenderer } from "./content-render.mjs";
|
|
67
|
+
import { resolvePathname } from "./pathnames.mjs";
|
|
68
|
+
import { minifyHtml } from "./html-minify.mjs";
|
|
69
|
+
import { mapWorld } from "./map-places.mjs";
|
|
70
|
+
import {
|
|
71
|
+
checkScriptFonts,
|
|
72
|
+
copyScriptFonts,
|
|
73
|
+
scriptFontFiles,
|
|
74
|
+
siteFontsDestination,
|
|
75
|
+
} from "./script-font-delivery.mjs";
|
|
76
|
+
import { buildSite, gatesFailed } from "./site-build.mjs";
|
|
77
|
+
import { readCachedNavigation, siteSettings } from "./site-config.mjs";
|
|
78
|
+
import { atomFeed, robotsTxt, sitemapIndexXml, sitemapXml } from "./site-files.mjs";
|
|
79
|
+
import { prepareTreeSqlTables } from "./sql-tables.mjs";
|
|
80
|
+
import { placeGeography, searchMetadata } from "./site-search.mjs";
|
|
81
|
+
import {
|
|
82
|
+
createSiteTheme,
|
|
83
|
+
humanize,
|
|
84
|
+
plainText,
|
|
85
|
+
siteThemeAssets,
|
|
86
|
+
subjectTags,
|
|
87
|
+
} from "./site-theme.mjs";
|
|
88
|
+
import { basePath, relUrl, tagUrl } from "./site-theme-urls.mjs";
|
|
89
|
+
import { slugify } from "./content-slug.mjs";
|
|
90
|
+
import { isPost } from "./post-fields.mjs";
|
|
91
|
+
import { siteBase } from "../content-config.mjs";
|
|
92
|
+
|
|
93
|
+
/** This package's root. */
|
|
94
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The files the layouts link that the theme ships as they are: the site and
|
|
98
|
+
* icon stylesheets, the spoiler script, the icon font and the theme's images.
|
|
99
|
+
*/
|
|
100
|
+
export const SITE_STATIC_DIR = path.join(PACKAGE_ROOT, "site-theme", "static");
|
|
101
|
+
|
|
102
|
+
/** The words of a post a feed entry summarises it with. */
|
|
103
|
+
const SUMMARY_WORDS = 70;
|
|
104
|
+
|
|
105
|
+
/** @param {unknown} value */
|
|
106
|
+
const text = (value) => (value === undefined || value === null ? "" : String(value));
|
|
107
|
+
|
|
108
|
+
/** Titles in the order an English reader sorts them. */
|
|
109
|
+
const collator = new Intl.Collator("en", { sensitivity: "variant" });
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* A title as it is sorted: an apostrophe typed as `'` sorts before one typed
|
|
113
|
+
* as `’`, rather than level with it. It is read as U+205E, the punctuation
|
|
114
|
+
* mark the collation places just before both.
|
|
115
|
+
*/
|
|
116
|
+
const sortKey = (title) => text(title).replaceAll("'", "\u205e");
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Pages in the order a reader walks them: by `weight`, lightest first and an
|
|
120
|
+
* unweighted page last; then newest first by `date`; then by title; then by
|
|
121
|
+
* address. A page's own `weight` and `date` are read before its `params`'.
|
|
122
|
+
*
|
|
123
|
+
* @template {{title?: string, url: string, date?: unknown, weight?: number, params?: object}} T
|
|
124
|
+
* @param {readonly T[]} pages - The pages.
|
|
125
|
+
* @returns {T[]} A sorted copy.
|
|
126
|
+
*/
|
|
127
|
+
export function siteOrder(pages) {
|
|
128
|
+
const weight = (page) => {
|
|
129
|
+
const value = Number(page.weight ?? page.params?.weight);
|
|
130
|
+
return value > 0 ? value : Infinity;
|
|
131
|
+
};
|
|
132
|
+
const time = (page) => {
|
|
133
|
+
const value = page.date ?? page.params?.date;
|
|
134
|
+
const date = value ? new Date(String(value)).getTime() : 0;
|
|
135
|
+
return Number.isNaN(date) ? 0 : date;
|
|
136
|
+
};
|
|
137
|
+
return [...pages].sort((a, b) => {
|
|
138
|
+
if (weight(a) !== weight(b)) return weight(a) < weight(b) ? -1 : 1;
|
|
139
|
+
if (time(a) !== time(b)) return time(b) - time(a);
|
|
140
|
+
const titles = collator.compare(sortKey(a.title), sortKey(b.title));
|
|
141
|
+
if (titles) return titles;
|
|
142
|
+
return (
|
|
143
|
+
a.url < b.url ? -1
|
|
144
|
+
: a.url > b.url ? 1
|
|
145
|
+
: 0
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** A page as a sibling names it. */
|
|
151
|
+
const siblingOf = (page) => ({
|
|
152
|
+
url: page.url,
|
|
153
|
+
title: page.title,
|
|
154
|
+
type: page.params?.type,
|
|
155
|
+
subType: page.params?.subType,
|
|
156
|
+
category: page.params?.category,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
/** A page as a listing names it. */
|
|
160
|
+
const memberOf = (page) => ({
|
|
161
|
+
url: page.url,
|
|
162
|
+
title: page.title,
|
|
163
|
+
description: page.params?.description,
|
|
164
|
+
content: page.content,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Every page of a site: the pages it is given, with their siblings, and the
|
|
169
|
+
* listings derived from them.
|
|
170
|
+
*
|
|
171
|
+
* @param {object} site - The site, as {@link writeSite} takes it.
|
|
172
|
+
* @param {readonly object[]} pages - The home page, the regular pages and any
|
|
173
|
+
* authored section pages.
|
|
174
|
+
* @returns {object[]} Every page, home first, then the regular pages, the
|
|
175
|
+
* sections, and the tag pages.
|
|
176
|
+
*/
|
|
177
|
+
export function siteTree(site, pages) {
|
|
178
|
+
const home = pages.filter((page) => page.kind === "home");
|
|
179
|
+
const regular = pages.filter((page) => page.kind === "page");
|
|
180
|
+
const authored = new Map(
|
|
181
|
+
pages.filter((page) => page.kind === "section").map((page) => [page.section, page]),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
/** @type {Map<string, object[]>} */
|
|
185
|
+
const sections = new Map();
|
|
186
|
+
for (const page of regular) {
|
|
187
|
+
const key = text(page.section);
|
|
188
|
+
if (!sections.has(key)) sections.set(key, []);
|
|
189
|
+
sections.get(key).push(page);
|
|
190
|
+
}
|
|
191
|
+
const listings = [];
|
|
192
|
+
for (const [key, list] of sections) {
|
|
193
|
+
const ordered = siteOrder(list);
|
|
194
|
+
const siblings = ordered.map(siblingOf);
|
|
195
|
+
for (const page of ordered) page.siblings = siblings;
|
|
196
|
+
if (!key) continue;
|
|
197
|
+
const page = authored.get(key) ?? {
|
|
198
|
+
kind: "section",
|
|
199
|
+
section: key,
|
|
200
|
+
title: humanize(key),
|
|
201
|
+
url: relUrl(site, `${key}/`),
|
|
202
|
+
params: {},
|
|
203
|
+
content: "",
|
|
204
|
+
};
|
|
205
|
+
listings.push({ ...page, members: ordered.map(memberOf) });
|
|
206
|
+
}
|
|
207
|
+
for (const [key, page] of authored)
|
|
208
|
+
if (!sections.has(key)) listings.push({ ...page, members: [] });
|
|
209
|
+
|
|
210
|
+
if (!site.tagPages) return [...home, ...regular, ...listings];
|
|
211
|
+
|
|
212
|
+
/** @type {Map<string, {name: string, pages: object[]}>} */
|
|
213
|
+
const tags = new Map();
|
|
214
|
+
for (const page of regular)
|
|
215
|
+
for (const name of subjectTags(page.params)) {
|
|
216
|
+
const key = slugify(name);
|
|
217
|
+
if (!key) continue;
|
|
218
|
+
if (!tags.has(key)) tags.set(key, { name, pages: [] });
|
|
219
|
+
tags.get(key).pages.push(page);
|
|
220
|
+
}
|
|
221
|
+
const terms = [...tags.values()].map((tag) => ({
|
|
222
|
+
kind: "term",
|
|
223
|
+
title: tag.name,
|
|
224
|
+
url: tagUrl(site, tag.name),
|
|
225
|
+
params: {},
|
|
226
|
+
content: "",
|
|
227
|
+
members: siteOrder(tag.pages).map(memberOf),
|
|
228
|
+
count: tag.pages.length,
|
|
229
|
+
date: tag.pages
|
|
230
|
+
.map((page) => text(page.date))
|
|
231
|
+
.filter(Boolean)
|
|
232
|
+
.sort()
|
|
233
|
+
.at(-1),
|
|
234
|
+
}));
|
|
235
|
+
const taxonomy = {
|
|
236
|
+
kind: "taxonomy",
|
|
237
|
+
title: "Tags",
|
|
238
|
+
url: relUrl(site, "tags/"),
|
|
239
|
+
params: {},
|
|
240
|
+
content: "",
|
|
241
|
+
members: siteOrder(terms).map((term) => ({
|
|
242
|
+
url: term.url,
|
|
243
|
+
title: term.title,
|
|
244
|
+
count: term.count,
|
|
245
|
+
})),
|
|
246
|
+
};
|
|
247
|
+
return [...home, ...regular, ...listings, taxonomy, ...terms];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The file a page is written to.
|
|
252
|
+
*
|
|
253
|
+
* @param {object} site - The site.
|
|
254
|
+
* @param {string} out - The directory the site is written to.
|
|
255
|
+
* @param {string} url - The page's address, under the site's base path.
|
|
256
|
+
* @returns {string} The `index.html` the address serves.
|
|
257
|
+
* @throws {Error} When the address is not under the site's base path.
|
|
258
|
+
*/
|
|
259
|
+
function pageFile(site, out, url) {
|
|
260
|
+
const base = basePath(site);
|
|
261
|
+
if (!url.startsWith(base))
|
|
262
|
+
throw new Error(`the page ${url} is not under the site's base path ${base}`);
|
|
263
|
+
return path.join(out, ...url.slice(base.length).split("/").filter(Boolean), "index.html");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Write the files the layouts link: the theme's own, and the code stylesheet.
|
|
268
|
+
*
|
|
269
|
+
* @param {string} out - The directory the site is written to.
|
|
270
|
+
* @param {string} [staticDir] - The theme's own files.
|
|
271
|
+
* @returns {string[]} The files the theme generates, each written. The theme's
|
|
272
|
+
* own files are copied as a tree and not listed.
|
|
273
|
+
*/
|
|
274
|
+
export function writeThemeFiles(out, staticDir = SITE_STATIC_DIR) {
|
|
275
|
+
fs.mkdirSync(out, { recursive: true });
|
|
276
|
+
if (fs.existsSync(staticDir)) fs.cpSync(staticDir, out, { recursive: true });
|
|
277
|
+
return siteThemeAssets().map((asset) => {
|
|
278
|
+
const file = path.join(out, asset.path);
|
|
279
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
280
|
+
fs.writeFileSync(file, asset.content);
|
|
281
|
+
return file;
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Draw a site and write it.
|
|
287
|
+
*
|
|
288
|
+
* @param {object} options - The inputs.
|
|
289
|
+
* @param {object} options.site - The site, as
|
|
290
|
+
* {@link module:engine/site-theme.createSiteTheme} takes it, with `tagPages`,
|
|
291
|
+
* `feed` and `sitemaps` as the module description sets out. `pages` and
|
|
292
|
+
* `pageExists` are filled in here.
|
|
293
|
+
* @param {readonly object[]} options.pages - The home page, the regular pages
|
|
294
|
+
* and any authored section pages, each as a layout is given it.
|
|
295
|
+
* @param {string} options.out - The directory written to. Emptied first.
|
|
296
|
+
* @param {(finding: import("./site-theme.mjs").Finding) => void} [options.report] -
|
|
297
|
+
* Receives each finding the layouts make.
|
|
298
|
+
* @param {string} [options.staticDir] - The theme's own files, copied first.
|
|
299
|
+
* @param {ReadonlySet<string>} [options.only] - The addresses of the pages to
|
|
300
|
+
* draw again into a site already written. Nothing else is touched.
|
|
301
|
+
* @returns {{files: string[], pages: string[], findings: object[]}} Every file
|
|
302
|
+
* written, the address of every page, and the layouts' findings.
|
|
303
|
+
* @throws {Error} When a page's address is outside the site, or a site served
|
|
304
|
+
* under a path asks for a sitemap index.
|
|
305
|
+
*/
|
|
306
|
+
export function writeSite({ site, pages, out, report, staticDir = SITE_STATIC_DIR, only }) {
|
|
307
|
+
if (site.sitemaps && basePath(site) !== "/")
|
|
308
|
+
throw new Error(
|
|
309
|
+
`a sitemap index and robots.txt belong at the root of an origin, and this site is served under ${basePath(site)}`,
|
|
310
|
+
);
|
|
311
|
+
const tree = siteTree(site, pages);
|
|
312
|
+
const published = new Set(tree.map((page) => page.url));
|
|
313
|
+
const feedUrl = site.feed ? relUrl(site, `${site.feed.section}/atom.xml`) : "";
|
|
314
|
+
const theme = createSiteTheme({
|
|
315
|
+
site: {
|
|
316
|
+
...site,
|
|
317
|
+
feed:
|
|
318
|
+
site.feed ?
|
|
319
|
+
{
|
|
320
|
+
title: site.feed.title ?? site.title,
|
|
321
|
+
url: feedUrl,
|
|
322
|
+
section: text(site.feed.section),
|
|
323
|
+
}
|
|
324
|
+
: undefined,
|
|
325
|
+
pages: tree
|
|
326
|
+
.filter((page) => page.kind === "page")
|
|
327
|
+
.map((page) => ({
|
|
328
|
+
url: page.url,
|
|
329
|
+
title: page.title,
|
|
330
|
+
section: text(page.section),
|
|
331
|
+
type: page.params?.type,
|
|
332
|
+
subType: page.params?.subType,
|
|
333
|
+
date: page.date ?? page.params?.date,
|
|
334
|
+
content: page.content,
|
|
335
|
+
})),
|
|
336
|
+
pageExists: (url) => published.has(url) || published.has(`${url.replace(/\/?$/, "/")}`),
|
|
337
|
+
},
|
|
338
|
+
report,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const files = [];
|
|
342
|
+
const write = (file, content) => {
|
|
343
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
344
|
+
fs.writeFileSync(file, content);
|
|
345
|
+
files.push(file);
|
|
346
|
+
};
|
|
347
|
+
const origin = (url) => new URL(url, site.baseURL).href;
|
|
348
|
+
const draw = (page) =>
|
|
349
|
+
write(
|
|
350
|
+
pageFile(site, out, page.url),
|
|
351
|
+
minifyHtml(theme.render({ ...page, permalink: origin(page.url) })),
|
|
352
|
+
);
|
|
353
|
+
if (only) {
|
|
354
|
+
for (const page of tree) if (only.has(page.url)) draw(page);
|
|
355
|
+
return { files, pages: tree.map((page) => page.url), findings: theme.findings };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
fs.rmSync(out, { recursive: true, force: true });
|
|
359
|
+
files.push(...writeThemeFiles(out, staticDir));
|
|
360
|
+
for (const page of tree) draw(page);
|
|
361
|
+
write(
|
|
362
|
+
path.join(out, "404.html"),
|
|
363
|
+
minifyHtml(
|
|
364
|
+
theme.render({
|
|
365
|
+
kind: "404",
|
|
366
|
+
title: "Page not found",
|
|
367
|
+
url: relUrl(site, "404.html"),
|
|
368
|
+
params: {},
|
|
369
|
+
}),
|
|
370
|
+
),
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
write(
|
|
374
|
+
path.join(out, "sitemap.xml"),
|
|
375
|
+
sitemapXml(
|
|
376
|
+
tree.map((page) => ({
|
|
377
|
+
loc: origin(page.url),
|
|
378
|
+
lastmod: page.params?.lastmod ?? page.date ?? page.params?.date,
|
|
379
|
+
})),
|
|
380
|
+
),
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
if (site.feed) {
|
|
384
|
+
const section = text(site.feed.section);
|
|
385
|
+
const time = (page) => new Date(text(page.date ?? page.params?.date)).getTime() || 0;
|
|
386
|
+
const posts = tree
|
|
387
|
+
.filter((page) => page.kind === "page" && text(page.section) === section)
|
|
388
|
+
.sort((a, b) => time(b) - time(a));
|
|
389
|
+
write(
|
|
390
|
+
path.join(out, ...section.split("/").filter(Boolean), "atom.xml"),
|
|
391
|
+
atomFeed({
|
|
392
|
+
title: site.feed.title ?? site.title,
|
|
393
|
+
id: origin(relUrl(site, `${section}/`)),
|
|
394
|
+
self: origin(feedUrl),
|
|
395
|
+
author: site.params?.author,
|
|
396
|
+
entries: posts.map((post) => {
|
|
397
|
+
const words = plainText(post.content).split(" ").filter(Boolean);
|
|
398
|
+
return {
|
|
399
|
+
title: post.title,
|
|
400
|
+
url: origin(post.url),
|
|
401
|
+
date: post.date ?? post.params?.date,
|
|
402
|
+
author: text(post.params?.data?.author) || site.params?.author,
|
|
403
|
+
summary:
|
|
404
|
+
text(post.params?.description) ||
|
|
405
|
+
(words.length > SUMMARY_WORDS ?
|
|
406
|
+
`${words.slice(0, SUMMARY_WORDS).join(" ")}…`
|
|
407
|
+
: words.join(" ")),
|
|
408
|
+
content: post.content,
|
|
409
|
+
};
|
|
410
|
+
}),
|
|
411
|
+
}),
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (site.sitemaps) {
|
|
416
|
+
const own = origin(relUrl(site, "sitemap.xml"));
|
|
417
|
+
const index = origin(relUrl(site, "sitemap-index.xml"));
|
|
418
|
+
const all = [own, ...site.sitemaps.filter((loc) => loc !== own)];
|
|
419
|
+
write(path.join(out, "sitemap-index.xml"), sitemapIndexXml(all));
|
|
420
|
+
write(path.join(out, "robots.txt"), robotsTxt([index, ...all]));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return { files, pages: tree.map((page) => page.url), findings: theme.findings };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* A package's pages, from its site build.
|
|
428
|
+
*
|
|
429
|
+
* Each body is rendered by the `web` renderer from the markdown the build
|
|
430
|
+
* expanded, and each page carries what it tells the search; see
|
|
431
|
+
* {@link module:engine/site-search}. The site carries what the configuration
|
|
432
|
+
* asks for: `site.tags` as `tagPages`, `site.feed` as the feed's section, each
|
|
433
|
+
* package of `site.sitemaps` as its sitemap's address on the site's origin,
|
|
434
|
+
* and each of `site.mergeIndexes` as the path of its search bundle.
|
|
435
|
+
*
|
|
436
|
+
* @param {object} options - The inputs.
|
|
437
|
+
* @param {object} options.config - The resolved configuration.
|
|
438
|
+
* @param {object} options.built - What `buildSite` returned with `write: false`.
|
|
439
|
+
* @param {{baseURL: string, title: string, params: object}} options.settings -
|
|
440
|
+
* From {@link module:engine/site-config.siteSettings}.
|
|
441
|
+
* @param {readonly {name: string, url: string, children?: object[]}[]} [options.navigation] -
|
|
442
|
+
* The menu.
|
|
443
|
+
* @param {number} [options.year] - The year the footer states.
|
|
444
|
+
* @returns {{site: object, pages: object[], pageOf: (entry: object) => object}}
|
|
445
|
+
* What {@link writeSite} is given, and how one note the build rendered again
|
|
446
|
+
* becomes its page.
|
|
447
|
+
*/
|
|
448
|
+
export function packageSite({ config, built, settings, navigation = [], year }) {
|
|
449
|
+
const renderer = createRenderer("web", { icons: config.icons });
|
|
450
|
+
const resolveSrc = (src) => resolvePathname(src, config)?.web ?? null;
|
|
451
|
+
const { site: from } = built;
|
|
452
|
+
const relative = (file) =>
|
|
453
|
+
file ? path.relative(process.cwd(), file).split(path.sep).join("/") : undefined;
|
|
454
|
+
const world = mapWorld({
|
|
455
|
+
records: from.records,
|
|
456
|
+
foreignIndex: from.foreign?.index ?? new Map(),
|
|
457
|
+
contentBase: config.paths.content,
|
|
458
|
+
base: from.base,
|
|
459
|
+
config,
|
|
460
|
+
});
|
|
461
|
+
const geography = placeGeography(world.places);
|
|
462
|
+
const search = (data) => searchMetadata(data, { geography, index: from.index });
|
|
463
|
+
|
|
464
|
+
/** Each note `site.sections` names, by address, with the section it is the page of. */
|
|
465
|
+
const sectionOf = new Map(
|
|
466
|
+
Object.entries(config.site.sections).map(([name, address]) => [address, name]),
|
|
467
|
+
);
|
|
468
|
+
/**
|
|
469
|
+
* A content page, from what the build made of its note: a section's page
|
|
470
|
+
* for a note `site.sections` names, and a page of the feed's section for
|
|
471
|
+
* a post.
|
|
472
|
+
*/
|
|
473
|
+
const pageOf = ({ page, markdown, figures, data }) => {
|
|
474
|
+
const section = sectionOf.get(page.slug);
|
|
475
|
+
return {
|
|
476
|
+
kind: section ? "section" : "page",
|
|
477
|
+
title: text(data.title),
|
|
478
|
+
url: page.url,
|
|
479
|
+
section: section ?? (isPost(data) ? config.site.feed : ""),
|
|
480
|
+
params: data,
|
|
481
|
+
date: isPost(data) ? data.data?.date : undefined,
|
|
482
|
+
weight: data.weight,
|
|
483
|
+
content: renderer.render(markdown, { runs: from.runs, figures, resolveSrc }),
|
|
484
|
+
source: relative(page.file),
|
|
485
|
+
search: search(data),
|
|
486
|
+
};
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const pages = [];
|
|
490
|
+
for (const home of from.homepages)
|
|
491
|
+
pages.push({
|
|
492
|
+
kind: "home",
|
|
493
|
+
title: text(home.data.title),
|
|
494
|
+
url: from.base,
|
|
495
|
+
params: home.data,
|
|
496
|
+
content: renderer.render(home.body, { runs: from.runs, resolveSrc }),
|
|
497
|
+
source: relative(home.file),
|
|
498
|
+
search: search(home.data),
|
|
499
|
+
});
|
|
500
|
+
for (const entry of from.pages) pages.push(pageOf(entry));
|
|
501
|
+
|
|
502
|
+
const site = {
|
|
503
|
+
title: settings.title,
|
|
504
|
+
baseURL: settings.baseURL,
|
|
505
|
+
params: { ...settings.params, search: config.site.search !== false },
|
|
506
|
+
menu: navigation.map((entry) => ({
|
|
507
|
+
name: entry.name,
|
|
508
|
+
url: entry.url,
|
|
509
|
+
children: (entry.children ?? []).map((child) => ({ name: child.name, url: child.url })),
|
|
510
|
+
})),
|
|
511
|
+
year,
|
|
512
|
+
tagPages: config.site.tags,
|
|
513
|
+
...(config.site.feed ? { feed: { section: config.site.feed } } : {}),
|
|
514
|
+
...(config.site.sitemaps.length ?
|
|
515
|
+
{
|
|
516
|
+
sitemaps: config.site.sitemaps.map(
|
|
517
|
+
(pkg) => new URL(`/${pkg}/sitemap.xml`, settings.baseURL).href,
|
|
518
|
+
),
|
|
519
|
+
}
|
|
520
|
+
: {}),
|
|
521
|
+
...(config.site.mergeIndexes.length ?
|
|
522
|
+
{ mergeIndexes: config.site.mergeIndexes.map((pkg) => `/${pkg}/pagefind/`) }
|
|
523
|
+
: {}),
|
|
524
|
+
};
|
|
525
|
+
return { site, pages, pageOf };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* The directory a package's site is written to: its base path under the
|
|
530
|
+
* deployed root, so a site served at `/` is written at the root itself.
|
|
531
|
+
*
|
|
532
|
+
* @param {object} config - The resolved configuration.
|
|
533
|
+
* @param {string} root - The deployed root.
|
|
534
|
+
* @returns {string} The directory.
|
|
535
|
+
*/
|
|
536
|
+
export function siteDirectory(config, root) {
|
|
537
|
+
return path.join(root, ...siteBase(config).split("/").filter(Boolean));
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Copy the script fonts the site's pages set their marked text in.
|
|
542
|
+
*
|
|
543
|
+
* @param {object} config - The resolved configuration.
|
|
544
|
+
* @param {string} out - The directory the site is written to.
|
|
545
|
+
* @returns {{findings: object[], count: number, dest?: string}} What checking
|
|
546
|
+
* the installed fonts found, and how many files were copied where. Nothing is
|
|
547
|
+
* copied when there is a finding, or when the package sets no script.
|
|
548
|
+
*/
|
|
549
|
+
export function deliverSiteFonts(config, out) {
|
|
550
|
+
const found = scriptFontFiles(config, ["woff2"]);
|
|
551
|
+
if (!found) return { findings: [], count: 0 };
|
|
552
|
+
const findings = checkScriptFonts(config);
|
|
553
|
+
if (findings.length) return { findings, count: 0 };
|
|
554
|
+
const dest = path.join(out, siteFontsDestination(found.version));
|
|
555
|
+
return { findings, count: copyScriptFonts(config, dest, ["woff2"]), dest };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Build a package's site from its content tree and write it.
|
|
560
|
+
*
|
|
561
|
+
* The site's address, title and menu are read before the tree is, so a site
|
|
562
|
+
* that cannot be described fails before anything is built. A gate that fires
|
|
563
|
+
* stops the build before anything is written; a note in error is written as
|
|
564
|
+
* its page shows it, with the finding in what is returned, which
|
|
565
|
+
* `siteBuildFailed` reads.
|
|
566
|
+
*
|
|
567
|
+
* @param {object} options - The inputs.
|
|
568
|
+
* @param {object} options.config - The resolved configuration.
|
|
569
|
+
* @param {string} options.out - The directory the site is written to, which
|
|
570
|
+
* is the package's own directory under the deployed root.
|
|
571
|
+
* @param {number} [options.year] - The year the footer states.
|
|
572
|
+
* @returns {Promise<{built: object, written: object|null, fonts: object|null,
|
|
573
|
+
* site?: object, pages?: object[], pageOf?: (entry: object) => object}>}
|
|
574
|
+
* What the build found, and, when it found nothing that stops it, what was
|
|
575
|
+
* written, the pages it was written from and how a note rendered again
|
|
576
|
+
* becomes its page. `written` is `null` when a gate fired.
|
|
577
|
+
* @throws {Error} When the site's settings or its cached menu cannot be read.
|
|
578
|
+
*/
|
|
579
|
+
export async function buildPackageSite({ config, out, year }) {
|
|
580
|
+
const settings = siteSettings({ config });
|
|
581
|
+
const navigation = readCachedNavigation(config);
|
|
582
|
+
const sqlTables = await prepareTreeSqlTables(config.paths.content, {
|
|
583
|
+
skipDirectories: config.skipDirectories,
|
|
584
|
+
config,
|
|
585
|
+
audience: "public",
|
|
586
|
+
});
|
|
587
|
+
const built = buildSite({ config, sqlTables, write: false });
|
|
588
|
+
if (gatesFailed(built.gates)) return { built, written: null, fonts: null };
|
|
589
|
+
const { site, pages, pageOf } = packageSite({ config, built, settings, navigation, year });
|
|
590
|
+
const written = writeSite({ site, pages, out });
|
|
591
|
+
const fonts = deliverSiteFonts(config, out);
|
|
592
|
+
return { built, written, fonts, site, pages, pageOf };
|
|
593
|
+
}
|
package/engine/site-root.mjs
CHANGED
|
@@ -15,20 +15,20 @@
|
|
|
15
15
|
* The files that belong to a deployment's **root** rather than to the rendered
|
|
16
16
|
* site.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
18
|
+
* `package-build site build` renders into `<out>/<package>/`, because the deployment carries the
|
|
19
19
|
* `/<package>/` prefix physically and the routing layer is a path-preserving
|
|
20
20
|
* pass-through. The directory that is *uploaded* is its parent, and Cloudflare
|
|
21
21
|
* Pages reads `_headers` and `_redirects` from there and nowhere else — a copy
|
|
22
|
-
* inside the prefix is published as a text file and never applied.
|
|
22
|
+
* inside the prefix is published as a text file and never applied. The render owns
|
|
23
23
|
* everything under the prefix; this owns what sits beside it.
|
|
24
24
|
*
|
|
25
25
|
* **One file, `_headers`.** The prefix root is the homepage — the site build
|
|
26
|
-
*
|
|
26
|
+
* renders it at the package root — so nothing redirects, and no
|
|
27
27
|
* `_redirects` is written. One left beside the site by an earlier build is
|
|
28
28
|
* removed rather than left to send every reader somewhere nothing publishes.
|
|
29
29
|
*
|
|
30
30
|
* **And one directory inside the prefix, `pagefind/`.** Every package site
|
|
31
|
-
* ends the same way —
|
|
31
|
+
* ends the same way — the render fills the output directory, this
|
|
32
32
|
* post-processes it there — so the search index is built in this step and
|
|
33
33
|
* every site gets one without each repository adding a dependency and a
|
|
34
34
|
* script. Pagefind indexes the rendered pages and writes `<package>/pagefind/`
|
|
@@ -120,15 +120,19 @@ export function noindexHeaders() {
|
|
|
120
120
|
* that names their version, so a year's lifetime never serves a stale file.
|
|
121
121
|
*
|
|
122
122
|
* @param {object} [options] - Options.
|
|
123
|
-
* @param {string} [options.pkg] - The content package,
|
|
123
|
+
* @param {string} [options.pkg] - The content package, whose site is served at
|
|
124
|
+
* `/<pkg>/` unless `base` says otherwise.
|
|
125
|
+
* @param {string} [options.base] - The path the site is served at, beginning
|
|
126
|
+
* and ending with `/`.
|
|
124
127
|
* @param {boolean} [options.fonts] - Whether the site carries the fonts.
|
|
125
128
|
* @returns {string} The file's contents.
|
|
126
129
|
*/
|
|
127
|
-
export function headers({ pkg, fonts = false } = {}) {
|
|
130
|
+
export function headers({ pkg, base, fonts = false } = {}) {
|
|
128
131
|
const lines = noindexHeaders();
|
|
129
|
-
|
|
132
|
+
const prefix = base ?? (pkg ? `/${pkg}/` : "");
|
|
133
|
+
if (fonts && prefix) {
|
|
130
134
|
lines.push(
|
|
131
|
-
|
|
135
|
+
`${prefix}fonts/heroiclands/*`,
|
|
132
136
|
" Cache-Control: public, max-age=31536000, immutable",
|
|
133
137
|
"",
|
|
134
138
|
);
|
|
@@ -245,7 +249,10 @@ export async function indexSite({ site, output }) {
|
|
|
245
249
|
*
|
|
246
250
|
* @param {object} options - Options.
|
|
247
251
|
* @param {string} options.pkg - The content package name, which is also the
|
|
248
|
-
* directory
|
|
252
|
+
* directory the site is rendered into unless `base` says otherwise.
|
|
253
|
+
* @param {string} [options.base] - The path the site is served at, beginning
|
|
254
|
+
* and ending with `/`; the site is rendered into that path under `out`, so
|
|
255
|
+
* a site served at `/` is rendered at `out` itself.
|
|
249
256
|
* @param {string} options.out - The directory that is deployed.
|
|
250
257
|
* @param {boolean} [options.search] - Whether to build the search index;
|
|
251
258
|
* `site.search` in the configuration. Default `true`.
|
|
@@ -259,18 +266,20 @@ export async function indexSite({ site, output }) {
|
|
|
259
266
|
* under the prefix; or, located at the rendered site, when the indexer
|
|
260
267
|
* fails.
|
|
261
268
|
*/
|
|
262
|
-
export async function writeSiteRoot({ pkg, out, search = true, indexer = indexSite }) {
|
|
269
|
+
export async function writeSiteRoot({ pkg, base, out, search = true, indexer = indexSite }) {
|
|
263
270
|
const root = path.resolve(out);
|
|
264
|
-
const
|
|
271
|
+
const prefix = base ?? `/${pkg}/`;
|
|
272
|
+
const rendered = path.join(root, ...prefix.split("/").filter(Boolean));
|
|
265
273
|
if (!fs.existsSync(path.join(rendered, "index.html"))) {
|
|
266
274
|
throw new Error(
|
|
267
|
-
`${out}
|
|
275
|
+
`${path.join(out, ...prefix.split("/").filter(Boolean))}/ holds no rendered site — ` +
|
|
276
|
+
"build the site before its root files",
|
|
268
277
|
);
|
|
269
278
|
}
|
|
270
279
|
|
|
271
280
|
const file = path.join(root, "_headers");
|
|
272
281
|
const fonts = fs.existsSync(path.join(rendered, "fonts", "heroiclands"));
|
|
273
|
-
fs.writeFileSync(file, headers({ pkg, fonts }));
|
|
282
|
+
fs.writeFileSync(file, headers({ pkg, base: prefix, fonts }));
|
|
274
283
|
fs.rmSync(path.join(root, "_redirects"), { force: true });
|
|
275
284
|
|
|
276
285
|
const dir = path.join(rendered, SEARCH_DIR);
|