@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.
Files changed (42) hide show
  1. package/README.md +211 -137
  2. package/contracts/render-policy.jcs +1 -1
  3. package/contracts/theme-styling-contract.jcs +1 -1
  4. package/package.json +5 -6
  5. package/src/core/errors.js +13 -13
  6. package/src/core/index.js +130 -75
  7. package/src/core/internal/appearance/base-layer.js +180 -0
  8. package/src/core/internal/appearance/bootstrap-script.js +11 -11
  9. package/src/core/internal/appearance/contract.js +59 -44
  10. package/src/core/internal/appearance/controller-markup.js +7 -8
  11. package/src/core/internal/appearance/styling-contract.js +132 -29
  12. package/src/core/internal/canonical-jcs.js +0 -0
  13. package/src/core/internal/content-security.js +0 -0
  14. package/src/core/internal/eleventy-render.js +52 -35
  15. package/src/core/internal/feeds.js +15 -15
  16. package/src/core/internal/fs-walk.js +2 -2
  17. package/src/core/internal/media/avif-probe.js +4 -4
  18. package/src/core/internal/media/font-policy.js +2 -2
  19. package/src/core/internal/media/jpeg-codec.js +0 -0
  20. package/src/core/internal/media/limits.js +32 -34
  21. package/src/core/internal/media/pipeline.js +4 -4
  22. package/src/core/internal/media/png-codec.js +7 -7
  23. package/src/core/internal/media/resize.js +2 -2
  24. package/src/core/internal/media/sniff.js +4 -4
  25. package/src/core/internal/media/theme-svg-sanitizer.js +278 -0
  26. package/src/core/internal/media/webp-probe.js +3 -3
  27. package/src/core/internal/messages.js +8 -8
  28. package/src/core/internal/page-kinds.js +50 -51
  29. package/src/core/internal/render-policy-content.js +57 -21
  30. package/src/core/internal/route-labels.js +5 -6
  31. package/src/core/internal/route.js +58 -20
  32. package/src/core/internal/search-index.js +17 -16
  33. package/src/core/internal/semver-range.js +84 -0
  34. package/src/core/internal/seo.js +24 -24
  35. package/src/core/internal/sitemap.js +11 -12
  36. package/src/core/internal/skeleton.js +65 -46
  37. package/src/core/internal/source-inventory.js +4 -5
  38. package/src/core/internal/text-direction.js +1 -2
  39. package/src/core/internal/theme-assets.js +314 -68
  40. package/src/core/internal/uuid.js +4 -5
  41. package/src/core/manifest.js +0 -0
  42. 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:46b43a6d1775f88bf2ded331512bd0cf27d232e256e40a89bda773e50897877f","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"],"functionalSelectorArguments":"compound-only","maximumFunctionalDepth":1,"nthExpressionProfile":"gala-positive-an-plus-b-v2","rootScope":"first-compound-required"},"contractVersion":"2.0.0","functionalPseudos":["is","where","not","nth-child","nth-last-child"],"idSelectors":["gala-appearance-color-mode","main-content"],"orderedLayers":["gala-tokens","gala-components","gala-utilities","gala-print"],"profile":"gala-template-styling-contract-v2","pseudoClasses":[],"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.0.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"]}
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.0.0",
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.18.0",
33
- "npm": "11.16.0"
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 audit --audit-level=high",
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": {
@@ -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 (brief S2 section 3: "fails closed on invalid input").
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 (S2-T05) rejected an image or font reference: an
47
- * unrecognized or malformed format, a decompression or resource ceiling
48
- * crossing, an unconditionally-rejected SVG image input, a font that is not
49
- * bounded WOFF2, or a source file whose bytes do not match its declared
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 (brief S2 section 3: "Reject decompression or resource
53
- * excess"; "SVG is never processed as author media").
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 (brief S2 section 3: "fail-closed
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 `cssLayers` projection that does not
99
- * byte-equal the corresponding ordered layer names, or a missing declared
100
- * file (task packet S2-T12).
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`, `THEME_CSS_LAYERS_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 S2-T03's Eleventy-confined renderer adapter, extended by S2-T05's
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 S2-T04's
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
- * S2-T05 adds `options.sourceDirectory` (the caller-mounted, read-only
29
- * repository source tree `build-input`'s `resolvedFile` references point
30
- * into) and runs `processMedia` after the Eleventy route listing is already
31
- * computed, so generated derivative files under `assets/media/` are never
32
- * mistaken for an HTML route.
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; S2-T04) and the media
35
- * pipeline (S2-T05) are layered on the same adapter. S2-T06 adds the core
36
- * semantic skeleton and navigation renderer (`internal/skeleton.js`) and
37
- * every generated page kind (`internal/page-kinds.js`): publication profile,
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 the
40
- * brief requires it, a breadcrumb trail. Feeds/search/sitemap remain
41
- * S2-T08. This adapter also emits every authored static redirect, shaped so
42
- * that later task extends the page-body pipeline without touching the
43
- * confinement, route-projection or manifest-assembly mechanics here.
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
- * S2-T07 adds the Light/Dark/System appearance controller
46
- * (`internal/appearance/`): the server-rendered control in the header's
47
- * `header-actions` slot (`controller-markup.js`), the one deterministic
48
- * pre-paint bootstrap script this renderer ever emits, written into the
49
- * candidate output directory and entered into the manifest as an ordinary
50
- * `manifestAsset` row exactly like an S2-T05 media derivative
51
- * (`bootstrap-script.js`), and the fixed `data-` attribute names, mode
52
- * values and storage key every one of those pieces shares
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.0.0';
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
- // S2-T12: an optional caller-mounted, read-only extracted theme package
184
- // directory (`internal/theme-assets.js`). Absent entirely by default
185
- // (every pre-S2-T12 caller, including this repository's own test suite)
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 (DEC-097 section 5: "An absent
232
- * policy file, wrong path/name/version/digest, mixed identity between
233
- * records or body produced under a different policy rejects before build").
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
- // (task packet S2-T06 scoped decision; see `internal/messages.js`).
325
+ // (see `internal/messages.js`).
324
326
  const siteMessages = getMessages(validatedInput.publication.defaultLanguage);
325
- const homeRoute = joinBasePathAndRoute(validatedInput.basePath, '/');
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 (task packet S2-T08): resolving
354
- // each page's own `og:image`/`twitter:image` needs the S2-T05 media
355
- // pipeline's own finished `assets` list (only it knows which derivative
356
- // file extension a given source image's `sourceDigest` produced — see
357
- // `internal/seo.js`). This only moves the in-memory computation earlier;
358
- // `mediaFiles`' actual bytes are still written to `outputDirectory` only
359
- // after the route listing below (unchanged from S2-T05), so
360
- // `outputDirectory/assets/media/` still can never be mistaken for an
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
- // S2-T08: the site-wide feeds, resolved once so every page's feed-
368
- // discovery `<link>` tags and the feed documents themselves agree on the
369
- // exact same self URLs.
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 = appearanceBootstrapScriptHref(
373
- validatedInput.basePath,
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
- // S2-T12: the selected theme package's copied stylesheets/passive assets
377
- // and the ordered `<link>` markup every generated page's `<head>` inserts.
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(validatedInput.basePath);
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: validatedInput.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(joinedRoute, validatedInput.baseUrl).toString(),
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.joinedTarget),
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
- // S2-T08: the one ordinary generated `404.html` error page, using the
477
- // S2-T06 `error` page kind's own pure body function. Pushed after the
478
- // guard above so the guard's own "is there anything authored to render"
479
- // check is unaffected by this always-present synthetic page.
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
- // S2-T08: feeds, sitemap and the static search index. Each is a fixed,
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: validatedInput.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
- // S2-T08: the exact `routeClass`/`mediaType` this renderer's own generated
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`, unchanged from
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
- // S2-T12: the selected theme package's stylesheet/passive-asset bytes,
667
- // written for the same reason and at the same point as the media pipeline
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
- // S2-T07's one deterministic pre-paint appearance-controller bootstrap
681
- // script (brief S2 section 3: "the appearance controller is the only
682
- // browser bootstrap"). Written after the routes listing above for the same
683
- // reason as the media pipeline output immediately above: it must never be
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';