@rathnasgala2/template 2.0.0 → 2.2.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/README.md +211 -137
- package/contracts/render-policy.jcs +1 -1
- package/contracts/theme-styling-contract.jcs +1 -1
- package/package.json +5 -6
- package/src/core/errors.js +13 -13
- package/src/core/index.js +130 -75
- package/src/core/internal/appearance/base-layer.js +180 -0
- package/src/core/internal/appearance/bootstrap-script.js +11 -11
- package/src/core/internal/appearance/contract.js +59 -44
- package/src/core/internal/appearance/controller-markup.js +7 -8
- package/src/core/internal/appearance/styling-contract.js +132 -29
- package/src/core/internal/canonical-jcs.js +0 -0
- package/src/core/internal/content-security.js +0 -0
- package/src/core/internal/eleventy-render.js +52 -35
- package/src/core/internal/feeds.js +15 -15
- package/src/core/internal/fs-walk.js +2 -2
- package/src/core/internal/media/avif-probe.js +4 -4
- package/src/core/internal/media/font-policy.js +2 -2
- package/src/core/internal/media/jpeg-codec.js +0 -0
- package/src/core/internal/media/limits.js +32 -34
- package/src/core/internal/media/pipeline.js +4 -4
- package/src/core/internal/media/png-codec.js +7 -7
- package/src/core/internal/media/resize.js +2 -2
- package/src/core/internal/media/sniff.js +4 -4
- package/src/core/internal/media/theme-svg-sanitizer.js +278 -0
- package/src/core/internal/media/webp-probe.js +3 -3
- package/src/core/internal/messages.js +8 -8
- package/src/core/internal/page-kinds.js +50 -51
- package/src/core/internal/render-policy-content.js +57 -21
- package/src/core/internal/route-labels.js +5 -6
- package/src/core/internal/route.js +58 -20
- package/src/core/internal/search-index.js +17 -16
- package/src/core/internal/semver-range.js +84 -0
- package/src/core/internal/seo.js +24 -24
- package/src/core/internal/sitemap.js +11 -12
- package/src/core/internal/skeleton.js +65 -46
- package/src/core/internal/source-inventory.js +4 -5
- package/src/core/internal/text-direction.js +1 -2
- package/src/core/internal/theme-assets.js +314 -68
- package/src/core/internal/uuid.js +4 -5
- package/src/core/manifest.js +0 -0
- package/types/index.d.ts +22 -8
|
@@ -1 +1 @@
|
|
|
1
|
-
{"attributes":[{"match":"exact-value","name":"data-gala-page-kind","role":"semantic","values":["archive","article","author","error","index","page","profile","series","tag"]},{"match":"exact-value","name":"data-gala-resolved-color-mode","role":"state","values":["dark","light"]},{"match":"exact-value","name":"data-gala-slot","role":"semantic","values":["account-intent","article-end","article-footer-ad","article-preamble","conversation","edition-selector","footer-auxiliary","footer-profile","header-actions","newsletter"]},{"match":"presence","name":"data-gala-publication-root","role":"root","values":[]}],"catalogDigest":"sha256:
|
|
1
|
+
{"attributes":[{"match":"exact-value","name":"data-gala-page-kind","role":"semantic","values":["archive","article","author","error","index","page","profile","series","tag"]},{"match":"exact-value","name":"data-gala-resolved-color-mode","role":"state","values":["dark","light"]},{"match":"exact-value","name":"data-gala-slot","role":"semantic","values":["account-intent","article-end","article-footer-ad","article-preamble","conversation","edition-selector","footer-auxiliary","footer-profile","header-actions","newsletter"]},{"match":"presence","name":"data-gala-publication-root","role":"root","values":[]}],"catalogDigest":"sha256:4aca7e9794bddfb2c3afd22f4efd078e7dcf69dde9d62ce52d28cec19aef8b91","classSelectors":["language-bash","language-css","language-diff","language-java","language-javascript","language-json","language-jsx","language-markdown","language-markup","language-python","language-sql","language-tsx","language-typescript","language-yaml","token"],"combinators":[" "," > "," + "," ~ "],"composition":{"compoundOrder":["type","id","class","attribute","pseudo-class","pseudo-element"],"functionalPseudoKeywordArguments":["even","odd"],"functionalSelectorArguments":"compound-only","maximumFunctionalDepth":1,"nthExpressionProfile":"gala-positive-an-plus-b-v2","rootScope":"first-compound-required"},"contractVersion":"2.1.0","functionalPseudos":["is","where","not","nth-child","nth-last-child"],"idSelectors":["gala-appearance-color-mode","main-content"],"orderedLayers":["gala-base","gala-tokens","gala-components","gala-utilities","gala-print"],"profile":"gala-template-styling-contract-v2","pseudoClasses":["active","disabled","focus-visible","hover","visited"],"pseudoElements":["after","before","marker","selection"],"publicThemeSlotHooks":[{"hookId":"code-block","selectorAtom":"pre"},{"hookId":"code-inline","selectorAtom":"code"},{"hookId":"code-language-bash","selectorAtom":".language-bash"},{"hookId":"code-language-css","selectorAtom":".language-css"},{"hookId":"code-language-diff","selectorAtom":".language-diff"},{"hookId":"code-language-java","selectorAtom":".language-java"},{"hookId":"code-language-javascript","selectorAtom":".language-javascript"},{"hookId":"code-language-json","selectorAtom":".language-json"},{"hookId":"code-language-jsx","selectorAtom":".language-jsx"},{"hookId":"code-language-markdown","selectorAtom":".language-markdown"},{"hookId":"code-language-markup","selectorAtom":".language-markup"},{"hookId":"code-language-python","selectorAtom":".language-python"},{"hookId":"code-language-sql","selectorAtom":".language-sql"},{"hookId":"code-language-tsx","selectorAtom":".language-tsx"},{"hookId":"code-language-typescript","selectorAtom":".language-typescript"},{"hookId":"code-language-yaml","selectorAtom":".language-yaml"},{"hookId":"code-token","selectorAtom":".token"},{"hookId":"content-article","selectorAtom":"article"},{"hookId":"control-appearance-select","selectorAtom":"#gala-appearance-color-mode"},{"hookId":"control-label","selectorAtom":"label"},{"hookId":"control-option","selectorAtom":"option"},{"hookId":"control-select","selectorAtom":"select"},{"hookId":"heading-1","selectorAtom":"h1"},{"hookId":"heading-2","selectorAtom":"h2"},{"hookId":"heading-3","selectorAtom":"h3"},{"hookId":"heading-4","selectorAtom":"h4"},{"hookId":"heading-5","selectorAtom":"h5"},{"hookId":"heading-6","selectorAtom":"h6"},{"hookId":"landmark-footer","selectorAtom":"footer"},{"hookId":"landmark-header","selectorAtom":"header"},{"hookId":"landmark-main","selectorAtom":"main"},{"hookId":"landmark-main-content","selectorAtom":"#main-content"},{"hookId":"landmark-navigation","selectorAtom":"nav"},{"hookId":"media-image","selectorAtom":"img"},{"hookId":"page-archive","selectorAtom":"[data-gala-page-kind=\"archive\"]"},{"hookId":"page-article","selectorAtom":"[data-gala-page-kind=\"article\"]"},{"hookId":"page-author","selectorAtom":"[data-gala-page-kind=\"author\"]"},{"hookId":"page-error","selectorAtom":"[data-gala-page-kind=\"error\"]"},{"hookId":"page-index","selectorAtom":"[data-gala-page-kind=\"index\"]"},{"hookId":"page-page","selectorAtom":"[data-gala-page-kind=\"page\"]"},{"hookId":"page-profile","selectorAtom":"[data-gala-page-kind=\"profile\"]"},{"hookId":"page-series","selectorAtom":"[data-gala-page-kind=\"series\"]"},{"hookId":"page-tag","selectorAtom":"[data-gala-page-kind=\"tag\"]"},{"hookId":"prose-blockquote","selectorAtom":"blockquote"},{"hookId":"prose-divider","selectorAtom":"hr"},{"hookId":"prose-emphasis","selectorAtom":"em"},{"hookId":"prose-link","selectorAtom":"a"},{"hookId":"prose-list-item","selectorAtom":"li"},{"hookId":"prose-list-ordered","selectorAtom":"ol"},{"hookId":"prose-list-unordered","selectorAtom":"ul"},{"hookId":"prose-paragraph","selectorAtom":"p"},{"hookId":"prose-span","selectorAtom":"span"},{"hookId":"prose-strong","selectorAtom":"strong"},{"hookId":"prose-time","selectorAtom":"time"},{"hookId":"slot-account-intent","selectorAtom":"[data-gala-slot=\"account-intent\"]"},{"hookId":"slot-article-end","selectorAtom":"[data-gala-slot=\"article-end\"]"},{"hookId":"slot-article-footer-ad","selectorAtom":"[data-gala-slot=\"article-footer-ad\"]"},{"hookId":"slot-article-preamble","selectorAtom":"[data-gala-slot=\"article-preamble\"]"},{"hookId":"slot-conversation","selectorAtom":"[data-gala-slot=\"conversation\"]"},{"hookId":"slot-edition-selector","selectorAtom":"[data-gala-slot=\"edition-selector\"]"},{"hookId":"slot-footer-auxiliary","selectorAtom":"[data-gala-slot=\"footer-auxiliary\"]"},{"hookId":"slot-footer-profile","selectorAtom":"[data-gala-slot=\"footer-profile\"]"},{"hookId":"slot-header-actions","selectorAtom":"[data-gala-slot=\"header-actions\"]"},{"hookId":"slot-newsletter","selectorAtom":"[data-gala-slot=\"newsletter\"]"}],"publicationRootSelector":"[data-gala-publication-root]","resolvedPaletteSelectors":{"dark":"[data-gala-publication-root][data-gala-resolved-color-mode=\"dark\"]","light":"[data-gala-publication-root][data-gala-resolved-color-mode=\"light\"]"},"stylesheetLayers":{"components":"gala-components","print":"gala-print","tokens":"gala-tokens","utilities":"gala-utilities"},"templatePackage":"@rathnasgala2/template","templateVersion":"2.1.0","typeSelectors":["a","article","blockquote","code","em","footer","h1","h2","h3","h4","h5","h6","header","hr","img","label","li","main","nav","ol","option","p","pre","select","span","strong","time","ul"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rathnasgala2/template",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Deterministic Eleventy-backed static publication renderer for Galascribe portable repositories.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -25,12 +25,11 @@
|
|
|
25
25
|
"contracts/",
|
|
26
26
|
"types/",
|
|
27
27
|
"LICENSE",
|
|
28
|
-
"NOTICE",
|
|
29
28
|
"README.md"
|
|
30
29
|
],
|
|
31
30
|
"engines": {
|
|
32
|
-
"node": "24.
|
|
33
|
-
"npm": "11.
|
|
31
|
+
"node": ">=24.0.0",
|
|
32
|
+
"npm": ">=11.0.0"
|
|
34
33
|
},
|
|
35
34
|
"devEngines": {
|
|
36
35
|
"runtime": {
|
|
@@ -61,12 +60,12 @@
|
|
|
61
60
|
"test": "node --test",
|
|
62
61
|
"licenses:generate": "node scripts/generate-license-inventory.mjs",
|
|
63
62
|
"licenses:check": "node scripts/generate-license-inventory.mjs --check",
|
|
64
|
-
"sbom:generate": "cyclonedx-npm --output-file sbom.cdx.json --output-format JSON --spec-version 1.6 && node scripts/apply-sbom-normalization.mjs",
|
|
63
|
+
"sbom:generate": "cyclonedx-npm --package-lock-only --output-file sbom.cdx.json --output-format JSON --spec-version 1.6 && node scripts/apply-sbom-normalization.mjs",
|
|
65
64
|
"sbom:check": "node scripts/check-sbom-current.mjs",
|
|
66
65
|
"workflows:check": "node scripts/check-workflow-pins.mjs",
|
|
67
66
|
"workflows:drift": "node scripts/check-workflow-drift.mjs",
|
|
68
67
|
"schema-pin:check": "node scripts/check-no-local-schema-pin.mjs",
|
|
69
|
-
"audit": "npm
|
|
68
|
+
"audit": "node scripts/npm-audit.mjs",
|
|
70
69
|
"verify": "npm run format:check && npm run lint && npm run typecheck && npm run architecture && npm run duplication && npm run contracts:check && npm run schema-pin:check && npm test && npm run licenses:check && npm run sbom:check && npm run workflows:check && npm run audit"
|
|
71
70
|
},
|
|
72
71
|
"dependencies": {
|
package/src/core/errors.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* bare `Error`, so a caller (eventually `publish-kernel`) can distinguish
|
|
5
5
|
* "the supplied build input is not schema-valid" from "the caller's own
|
|
6
6
|
* adapter options are incomplete or unsafe" without string-matching a
|
|
7
|
-
* message
|
|
7
|
+
* message: this renderer fails closed on invalid input.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -43,14 +43,14 @@ export class RenderOptionsError extends Error {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
* The media pipeline
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
46
|
+
* The media pipeline rejected an image or font reference: an unrecognized or
|
|
47
|
+
* malformed format, a decompression or resource ceiling crossing, an
|
|
48
|
+
* unconditionally-rejected SVG image input, a font that is not bounded
|
|
49
|
+
* WOFF2, or a source file whose bytes do not match its declared
|
|
50
50
|
* `build-input` digest. Every one of these fails closed — before any
|
|
51
51
|
* derivative is written and before rendering completes — never as a warning
|
|
52
|
-
* or silent fallback
|
|
53
|
-
*
|
|
52
|
+
* or silent fallback: decompression or resource excess is always rejected,
|
|
53
|
+
* and SVG is never processed as author media.
|
|
54
54
|
*/
|
|
55
55
|
export class MediaPipelineError extends Error {
|
|
56
56
|
/**
|
|
@@ -78,8 +78,8 @@ export class MediaPipelineError extends Error {
|
|
|
78
78
|
* byte-equal the published render-policy identity, a non-empty module or
|
|
79
79
|
* placement value, or sanitized output that still matched a forbidden
|
|
80
80
|
* pattern (an adapter defect, never expected of admitted content). Rendering
|
|
81
|
-
* never completes for the affected build
|
|
82
|
-
* behaviour on any content the policy rejects
|
|
81
|
+
* never completes for the affected build: this renderer has fail-closed
|
|
82
|
+
* behaviour on any content the policy rejects.
|
|
83
83
|
*/
|
|
84
84
|
export class RenderPolicyViolationError extends Error {
|
|
85
85
|
/**
|
|
@@ -95,14 +95,14 @@ export class RenderPolicyViolationError extends Error {
|
|
|
95
95
|
* The selected theme package's `theme.json` (or its declared file set) did
|
|
96
96
|
* not conform to the closed shape `internal/theme-assets.js` needs to copy
|
|
97
97
|
* its stylesheets/passive assets and render its `<link>` elements: an
|
|
98
|
-
* invalid `stylesheets` list shape, a
|
|
99
|
-
*
|
|
100
|
-
*
|
|
98
|
+
* invalid `stylesheets` list shape, a schema/contract-integrity violation
|
|
99
|
+
* (which also covers an inadmissible `cssLayers` projection — see
|
|
100
|
+
* `urn:gala:schema:theme-contract:2.0.0`), or a missing declared file.
|
|
101
101
|
*/
|
|
102
102
|
export class ThemeAssetError extends Error {
|
|
103
103
|
/**
|
|
104
104
|
* @param {string} reasonCode a stable, machine-checkable reason code (for
|
|
105
|
-
* example `THEME_STYLESHEETS_INVALID`, `
|
|
105
|
+
* example `THEME_STYLESHEETS_INVALID`, `THEME_CONTRACT_SCHEMA_INVALID`)
|
|
106
106
|
* @param {string} message a human-readable description
|
|
107
107
|
*/
|
|
108
108
|
constructor(reasonCode, message) {
|
package/src/core/index.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@rathnasgala2/template` public entry point.
|
|
3
3
|
*
|
|
4
|
-
* This is
|
|
5
|
-
* media pipeline: it consumes one validated
|
|
4
|
+
* This is the Eleventy-confined renderer adapter: it consumes one validated
|
|
6
5
|
* `urn:gala:schema:build-input:2.0.0` instance and emits a candidate output
|
|
7
|
-
* directory plus one `urn:gala:schema:artifact-manifest:2.0.0` instance
|
|
8
|
-
* (brief S2 section 3).
|
|
6
|
+
* directory plus one `urn:gala:schema:artifact-manifest:2.0.0` instance.
|
|
9
7
|
*
|
|
10
8
|
* `renderPublication` is the only documented *rendering* entry point. It
|
|
11
9
|
* fails closed: an invalid `build-input` is rejected with
|
|
@@ -14,7 +12,7 @@
|
|
|
14
12
|
* Eleventy ever runs, and a rejected image or font is rejected with
|
|
15
13
|
* {@link MediaPipelineError} before the manifest is assembled.
|
|
16
14
|
*
|
|
17
|
-
* `normalizeAuthoredMarkdown` (re-exported here from
|
|
15
|
+
* `normalizeAuthoredMarkdown` (re-exported here from
|
|
18
16
|
* `internal/content-security.js`) is this package's second public entry
|
|
19
17
|
* point: the upstream normalization step a `build-input:2.0.0` producer
|
|
20
18
|
* (`publish-kernel`/`publish-action`) calls once per authored Markdown
|
|
@@ -25,38 +23,36 @@
|
|
|
25
23
|
* reaches this renderer, and how `renderPublication` verifies that instead
|
|
26
24
|
* of re-deriving it.
|
|
27
25
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
26
|
+
* `options.sourceDirectory` is the caller-mounted, read-only repository
|
|
27
|
+
* source tree `build-input`'s `resolvedFile` references point into.
|
|
28
|
+
* `processMedia` runs after the Eleventy route listing is already computed,
|
|
29
|
+
* so generated derivative files under `assets/media/` are never mistaken for
|
|
30
|
+
* an HTML route.
|
|
33
31
|
*
|
|
34
|
-
* Output-security (markdown/sanitizer/highlighter/CSP
|
|
35
|
-
* pipeline
|
|
36
|
-
*
|
|
37
|
-
*
|
|
32
|
+
* Output-security (markdown/sanitizer/highlighter/CSP) and the media
|
|
33
|
+
* pipeline are layered on the same adapter, alongside the core semantic
|
|
34
|
+
* skeleton and navigation renderer (`internal/skeleton.js`) and every
|
|
35
|
+
* generated page kind (`internal/page-kinds.js`): publication profile,
|
|
38
36
|
* author, article/page, index, tag, series and archive pages, each composed
|
|
39
|
-
* with one shared header/primary-navigation/footer chrome and, where
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
37
|
+
* with one shared header/primary-navigation/footer chrome and, where
|
|
38
|
+
* required, a breadcrumb trail. This adapter also emits every authored
|
|
39
|
+
* static redirect and the feeds/search/sitemap outputs, shaped so that later
|
|
40
|
+
* work can extend the page-body pipeline without touching the confinement,
|
|
41
|
+
* route-projection or manifest-assembly mechanics here.
|
|
44
42
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* `
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* (`contract.js`) — the contract S2-T12's published theme styling catalog
|
|
43
|
+
* The Light/Dark/System appearance controller (`internal/appearance/`) adds:
|
|
44
|
+
* the server-rendered control in the header's `header-actions` slot
|
|
45
|
+
* (`controller-markup.js`), the one deterministic pre-paint bootstrap script
|
|
46
|
+
* this renderer ever emits, written into the candidate output directory and
|
|
47
|
+
* entered into the manifest as an ordinary `manifestAsset` row exactly like a
|
|
48
|
+
* media derivative (`bootstrap-script.js`), and the fixed `data-` attribute
|
|
49
|
+
* names, mode values and storage key every one of those pieces shares
|
|
50
|
+
* (`contract.js`) — the same contract the published theme styling catalog
|
|
54
51
|
* targets.
|
|
55
52
|
*
|
|
56
53
|
* `src/core/` is the only admitted source root in this repository. No
|
|
57
54
|
* `src/modules/` tree, module import edge, registration stub, module
|
|
58
55
|
* configuration, module output or module runtime is ever added beside it
|
|
59
|
-
* (brief S2 section 3; DEC-097 sections 2 and 5).
|
|
60
56
|
*/
|
|
61
57
|
|
|
62
58
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
@@ -70,6 +66,11 @@ import {
|
|
|
70
66
|
appearanceBootstrapScriptHref,
|
|
71
67
|
} from './internal/appearance/contract.js';
|
|
72
68
|
import { APPEARANCE_BOOTSTRAP_SCRIPT_SOURCE } from './internal/appearance/bootstrap-script.js';
|
|
69
|
+
import {
|
|
70
|
+
GALA_BASE_STYLESHEET_MEDIA_TYPE,
|
|
71
|
+
GALA_BASE_STYLESHEET_PATH,
|
|
72
|
+
GALA_BASE_STYLESHEET_SOURCE,
|
|
73
|
+
} from './internal/appearance/base-layer.js';
|
|
73
74
|
import { renderAppearanceControl } from './internal/appearance/controller-markup.js';
|
|
74
75
|
import {
|
|
75
76
|
ArtifactManifestValidationError,
|
|
@@ -99,7 +100,9 @@ import {
|
|
|
99
100
|
} from './internal/page-kinds.js';
|
|
100
101
|
import { assertProvenance } from './internal/provenance.js';
|
|
101
102
|
import {
|
|
103
|
+
derivePublicBasePath,
|
|
102
104
|
errorDocumentPath,
|
|
105
|
+
joinPublicRoute,
|
|
103
106
|
joinBasePathAndRoute,
|
|
104
107
|
projectFixedAssetPath,
|
|
105
108
|
projectRouteToFilePath,
|
|
@@ -151,7 +154,7 @@ export const TEMPLATE_PACKAGE_NAME = '@rathnasgala2/template';
|
|
|
151
154
|
*
|
|
152
155
|
* @type {string}
|
|
153
156
|
*/
|
|
154
|
-
export const TEMPLATE_PACKAGE_VERSION = '2.
|
|
157
|
+
export const TEMPLATE_PACKAGE_VERSION = '2.1.0';
|
|
155
158
|
|
|
156
159
|
/**
|
|
157
160
|
* Validate the caller-supplied render options, independent of build-input
|
|
@@ -180,10 +183,9 @@ function assertOptions(options) {
|
|
|
180
183
|
throw new RenderOptionsError(`options.${key} must be an absolute path`);
|
|
181
184
|
}
|
|
182
185
|
}
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
// so this addition is purely additive.
|
|
186
|
+
// An optional caller-mounted, read-only extracted theme package directory
|
|
187
|
+
// (`internal/theme-assets.js`). Absent entirely by default, so this
|
|
188
|
+
// addition is purely additive.
|
|
187
189
|
if (options.themeDirectory !== undefined) {
|
|
188
190
|
if (
|
|
189
191
|
typeof options.themeDirectory !== 'string' ||
|
|
@@ -228,9 +230,9 @@ function assertOptions(options) {
|
|
|
228
230
|
* render-policy identity, and that no module or placement value leaked
|
|
229
231
|
* through (belt-and-suspenders on top of the `build-input:2.0.0` schema's
|
|
230
232
|
* own `modules: {}` / `placements: []` closure). Fails closed before any
|
|
231
|
-
* directory is created or Eleventy ever runs
|
|
232
|
-
*
|
|
233
|
-
*
|
|
233
|
+
* directory is created or Eleventy ever runs: an absent policy file, wrong
|
|
234
|
+
* path/name/version/digest, mixed identity between records, or body
|
|
235
|
+
* produced under a different policy rejects before build.
|
|
234
236
|
*
|
|
235
237
|
* @param {import('../../types/index.d.ts').NormalizedBuildInput} validatedInput
|
|
236
238
|
* the schema-validated build input
|
|
@@ -320,9 +322,16 @@ export async function renderPublication(buildInput, options) {
|
|
|
320
322
|
// `page.language` below); only the *chrome strings surrounding it* are
|
|
321
323
|
// rendered in one consistent language, matching a reader's expectation
|
|
322
324
|
// that navigation/footer text does not flip language from page to page
|
|
323
|
-
// (
|
|
325
|
+
// (see `internal/messages.js`).
|
|
324
326
|
const siteMessages = getMessages(validatedInput.publication.defaultLanguage);
|
|
325
|
-
|
|
327
|
+
// Every emitted URL carries the `baseUrl` path (for example `/g9/` on a
|
|
328
|
+
// GitHub project site); output file paths keep using `basePath` alone.
|
|
329
|
+
const publicBasePath = derivePublicBasePath(
|
|
330
|
+
validatedInput.baseUrl,
|
|
331
|
+
validatedInput.basePath,
|
|
332
|
+
);
|
|
333
|
+
const urlPrefix = derivePublicBasePath(validatedInput.baseUrl, '/');
|
|
334
|
+
const homeRoute = joinPublicRoute(publicBasePath, '/');
|
|
326
335
|
|
|
327
336
|
const appearanceControlHtml = renderAppearanceControl({
|
|
328
337
|
messages: siteMessages,
|
|
@@ -336,6 +345,7 @@ export async function renderPublication(buildInput, options) {
|
|
|
336
345
|
const footerNavHtml = renderFooterNavigation({
|
|
337
346
|
items: validatedInput.navigation.footerItems,
|
|
338
347
|
messages: siteMessages,
|
|
348
|
+
urlPrefix,
|
|
339
349
|
});
|
|
340
350
|
const footerCard = validatedInput.publication.footerCard;
|
|
341
351
|
// Already verified render-policy-conformant HTML by
|
|
@@ -350,31 +360,37 @@ export async function renderPublication(buildInput, options) {
|
|
|
350
360
|
messages: siteMessages,
|
|
351
361
|
});
|
|
352
362
|
|
|
353
|
-
// Run before page assembly, not after
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
// Eleventy-rendered HTML route.
|
|
363
|
+
// Run before page assembly, not after: resolving each page's own
|
|
364
|
+
// `og:image`/`twitter:image` needs the media pipeline's own finished
|
|
365
|
+
// `assets` list (only it knows which derivative file extension a given
|
|
366
|
+
// source image's `sourceDigest` produced — see `internal/seo.js`). This
|
|
367
|
+
// only moves the in-memory computation earlier; `mediaFiles`' actual
|
|
368
|
+
// bytes are still written to `outputDirectory` only after the route
|
|
369
|
+
// listing below, so `outputDirectory/assets/media/` still can never be
|
|
370
|
+
// mistaken for an Eleventy-rendered HTML route.
|
|
362
371
|
const { assets: mediaAssets, files: mediaFiles } = await processMedia(
|
|
363
372
|
validatedInput,
|
|
364
373
|
{ sourceDirectory: path.resolve(options.sourceDirectory) },
|
|
365
374
|
);
|
|
366
375
|
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
//
|
|
376
|
+
// The site-wide feeds, resolved once so every page's feed-discovery
|
|
377
|
+
// `<link>` tags and the feed documents themselves agree on the exact same
|
|
378
|
+
// self URLs.
|
|
370
379
|
const feeds = buildFeeds(validatedInput);
|
|
371
380
|
const siteName = validatedInput.publication.title;
|
|
372
|
-
const appearanceScriptHrefValue =
|
|
373
|
-
|
|
381
|
+
const appearanceScriptHrefValue =
|
|
382
|
+
appearanceBootstrapScriptHref(publicBasePath);
|
|
383
|
+
// TPL-H3/TPL-M7: the template-owned gala-base stylesheet's own basePath-
|
|
384
|
+
// joined href, computed the same way as the appearance script href above
|
|
385
|
+
// so it can never drift from the physical file/manifest path written
|
|
386
|
+
// below.
|
|
387
|
+
const baseStylesheetHrefValue = joinBasePathAndRoute(
|
|
388
|
+
publicBasePath,
|
|
389
|
+
`/${GALA_BASE_STYLESHEET_PATH}`,
|
|
374
390
|
);
|
|
375
391
|
|
|
376
|
-
//
|
|
377
|
-
//
|
|
392
|
+
// The selected theme package's copied stylesheets/passive assets and the
|
|
393
|
+
// ordered `<link>` markup every generated page's `<head>` inserts.
|
|
378
394
|
// Run before page assembly (same reasoning as the media pipeline above):
|
|
379
395
|
// every page's `data.themeStylesheetLinksHtml` needs the same one
|
|
380
396
|
// already-rendered fragment.
|
|
@@ -382,15 +398,16 @@ export async function renderPublication(buildInput, options) {
|
|
|
382
398
|
? await loadThemeAssets({
|
|
383
399
|
themeDirectory: path.resolve(options.themeDirectory),
|
|
384
400
|
basePath: validatedInput.basePath,
|
|
401
|
+
publicBasePath,
|
|
385
402
|
})
|
|
386
403
|
: undefined;
|
|
387
404
|
const themeStylesheetLinksHtml = themeAssetsResult
|
|
388
405
|
? themeAssetsResult.linkTagsHtml
|
|
389
|
-
: defaultThemeStylesheetLinksHtml(
|
|
406
|
+
: defaultThemeStylesheetLinksHtml(publicBasePath);
|
|
390
407
|
|
|
391
408
|
/** @type {{virtualPath: string, content: string, permalink: string, layout?: string, data?: Record<string, unknown>}[]} */
|
|
392
409
|
const contentPages = [];
|
|
393
|
-
/** @type {{joinedSource: string, joinedTarget: string, filePath: string}[]} */
|
|
410
|
+
/** @type {{joinedSource: string, joinedTarget: string, publicTarget: string, filePath: string}[]} */
|
|
394
411
|
const redirectProjections = [];
|
|
395
412
|
|
|
396
413
|
const generatedPages = buildGeneratedPages(validatedInput);
|
|
@@ -403,6 +420,7 @@ export async function renderPublication(buildInput, options) {
|
|
|
403
420
|
items: validatedInput.navigation.items,
|
|
404
421
|
currentRoute: joinedRoute,
|
|
405
422
|
messages: siteMessages,
|
|
423
|
+
urlPrefix,
|
|
406
424
|
});
|
|
407
425
|
const bodyHtml = renderPageBody({
|
|
408
426
|
messages: siteMessages,
|
|
@@ -416,7 +434,7 @@ export async function renderPublication(buildInput, options) {
|
|
|
416
434
|
mediaAssets,
|
|
417
435
|
reference: page.socialImageRef,
|
|
418
436
|
baseUrl: validatedInput.baseUrl,
|
|
419
|
-
basePath:
|
|
437
|
+
basePath: publicBasePath,
|
|
420
438
|
});
|
|
421
439
|
contentPages.push({
|
|
422
440
|
virtualPath: `pages/${page.kind}-${index}.html`,
|
|
@@ -430,7 +448,10 @@ export async function renderPublication(buildInput, options) {
|
|
|
430
448
|
pageKind: page.kind,
|
|
431
449
|
description: page.description,
|
|
432
450
|
robotsContent: page.robotsContent,
|
|
433
|
-
canonicalUrl: new URL(
|
|
451
|
+
canonicalUrl: new URL(
|
|
452
|
+
joinPublicRoute(publicBasePath, page.route),
|
|
453
|
+
validatedInput.baseUrl,
|
|
454
|
+
).toString(),
|
|
434
455
|
siteName,
|
|
435
456
|
ogType: page.ogType ?? 'website',
|
|
436
457
|
ogImage,
|
|
@@ -438,6 +459,7 @@ export async function renderPublication(buildInput, options) {
|
|
|
438
459
|
atomFeedUrl: feeds.atomSelfUrl,
|
|
439
460
|
rssFeedUrl: feeds.rssSelfUrl,
|
|
440
461
|
appearanceScriptHref: appearanceScriptHrefValue,
|
|
462
|
+
baseStylesheetHref: baseStylesheetHrefValue,
|
|
441
463
|
themeStylesheetLinksHtml,
|
|
442
464
|
},
|
|
443
465
|
});
|
|
@@ -456,6 +478,10 @@ export async function renderPublication(buildInput, options) {
|
|
|
456
478
|
redirectProjections.push({
|
|
457
479
|
joinedSource,
|
|
458
480
|
joinedTarget: joined,
|
|
481
|
+
publicTarget: joinPublicRoute(
|
|
482
|
+
publicBasePath,
|
|
483
|
+
contentRoute(record.frontmatter),
|
|
484
|
+
),
|
|
459
485
|
filePath: projectRouteToFilePath(joinedSource, routeProfile),
|
|
460
486
|
});
|
|
461
487
|
}
|
|
@@ -463,7 +489,7 @@ export async function renderPublication(buildInput, options) {
|
|
|
463
489
|
|
|
464
490
|
const redirectPages = redirectProjections.map((projection, index) => ({
|
|
465
491
|
virtualPath: `redirects/${index}.html`,
|
|
466
|
-
content: renderRedirectDocument(projection.
|
|
492
|
+
content: renderRedirectDocument(projection.publicTarget),
|
|
467
493
|
permalink: projection.filePath,
|
|
468
494
|
}));
|
|
469
495
|
|
|
@@ -473,14 +499,15 @@ export async function renderPublication(buildInput, options) {
|
|
|
473
499
|
);
|
|
474
500
|
}
|
|
475
501
|
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
// guard
|
|
479
|
-
//
|
|
502
|
+
// The one ordinary generated `404.html` error page, using the `error`
|
|
503
|
+
// page kind's own pure body function. Pushed after the guard above so the
|
|
504
|
+
// guard's own "is there anything authored to render" check is unaffected
|
|
505
|
+
// by this always-present synthetic page.
|
|
480
506
|
const errorNavHtml = renderPrimaryNavigation({
|
|
481
507
|
items: validatedInput.navigation.items,
|
|
482
508
|
currentRoute: undefined,
|
|
483
509
|
messages: siteMessages,
|
|
510
|
+
urlPrefix,
|
|
484
511
|
});
|
|
485
512
|
const errorBodyHtml = renderPageBody({
|
|
486
513
|
messages: siteMessages,
|
|
@@ -509,11 +536,12 @@ export async function renderPublication(buildInput, options) {
|
|
|
509
536
|
atomFeedUrl: feeds.atomSelfUrl,
|
|
510
537
|
rssFeedUrl: feeds.rssSelfUrl,
|
|
511
538
|
appearanceScriptHref: appearanceScriptHrefValue,
|
|
539
|
+
baseStylesheetHref: baseStylesheetHrefValue,
|
|
512
540
|
themeStylesheetLinksHtml,
|
|
513
541
|
},
|
|
514
542
|
});
|
|
515
543
|
|
|
516
|
-
//
|
|
544
|
+
// Feeds, sitemap and the static search index. Each is a fixed,
|
|
517
545
|
// literal-extension file — never route-normalized like an ordinary
|
|
518
546
|
// content route (`internal/route.js#projectFixedAssetPath`) — and each
|
|
519
547
|
// bypasses the shared HTML skeleton layout entirely (no `layout` field,
|
|
@@ -537,7 +565,7 @@ export async function renderPublication(buildInput, options) {
|
|
|
537
565
|
);
|
|
538
566
|
const sitemapXml = buildSitemap({
|
|
539
567
|
generatedPages,
|
|
540
|
-
basePath:
|
|
568
|
+
basePath: publicBasePath,
|
|
541
569
|
baseUrl: validatedInput.baseUrl,
|
|
542
570
|
});
|
|
543
571
|
const searchIndexJson = buildSearchIndex(validatedInput);
|
|
@@ -570,10 +598,9 @@ export async function renderPublication(buildInput, options) {
|
|
|
570
598
|
outputDirectory,
|
|
571
599
|
});
|
|
572
600
|
|
|
573
|
-
//
|
|
601
|
+
// The exact `routeClass`/`mediaType` this renderer's own generated
|
|
574
602
|
// non-HTML routes carry, keyed by their fixed output path. Every other
|
|
575
|
-
// route defaults to `'html'`/`text/html; charset=utf-8
|
|
576
|
-
// S2-T03.
|
|
603
|
+
// route defaults to `'html'`/`text/html; charset=utf-8`.
|
|
577
604
|
/** @type {Map<string, {routeClass: import('../../types/index.d.ts').ManifestRouteEntry['routeClass'], mediaType: string}>} */
|
|
578
605
|
const specialRoutes = new Map([
|
|
579
606
|
[
|
|
@@ -663,8 +690,8 @@ export async function renderPublication(buildInput, options) {
|
|
|
663
690
|
await writeFile(destination, file.bytes);
|
|
664
691
|
}
|
|
665
692
|
|
|
666
|
-
//
|
|
667
|
-
//
|
|
693
|
+
// The selected theme package's stylesheet/passive-asset bytes, written
|
|
694
|
+
// for the same reason and at the same point as the media pipeline
|
|
668
695
|
// output immediately above (`themeAssetsResult` is `undefined` when no
|
|
669
696
|
// `options.themeDirectory` was supplied, in which case there is nothing to
|
|
670
697
|
// write — the fallback `defaultThemeStylesheetLinksHtml` link above points
|
|
@@ -677,10 +704,10 @@ export async function renderPublication(buildInput, options) {
|
|
|
677
704
|
}
|
|
678
705
|
}
|
|
679
706
|
|
|
680
|
-
//
|
|
681
|
-
//
|
|
682
|
-
//
|
|
683
|
-
//
|
|
707
|
+
// The one deterministic pre-paint appearance-controller bootstrap script:
|
|
708
|
+
// the appearance controller is the only browser bootstrap this renderer
|
|
709
|
+
// ever emits. Written after the routes listing above for the same reason
|
|
710
|
+
// as the media pipeline output immediately above: it must never be
|
|
684
711
|
// mistaken for an Eleventy-rendered HTML route. Its bytes are a pure
|
|
685
712
|
// function of `internal/appearance/contract.js`'s own fixed constants, so
|
|
686
713
|
// this asset row is identical on every build regardless of build input.
|
|
@@ -714,9 +741,36 @@ export async function renderPublication(buildInput, options) {
|
|
|
714
741
|
immutable: false,
|
|
715
742
|
};
|
|
716
743
|
|
|
744
|
+
// TPL-H3/TPL-M7: the template-owned gala-base stylesheet, written and
|
|
745
|
+
// manifested for the same reason and at the same point as the appearance
|
|
746
|
+
// bootstrap script immediately above. Always emitted, independent of
|
|
747
|
+
// whether a theme was selected (`baseStylesheetHrefValue` above is
|
|
748
|
+
// likewise unconditional), because it is what makes contract-mandated
|
|
749
|
+
// chrome presentable even with no theme at all.
|
|
750
|
+
const baseStylesheetBytes = Buffer.from(GALA_BASE_STYLESHEET_SOURCE, 'utf8');
|
|
751
|
+
const joinedBaseStylesheetPath = projectFixedAssetPath(
|
|
752
|
+
validatedInput.basePath,
|
|
753
|
+
`/${GALA_BASE_STYLESHEET_PATH}`,
|
|
754
|
+
);
|
|
755
|
+
const baseStylesheetDestination = path.join(
|
|
756
|
+
outputDirectory,
|
|
757
|
+
joinedBaseStylesheetPath,
|
|
758
|
+
);
|
|
759
|
+
await mkdir(path.dirname(baseStylesheetDestination), { recursive: true });
|
|
760
|
+
await writeFile(baseStylesheetDestination, baseStylesheetBytes);
|
|
761
|
+
/** @type {import('../../types/index.d.ts').ManifestAssetEntry} */
|
|
762
|
+
const baseStylesheetAsset = {
|
|
763
|
+
path: joinedBaseStylesheetPath,
|
|
764
|
+
mediaType: GALA_BASE_STYLESHEET_MEDIA_TYPE,
|
|
765
|
+
byteLength: String(baseStylesheetBytes.byteLength),
|
|
766
|
+
sha256: digestBytes(baseStylesheetBytes),
|
|
767
|
+
immutable: false,
|
|
768
|
+
};
|
|
769
|
+
|
|
717
770
|
const assets = [
|
|
718
771
|
...joinedMediaAssets,
|
|
719
772
|
appearanceScriptAsset,
|
|
773
|
+
baseStylesheetAsset,
|
|
720
774
|
...(themeAssetsResult ? themeAssetsResult.assets : []),
|
|
721
775
|
].sort((a, b) => compareUtf8Bytes(a.path, b.path));
|
|
722
776
|
|
|
@@ -754,5 +808,6 @@ export {
|
|
|
754
808
|
|
|
755
809
|
export {
|
|
756
810
|
computeBodyDigest,
|
|
811
|
+
computeRenderPolicyIdentity,
|
|
757
812
|
normalizeAuthoredMarkdown,
|
|
758
813
|
} from './internal/content-security.js';
|