@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
package/README.md
CHANGED
|
@@ -5,14 +5,14 @@ portable author repository format. It is one of the three author-facing S2
|
|
|
5
5
|
artifacts, alongside one `@rathnasgala2/theme-*` package and
|
|
6
6
|
`@rathnasgala2/publish-action`.
|
|
7
7
|
|
|
8
|
-
This package is JavaScript ESM on Node 24, authored without TypeScript sources
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
This package is JavaScript ESM on Node 24, authored without TypeScript sources:
|
|
9
|
+
every export carries JSDoc types, checked by `tsc --checkJs --noEmit` against a
|
|
10
|
+
hand-maintained `.d.ts` surface.
|
|
11
11
|
|
|
12
12
|
## Status
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
absence gate, quality-gate tooling) plus
|
|
14
|
+
This repository's scaffold (exact pins, `src/core/`-only layout, module-tree
|
|
15
|
+
absence gate, quality-gate tooling) plus its renderer adapter:
|
|
16
16
|
`renderPublication(buildInput, options)`, exported from `src/core/index.js`, is
|
|
17
17
|
the package's one documented entry point. It:
|
|
18
18
|
|
|
@@ -29,7 +29,7 @@ the package's one documented entry point. It:
|
|
|
29
29
|
`urn:gala:schema:artifact-manifest:2.0.0` instance describing the candidate
|
|
30
30
|
output directory.
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
The media pipeline lives under `src/core/internal/media/`, run from
|
|
33
33
|
`renderPublication` after the Eleventy route listing so its output is never
|
|
34
34
|
mistaken for an HTML route:
|
|
35
35
|
|
|
@@ -40,8 +40,8 @@ mistaken for an HTML route:
|
|
|
40
40
|
entropy decode (a documented scope decision — see
|
|
41
41
|
`webp-probe.js`/`avif-probe.js`). Every format shares one set of resource
|
|
42
42
|
ceilings in `limits.js` (raster dimension, pixel count, decoded-buffer and
|
|
43
|
-
source-byte caps reused from
|
|
44
|
-
plus this renderer's own documented per-publication ceilings).
|
|
43
|
+
source-byte caps reused from the theme package's own closed binary-asset
|
|
44
|
+
decoder bounds, plus this renderer's own documented per-publication ceilings).
|
|
45
45
|
- **Metadata strip, orientation.** PNG/JPEG derivatives are always re-encoded
|
|
46
46
|
from decoded pixels, never byte-copied, so no ancillary/EXIF metadata survives
|
|
47
47
|
into a derivative; JPEG's EXIF `Orientation` tag is read and applied
|
|
@@ -55,7 +55,7 @@ mistaken for an HTML route:
|
|
|
55
55
|
unconditionally as an image reference (`MEDIA_SVG_REJECTED`) — SVG is never
|
|
56
56
|
processed as author media.
|
|
57
57
|
- **Font policy.** `appearance.fontAssets` entries must be bounded WOFF2 (exact
|
|
58
|
-
signature,
|
|
58
|
+
signature, a fixed 4,194,304-byte font cap); a remote font reference cannot
|
|
59
59
|
enter `build-input` in the first place (`resolvedFile` is always a
|
|
60
60
|
repository-relative path), so "no remote font is ever retrieved" holds
|
|
61
61
|
structurally.
|
|
@@ -63,17 +63,16 @@ mistaken for an HTML route:
|
|
|
63
63
|
mismatch, SVG, unrecognized format, invalid font — is a `MediaPipelineError`
|
|
64
64
|
with a stable `reasonCode`, thrown before the manifest is assembled.
|
|
65
65
|
|
|
66
|
-
`options.sourceDirectory`
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
`
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
`
|
|
76
|
-
`const: "text/html"`): a `renderableBody.body` inside a validated
|
|
66
|
+
`options.sourceDirectory` is the caller-mounted, read-only repository source
|
|
67
|
+
tree `build-input`'s `resolvedFile` references point into; every referenced
|
|
68
|
+
file's bytes are read from there and verified against `build-input`'s declared
|
|
69
|
+
`sourceDigest` before being decoded.
|
|
70
|
+
|
|
71
|
+
The complete output-security pipeline (`src/core/internal/content-security.js`)
|
|
72
|
+
sits on top of that adapter. **Division of labour** (normalization replaces an
|
|
73
|
+
authored body path with a `renderableBody` carrying "the deterministic policy
|
|
74
|
+
output/digest"; `build-input.schema.json` fixes `renderableBody.bodyMediaType`
|
|
75
|
+
as `const: "text/html"`): a `renderableBody.body` inside a validated
|
|
77
76
|
`build-input:2.0.0` instance is **already** render-policy-conformant HTML,
|
|
78
77
|
produced once upstream of this renderer. This package therefore exposes two
|
|
79
78
|
distinct public entry points:
|
|
@@ -115,23 +114,37 @@ highlighter grammar/token-class catalog) both surfaces above read from,
|
|
|
115
114
|
generated deterministically by `scripts/generate-contracts.mjs` from
|
|
116
115
|
`src/core/internal/render-policy-content.js` — the single source of truth, so
|
|
117
116
|
the published contract and the enforced policy cannot drift apart. Every
|
|
118
|
-
rendered page also carries the
|
|
117
|
+
rendered page also carries the meta-safe CSP baseline as a
|
|
119
118
|
`<meta http-equiv="Content-Security-Policy">` tag (byte-identical on every
|
|
120
119
|
route, since S2 materializes no module package/configuration/output/runtime).
|
|
121
120
|
|
|
121
|
+
**TPL-H4 fix — meta vs. header-only directives.** The CSP specification
|
|
122
|
+
explicitly ignores `frame-ancestors` (and `report-uri`/`sandbox`, neither used
|
|
123
|
+
here) when a policy is delivered via `<meta http-equiv>`; the `<meta>` tag above
|
|
124
|
+
therefore never includes `frame-ancestors`, and
|
|
125
|
+
`contentSecurityPolicyHeaderOnlyDirectives()` (`internal/content-security.js`)
|
|
126
|
+
returns the one directive that is not: `frame-ancestors 'none'`. Clickjacking
|
|
127
|
+
protection for a published artifact is a **hosting-layer requirement**: the
|
|
128
|
+
adapter/deployment serving this artifact must add
|
|
129
|
+
`Content-Security-Policy: frame-ancestors 'none'` (or an equivalent
|
|
130
|
+
`X-Frame-Options` header) as a real HTTP response header. This cannot yet be
|
|
131
|
+
carried in `artifact-manifest:2.0.0`'s own `declarativeHeaders` field, which
|
|
132
|
+
`@rathnasgala2/schemas` currently fixes to `const: []`; surfacing it there is a
|
|
133
|
+
schema-repo change, out of this repository's scope.
|
|
134
|
+
|
|
122
135
|
`img[src]` is restricted to repository-relative paths only in this pipeline;
|
|
123
|
-
full manifest-coverage verification of media references is the
|
|
124
|
-
|
|
136
|
+
full manifest-coverage verification of media references is the media pipeline's
|
|
137
|
+
job (immediately above). A handful of manifest fields (`builder`,
|
|
125
138
|
`sourceIdentity.repository`, `workflowIdentity`, `buildToolVersions`,
|
|
126
139
|
`excludedInputs`) describe facts `build-input` does not itself carry;
|
|
127
140
|
`renderPublication` accepts them as an explicit `options.provenance` bundle
|
|
128
141
|
rather than fabricating them — see `src/core/manifest.js`'s module
|
|
129
|
-
documentation. `contracts/theme-styling-contract.jcs` is
|
|
130
|
-
`templateStylingContract` catalog (see the
|
|
142
|
+
documentation. `contracts/theme-styling-contract.jcs` is the real, closed
|
|
143
|
+
`templateStylingContract` catalog (see the theme styling contract section
|
|
144
|
+
below).
|
|
131
145
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
through:
|
|
146
|
+
The core semantic skeleton, page kinds and navigation renderer sit on top of the
|
|
147
|
+
output-security pipeline every generated page still passes through:
|
|
135
148
|
|
|
136
149
|
- **`src/core/internal/skeleton.js`** renders the one landmark contract every
|
|
137
150
|
page kind shares, through its one `renderPageBody` composition: a skip link
|
|
@@ -144,13 +157,13 @@ through:
|
|
|
144
157
|
template attribution line). Core owns nine versioned semantic slots
|
|
145
158
|
(`header-actions`, `article-preamble`, `article-end`, `footer-profile`,
|
|
146
159
|
`footer-auxiliary`, `account-intent`, `conversation`, `newsletter`,
|
|
147
|
-
`edition-selector`) plus the collapsed `article-footer-ad` slot
|
|
148
|
-
|
|
149
|
-
|
|
160
|
+
`edition-selector`) plus the collapsed `article-footer-ad` slot: every slot is
|
|
161
|
+
one `data-gala-slot="<name>"` attribute on an otherwise empty, non-landmark
|
|
162
|
+
`<div>`, and the collapsed slot additionally carries `hidden`. This renderer
|
|
150
163
|
has no module system, so every slot except `footer-profile` (which always
|
|
151
|
-
wraps the publication's own footer card) and `header-actions` (
|
|
152
|
-
|
|
153
|
-
|
|
164
|
+
wraps the publication's own footer card) and `header-actions` (the appearance
|
|
165
|
+
control, core content rather than a module) renders empty. This module also
|
|
166
|
+
renders breadcrumbs and deterministic pagination controls
|
|
154
167
|
(`rel="prev"`/`rel="next"`, an `aria-current="page"` status).
|
|
155
168
|
- **`src/core/internal/page-kinds.js`** builds every generated page kind from a
|
|
156
169
|
validated `build-input:2.0.0`: `profile` (the optional publication profile),
|
|
@@ -162,10 +175,10 @@ through:
|
|
|
162
175
|
page per distinct authored value, plus a `/tags`/`/series` root page linking
|
|
163
176
|
to each), and `archive` (one listing page per UTC calendar year, plus a
|
|
164
177
|
`/archive` root page). The `error` page kind (`renderErrorPageBody`) is
|
|
165
|
-
exported as a pure function for
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
178
|
+
exported as a pure function for the `404.html` caller (see below), not wired
|
|
179
|
+
into any generated route here. Listing page size (10), sort tie-break, and the
|
|
180
|
+
tag/series/archive root pages are documented scoped decisions this renderer's
|
|
181
|
+
own governing brief does not fix verbatim.
|
|
169
182
|
- **`src/core/internal/messages.js`** is the message catalog every user-visible
|
|
170
183
|
chrome string above is looked up from (never an inline literal), keyed by
|
|
171
184
|
BCP-47 tag with a fixed `en` fallback — the only catalog S2 authors;
|
|
@@ -186,19 +199,19 @@ every page (a scoped decision: navigation/footer text does not flip language
|
|
|
186
199
|
page to page); each page's own `<html lang>`/`dir` still reflects that
|
|
187
200
|
individual page's own content language.
|
|
188
201
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
202
|
+
The Light/Dark/System appearance controller (`src/core/internal/appearance/`) is
|
|
203
|
+
the only browser bootstrap this renderer emits ("Module absence and CSP
|
|
204
|
+
equality"):
|
|
192
205
|
|
|
193
206
|
- **`contract.js`** is the one module every other appearance-related file
|
|
194
|
-
imports every name/value from: the two
|
|
207
|
+
imports every name/value from: the two fixed root attributes
|
|
195
208
|
(`data-gala-publication-root` presence, `data-gala-resolved-color-mode`
|
|
196
209
|
exact-value `"light"`/`"dark"`), the template-internal
|
|
197
210
|
`data-gala-color-mode-selection` bookkeeping attribute, the three canonical
|
|
198
211
|
mode values (`light`/`dark`/`system`), the versioned local storage key
|
|
199
212
|
(`gala:appearance:color-mode:v1`), the control's `id`, the bootstrap script's
|
|
200
213
|
fixed output path/media type and the `color-scheme` meta content — the
|
|
201
|
-
contract
|
|
214
|
+
contract the published theme styling catalog targets.
|
|
202
215
|
- **`controller-markup.js`** renders a native `<select>` (a brief-admitted
|
|
203
216
|
pattern; needs no `aria-pressed`/`aria-checked` bookkeeping of its own, unlike
|
|
204
217
|
a custom button-group or radio-group would) bound to a visible `<label>`, with
|
|
@@ -225,29 +238,47 @@ section 3, "Module absence and CSP equality"):
|
|
|
225
238
|
directory and enters it into the manifest as an ordinary `manifestAsset` row
|
|
226
239
|
(`{path, byteLength, sha256}` plus its fixed
|
|
227
240
|
`application/javascript; charset=utf-8` media type), after the Eleventy route
|
|
228
|
-
listing — exactly like
|
|
229
|
-
|
|
230
|
-
|
|
241
|
+
listing — exactly like a media derivative — so it is never mistaken for an
|
|
242
|
+
HTML route. `src/core/internal/eleventy-render.js`'s skeleton layout carries
|
|
243
|
+
the `data-gala-publication-root` attribute, the
|
|
231
244
|
`<meta name="color-scheme" content="light dark">` tag and the one
|
|
232
245
|
`<script src>` reference, all fixed literals from `contract.js`, so every
|
|
233
|
-
route's `<head>` is still byte-identical (
|
|
246
|
+
route's `<head>` is still byte-identical (this renderer materializes no module
|
|
234
247
|
package/configuration/output/runtime).
|
|
235
248
|
- With JavaScript disabled, the control still renders (a native `<select>`,
|
|
236
|
-
fully keyboard-operable and usable with no script at all)
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
249
|
+
fully keyboard-operable and usable with no script at all). The resolved root
|
|
250
|
+
attribute is server-rendered as the fixed `light` default, so a no-JS reader,
|
|
251
|
+
an archival crawler, or a load where the bootstrap script fails still gets a
|
|
252
|
+
fully themed page instead of unstyled UA-default HTML; a scripted reader's
|
|
253
|
+
phase 1 bootstrap always overwrites this attribute with the reader's real
|
|
254
|
+
stored selection/system preference before first paint.
|
|
255
|
+
`<meta name="color-scheme">` also carries `prefers-color-scheme` through to
|
|
256
|
+
user-agent styling (form controls, scrollbars) independent of any script.
|
|
257
|
+
|
|
258
|
+
**Authoring a theme:** see [`docs/theme-authoring.md`](docs/theme-authoring.md)
|
|
259
|
+
for the full guide — how a theme is a delta over the template-owned `gala-base`
|
|
260
|
+
layer, the closed 35-token catalog, the closed five-member pseudo-class catalog,
|
|
261
|
+
passive-asset/SVG admission, the `theme.json` digest chain, and `templateRange`.
|
|
262
|
+
Run `node scripts/scaffold-theme.mjs <directory>` to write a minimal,
|
|
263
|
+
already-conformant theme package skeleton rather than hand-typing one.
|
|
264
|
+
|
|
265
|
+
The theme styling contract replaces `contracts/theme-styling-contract.jcs`'s
|
|
266
|
+
original scaffold placeholder with the real, closed `templateStylingContract`
|
|
267
|
+
catalog this renderer's own internal styling decision defines, and wires the
|
|
268
|
+
selected theme package's stylesheets into every generated page:
|
|
245
269
|
|
|
246
270
|
- **`src/core/internal/appearance/styling-contract.js`** is the one reviewed
|
|
247
271
|
source module `scripts/generate-contracts.mjs` emits the contract from: the
|
|
248
|
-
ordered
|
|
249
|
-
`gala-print`
|
|
250
|
-
|
|
272
|
+
ordered five-layer catalog (`gala-base`/`gala-tokens`/`gala-components`/
|
|
273
|
+
`gala-utilities`/`gala-print` — contract 2.1.0: `gala-base` is the template's
|
|
274
|
+
own layer, carrying the reset/type-scale/focus-ring defaults documented below
|
|
275
|
+
`internal/appearance/base-layer.js`, always ordered first so a theme's own
|
|
276
|
+
layers can override it), the publication-root/ resolved-palette selectors, the
|
|
277
|
+
closed type/class/id/attribute leaf catalogs, a closed five-member
|
|
278
|
+
pseudo-class catalog (`hover`, `focus-visible`, `active`, `visited`,
|
|
279
|
+
`disabled` — contract 2.1.0; see that module's own documentation for why each
|
|
280
|
+
is admitted and why the token catalog's `color-focus`/`color-link-visited` had
|
|
281
|
+
no reachable application before it), and exactly 64 `publicThemeSlotHooks` —
|
|
251
282
|
one per catalog leaf, so every leaf a theme could validly select is a named,
|
|
252
283
|
documented hook and no catalog member is orphaned. Every leaf is drawn from
|
|
253
284
|
what this renderer actually renders: 28 type-selector hooks (every
|
|
@@ -255,16 +286,16 @@ generated page:
|
|
|
255
286
|
`internal/page-kinds.js` and the markdown-it CommonMark pipeline can produce),
|
|
256
287
|
15 class-selector hooks (the base Prism `.token` class plus one
|
|
257
288
|
`.language-<grammar>` hook per admitted highlight grammar — fine-grained
|
|
258
|
-
per-token-kind classes are a documented
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
289
|
+
per-token-kind classes are a documented scope exclusion), 2 id-selector hooks
|
|
290
|
+
(`#main-content`, the appearance `<select>`'s fixed id) and 19 attribute-value
|
|
291
|
+
hooks (one per `data-gala-slot` value, one per the `data-gala-page-kind`
|
|
292
|
+
value). `catalogDigest` is
|
|
262
293
|
`SHA256(UTF8("GALA-TEMPLATE-STYLING-CONTRACT-V2\0") || JCS(...))`, matching
|
|
263
|
-
|
|
264
|
-
`templateStylingContract` profile uses
|
|
265
|
-
the package's public API, so this
|
|
266
|
-
domain-separated formula rather than importing
|
|
267
|
-
`assertTemplateStylingContractShape` is this repository's own structural
|
|
294
|
+
the same domain-separated digest construction and the digest profile
|
|
295
|
+
`@rathnasgala2/schemas`' own internal `templateStylingContract` profile uses
|
|
296
|
+
(that function is not exported through the package's public API, so this
|
|
297
|
+
module reimplements the same domain-separated formula rather than importing
|
|
298
|
+
it). `assertTemplateStylingContractShape` is this repository's own structural
|
|
268
299
|
self-validator (root/palette attribute rows, fixed layer/selector/composition
|
|
269
300
|
constants, hook count/uniqueness bound, `catalogDigest` recomputation); no
|
|
270
301
|
`urn:gala:schema:template-styling-contract` schema ID is registered in
|
|
@@ -286,50 +317,74 @@ generated page:
|
|
|
286
317
|
`@rathnasgala2/schemas`' own exported `validateGalaDocument`.
|
|
287
318
|
- **`src/core/internal/theme-assets.js`** is the optional theme-asset
|
|
288
319
|
integration layer: given `options.themeDirectory` (an extracted theme package
|
|
289
|
-
directory, shaped exactly like the
|
|
290
|
-
`tokens.css`, `components.css`, an optional `utilities.css`,
|
|
291
|
-
`package.json`, `LICENSE`, `README.md`, `assets/*`), it copies
|
|
292
|
-
stylesheets and any declared non-CSS passive assets into
|
|
293
|
-
enters each as an ordinary `{path, byteLength, sha256}`
|
|
294
|
-
and renders the ordered `<link>` markup every generated
|
|
295
|
-
inserts (`print.css` alone carries `media="print"`). It
|
|
296
|
-
own declared `stylesheets` list against the two admitted
|
|
297
|
-
without `utilities.css`) and that `cssLayers` byte-equals the
|
|
298
|
-
layer projection, but does **not** re-implement
|
|
299
|
-
Module admission grammar — that is the shared `theme-release.yml`
|
|
300
|
-
runner's job
|
|
320
|
+
directory, shaped exactly like the theme package's own file set —
|
|
321
|
+
`theme.json`, `tokens.css`, `components.css`, an optional `utilities.css`,
|
|
322
|
+
`print.css`, `package.json`, `LICENSE`, `README.md`, `assets/*`), it copies
|
|
323
|
+
the declared stylesheets and any declared non-CSS passive assets into
|
|
324
|
+
`assets/theme/`, enters each as an ordinary `{path, byteLength, sha256}`
|
|
325
|
+
`manifestAsset` row, and renders the ordered `<link>` markup every generated
|
|
326
|
+
page's `<head>` inserts (`print.css` alone carries `media="print"`). It
|
|
327
|
+
validates the theme's own declared `stylesheets` list against the two admitted
|
|
328
|
+
shapes (with or without `utilities.css`) and that `cssLayers` byte-equals the
|
|
329
|
+
corresponding layer projection, but does **not** re-implement the full CSS
|
|
330
|
+
Syntax Module admission grammar — that is the shared `theme-release.yml`
|
|
331
|
+
conformance runner's job, run once per theme release, never per publication
|
|
301
332
|
build. `renderPublication` falls back to the pre-existing fixed
|
|
302
333
|
`assets/theme/print.css` link when no `options.themeDirectory` is supplied
|
|
303
|
-
(every caller
|
|
304
|
-
exercise
|
|
334
|
+
(every caller that predates theme integration, and this repository's own tests
|
|
335
|
+
that do not exercise it). Minimal in-repo fixture theme packages for both
|
|
305
336
|
stylesheet-list shapes live under `test/fixtures/theme-fixture-full/` and
|
|
306
337
|
`test/fixtures/theme-fixture-minimal/` — deliberately not real, conformant
|
|
307
|
-
theme CSS (authoring one is
|
|
308
|
-
copy/link/manifest mechanism.
|
|
338
|
+
theme CSS (authoring one is a theme package's own task); they exist only to
|
|
339
|
+
exercise the copy/link/manifest mechanism.
|
|
340
|
+
|
|
341
|
+
Hardening at consume time (a theme package is lower-trust supply-chain input
|
|
342
|
+
than the repository owner's own authored content, so it is held to at least the
|
|
343
|
+
same bar): `theme.json` is schema-validated against
|
|
344
|
+
`urn:gala:schema:theme-contract:2.0.0` before anything else, and its
|
|
345
|
+
`contractVersion`/`stylingContractDigest`/ `templateRange` are checked against
|
|
346
|
+
this renderer's own published styling contract and version
|
|
347
|
+
(`internal/semver-range.js`: a deliberately closed exact-version/caret-range
|
|
348
|
+
subset). Every declared file's `byteLength`/ `sha256` is verified against the
|
|
349
|
+
bytes actually read, and `theme.json.budgets` is enforced. Every passive asset's
|
|
350
|
+
bytes are sniffed (never trusted from the declared `mediaType`) against a closed
|
|
351
|
+
allowlist: raster formats are admitted directly, and SVG is admitted only after
|
|
352
|
+
`internal/media/theme-svg-sanitizer.js`'s closed grammar rejects any
|
|
353
|
+
`<script>`/event-handler/external-reference construct outright. **Iconography is
|
|
354
|
+
in scope and expected** (TPL-H6 decision): this sanitizer is what makes a
|
|
355
|
+
theme-declared SVG icon mark safe to publish.
|
|
356
|
+
|
|
357
|
+
- **`baseUrl` path prefix (2.2.0).** `buildInput.baseUrl` may carry a path (a
|
|
358
|
+
GitHub project site, `https://owner.github.io/g9/`); it is not origin-only.
|
|
359
|
+
`internal/route.js#derivePublicBasePath(baseUrl, basePath)` joins that path
|
|
360
|
+
with `basePath` into the one public base path every emitted URL uses (route
|
|
361
|
+
and asset hrefs, the appearance script, stylesheet links, canonical/`og:url`,
|
|
362
|
+
feed, sitemap and search-index URLs). Output file paths still use `basePath`
|
|
363
|
+
alone, because the artifact is deployed at the hosting site root.
|
|
309
364
|
- **basePath fix (LOCAL-7 follow-up; independent-review finding B2).** Before
|
|
310
|
-
|
|
311
|
-
`og:image`/`twitter:image` URL were both root-absolute,
|
|
312
|
-
publication's own `basePath` — a non-root `basePath`
|
|
313
|
-
them. `internal/appearance/contract.js`'s new
|
|
365
|
+
theme integration existed, the appearance bootstrap script's `<script src>`
|
|
366
|
+
and the social-image `og:image`/`twitter:image` URL were both root-absolute,
|
|
367
|
+
unprefixed by the publication's own `basePath` — a non-root `basePath`
|
|
368
|
+
publication would 404 them. `internal/appearance/contract.js`'s new
|
|
314
369
|
`appearanceBootstrapScriptHref(basePath)` and `internal/seo.js`'s
|
|
315
370
|
`resolveSocialImageUrl` (now taking `basePath`) both join through
|
|
316
371
|
`internal/route.js`'s shared `joinBasePathAndRoute`/`projectFixedAssetPath`,
|
|
317
372
|
exactly like the pre-existing print-stylesheet convention (now folded into
|
|
318
373
|
`internal/theme-assets.js`) already did for _hrefs_. Independent review
|
|
319
374
|
(finding B2) found that this was only half the fix: the _physical files_ this
|
|
320
|
-
renderer writes for every asset class — theme CSS/passive assets, every
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
375
|
+
renderer writes for every asset class — theme CSS/passive assets, every media
|
|
376
|
+
derivative, and the appearance bootstrap script — were still written, and
|
|
377
|
+
entered into the manifest, at their bare **unprefixed** paths. Since the
|
|
378
|
+
local-directory adapter serves a `manifestAsset.path` verbatim, a non-root
|
|
379
|
+
`basePath` publication's `<link>`/`<script src>`/`og:image` href would point
|
|
380
|
+
at `<basePath>/assets/...` while the actual file sat at the unprefixed
|
|
381
|
+
`assets/...` — a 404 in practice, not just a theoretical href mismatch.
|
|
382
|
+
`src/core/index.js` now joins `basePath` into the physical destination path
|
|
383
|
+
and the `manifestAsset.path` for media derivatives (`joinedMediaAssets`,
|
|
384
|
+
computed with the same `projectFixedAssetPath` every other fixed-name
|
|
385
|
+
generated asset already uses) and the appearance bootstrap script
|
|
386
|
+
(`joinedAppearanceScriptPath`); `internal/theme-assets.js` computes its joined
|
|
387
|
+
output path once per file and derives both the `<link href>` and the
|
|
333
388
|
file/manifest path from that exact same value, so they cannot drift apart.
|
|
334
389
|
`test/theme-styling-contract.test.js` carries a dedicated non-root,
|
|
335
390
|
multi-segment `basePath` (`/blog/2024`) fixture test asserting every one of
|
|
@@ -356,14 +411,15 @@ generated page:
|
|
|
356
411
|
|
|
357
412
|
### Media pipeline scope decisions (documented, not silently assumed)
|
|
358
413
|
|
|
359
|
-
Neither
|
|
360
|
-
formats/quality, or a per-publication
|
|
361
|
-
media (
|
|
362
|
-
`gala-theme-binary-assets-v2` _theme passive-asset_
|
|
363
|
-
therefore documents its own choices in
|
|
364
|
-
rather than inventing undocumented magic
|
|
414
|
+
Neither this renderer's own governing brief nor its internal styling decision
|
|
415
|
+
fixes exact derivative widths, output formats/quality, or a per-publication
|
|
416
|
+
media byte/count ceiling for _author_ media (those numeric bounds are stated
|
|
417
|
+
only for the closed `gala-theme-binary-assets-v2` _theme passive-asset_
|
|
418
|
+
profile). This renderer therefore documents its own choices in
|
|
419
|
+
`src/core/internal/media/limits.js` rather than inventing undocumented magic
|
|
420
|
+
numbers:
|
|
365
421
|
|
|
366
|
-
- decode/resource ceilings reuse
|
|
422
|
+
- decode/resource ceilings reuse the theme binary-asset profile's numbers
|
|
367
423
|
(dimension, pixel-count, decoded-buffer, source-byte caps), since the
|
|
368
424
|
decode-bomb risk they defend against is identical regardless of asset class;
|
|
369
425
|
- responsive derivative widths (`320/640/960/1280/1920`), JPEG derivative
|
|
@@ -375,8 +431,8 @@ rather than inventing undocumented magic numbers:
|
|
|
375
431
|
proportion to this task, and is flagged as a follow-up rather than attempted
|
|
376
432
|
partially.
|
|
377
433
|
|
|
378
|
-
|
|
379
|
-
on top of
|
|
434
|
+
Every remaining "Required generated output" from this renderer's own governing
|
|
435
|
+
brief sits on top of the page kinds above:
|
|
380
436
|
|
|
381
437
|
- **Feeds** (`src/core/internal/feeds.js`): deterministic Atom 1.0
|
|
382
438
|
(`/feed/atom.xml`) and RSS 2.0 (`/feed/rss.xml`) documents over the exact
|
|
@@ -384,7 +440,8 @@ on top of S2-T06's page kinds:
|
|
|
384
440
|
`internal/page-kinds.js`'s `index` page kind paginates (the shared
|
|
385
441
|
`selectPublishedArticles` export), every item URL absolute against
|
|
386
442
|
`buildInput.baseUrl`, bounded to the most recent 50 entries (documented scoped
|
|
387
|
-
decision — neither the brief nor
|
|
443
|
+
decision — neither the brief nor this renderer's own styling decision fixes a
|
|
444
|
+
feed item count).
|
|
388
445
|
- **Sitemap** (`src/core/internal/sitemap.js`): `/sitemap.xml` over every page
|
|
389
446
|
`internal/page-kinds.js` generated that this renderer did not itself mark
|
|
390
447
|
`noindex` (unlisted content), deterministically sorted by each entry's own
|
|
@@ -394,9 +451,10 @@ on top of S2-T06's page kinds:
|
|
|
394
451
|
`/search-index.json`, a deterministic JSON document indexing every published
|
|
395
452
|
`article`/`page` record's title, description, a bounded plain-text excerpt,
|
|
396
453
|
tags and timestamps. Its shape is a documented scoped decision (neither the
|
|
397
|
-
brief nor
|
|
398
|
-
— the Light/Dark/System appearance controller
|
|
399
|
-
browser bootstrap this renderer ever emits
|
|
454
|
+
brief nor this renderer's own styling decision fixes one). **No runtime search
|
|
455
|
+
JavaScript accompanies it** — the Light/Dark/System appearance controller
|
|
456
|
+
remains the only browser bootstrap this renderer ever emits (acceptance test
|
|
457
|
+
C).
|
|
400
458
|
- **SEO/Open-Graph/Twitter/localization metadata**: every generated page's
|
|
401
459
|
`<head>` now carries a viewport meta, an optional description, an optional
|
|
402
460
|
`robots` directive (`noindex, follow` for `status: "unlisted"` content and for
|
|
@@ -406,16 +464,15 @@ on top of S2-T06's page kinds:
|
|
|
406
464
|
`<link>`s. **hreflang** is a documented scope decision, not a silent omission:
|
|
407
465
|
`build-input:2.0.0` has no translation/locale-variant linkage between distinct
|
|
408
466
|
`content[]` records (see `internal/seo.js`'s module documentation), so no
|
|
409
|
-
`<link rel="alternate" hreflang="...">` is ever emitted
|
|
410
|
-
- **`404.html`**: the
|
|
411
|
-
actually wired into `renderPublication` at
|
|
467
|
+
`<link rel="alternate" hreflang="...">` is ever emitted by this renderer.
|
|
468
|
+
- **`404.html`**: the `error` page kind's pure `renderErrorPageBody`, now
|
|
469
|
+
actually wired into `renderPublication` at the fixed `errorDocumentKey`
|
|
412
470
|
(`internal/route.js#errorDocumentPath`) — `404.html` at the root, or
|
|
413
471
|
`<basePath>/404.html` otherwise.
|
|
414
|
-
- **Redirect documents**: unchanged from
|
|
472
|
+
- **Redirect documents**: unchanged from the renderer adapter
|
|
415
473
|
(`internal/route.js#renderRedirectDocument`) — every authored redirect was
|
|
416
474
|
already an ordinary generated artifact with manifest status fixed at `200`;
|
|
417
|
-
this
|
|
418
|
-
it.
|
|
475
|
+
this is documenting that fact here, not re-implementing it.
|
|
419
476
|
- **Print CSS hookup** (`src/core/internal/print-stylesheet.js`): every page's
|
|
420
477
|
`<head>` carries a `<link rel="stylesheet" media="print">` at the documented
|
|
421
478
|
conventional path `<basePath>/assets/theme/print.css`. A selected theme's own
|
|
@@ -430,11 +487,16 @@ by the route's own fixed output path.
|
|
|
430
487
|
|
|
431
488
|
## Prerequisites
|
|
432
489
|
|
|
433
|
-
- Node.js 24.
|
|
434
|
-
|
|
490
|
+
- Node.js >= 24.0.0 (this repository's own development/CI toolchain is Node.js
|
|
491
|
+
24.18.0)
|
|
492
|
+
- npm >= 11.0.0 (this repository's own development/CI toolchain is npm 11.16.0)
|
|
435
493
|
|
|
436
|
-
|
|
437
|
-
|
|
494
|
+
`package.json`'s published `engines` field declares the supported _range_
|
|
495
|
+
(TPL-H5 fix: an exact pin published in the tarball rejects every consumer not on
|
|
496
|
+
that precise patch version, including under a consumer's own `engine-strict`
|
|
497
|
+
setting). This repository's own exact toolchain is pinned separately in
|
|
498
|
+
`devEngines` (evaluated only for this project root, never for a consumer) and in
|
|
499
|
+
`.nvmrc`/`.node-version`.
|
|
438
500
|
|
|
439
501
|
## Commands
|
|
440
502
|
|
|
@@ -443,11 +505,15 @@ npm install
|
|
|
443
505
|
npm run verify
|
|
444
506
|
```
|
|
445
507
|
|
|
446
|
-
`npm run verify`
|
|
508
|
+
`npm run verify` (this description is kept in sync with `package.json`'s own
|
|
509
|
+
`verify` script string by `test/readme-verify-description.test.js`, not
|
|
510
|
+
hand-maintained) runs, in order: Prettier format check, ESLint,
|
|
447
511
|
`tsc --checkJs --noEmit`, the dependency-cruiser
|
|
448
512
|
architecture/module-tree-absence gate, `jscpd` duplication scan, the
|
|
449
|
-
`contracts/*.jcs` canonical-form check,
|
|
450
|
-
check,
|
|
513
|
+
`contracts/*.jcs` canonical-form check, the no-local-schema-pin check,
|
|
514
|
+
`node --test`, the license inventory check, the SBOM currency check
|
|
515
|
+
(`sbom:check` compares `sbom.cdx.json` against the current lockfile — it is not
|
|
516
|
+
a presence check), the workflow-pin check, and `npm audit --audit-level=high`.
|
|
451
517
|
|
|
452
518
|
Individual gates:
|
|
453
519
|
|
|
@@ -458,21 +524,25 @@ npm run lint # ESLint flat config
|
|
|
458
524
|
npm run typecheck # tsc --checkJs --noEmit against types/index.d.ts
|
|
459
525
|
npm run architecture # dependency-cruiser: no src/modules/, no circular deps
|
|
460
526
|
npm run duplication # jscpd, 3% / 50 tokens
|
|
461
|
-
npm run contracts:generate # (re)write contracts/*.jcs
|
|
527
|
+
npm run contracts:generate # (re)write contracts/*.jcs from their reviewed source modules
|
|
462
528
|
npm run contracts:check # fail if committed contracts drifted
|
|
529
|
+
npm run schema-pin:check # fail on a file: @rathnasgala2/schemas specifier
|
|
463
530
|
npm test # node --test
|
|
464
531
|
npm run licenses:generate # (re)write THIRD_PARTY_LICENSES.json
|
|
465
532
|
npm run licenses:check # fail if it drifted from package-lock.json
|
|
466
533
|
npm run sbom:generate # cyclonedx-npm -> sbom.cdx.json
|
|
467
|
-
npm run sbom:check # fail if sbom.cdx.json is
|
|
534
|
+
npm run sbom:check # fail if sbom.cdx.json is stale against package-lock.json
|
|
535
|
+
npm run workflows:check # fail if a GitHub Actions ref is not SHA-pinned
|
|
536
|
+
npm run workflows:drift # fail if workflow files diverge from their own baseline
|
|
537
|
+
npm run audit # npm audit --audit-level=high
|
|
468
538
|
```
|
|
469
539
|
|
|
470
540
|
## Package layout
|
|
471
541
|
|
|
472
542
|
```text
|
|
473
543
|
src/core/ # the only source root
|
|
474
|
-
contracts/render-policy.jcs # normalized-body policy identity (
|
|
475
|
-
contracts/theme-styling-contract.jcs # published styling catalog (
|
|
544
|
+
contracts/render-policy.jcs # normalized-body policy identity (real content)
|
|
545
|
+
contracts/theme-styling-contract.jcs # published styling catalog (real and closed)
|
|
476
546
|
```
|
|
477
547
|
|
|
478
548
|
No `src/modules/` tree, module import edge, registration stub, module
|
|
@@ -481,7 +551,7 @@ configuration, module output or module runtime is ever admitted beside
|
|
|
481
551
|
`only-src-core-is-a-source-root` rule both enforce this; `interactions`,
|
|
482
552
|
`whitelabel`, `newsletter` and `prism` have no module directory, package,
|
|
483
553
|
configuration schema, stub, output, runtime code, network target or
|
|
484
|
-
compatibility promise in this MVP
|
|
554
|
+
compatibility promise in this MVP.
|
|
485
555
|
|
|
486
556
|
## Consuming `@rathnasgala2/schemas`
|
|
487
557
|
|
|
@@ -508,8 +578,12 @@ and `artifact-manifest` are byte-identical from 2.6.1 to 2.8.0.
|
|
|
508
578
|
|
|
509
579
|
## Governing documents
|
|
510
580
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
-
|
|
581
|
+
The internal specifications this package is built against live outside this
|
|
582
|
+
repository and have no public URL, so they are named here as plain text rather
|
|
583
|
+
than as dead links:
|
|
584
|
+
|
|
585
|
+
- Slice brief S2: author-owned publication, section 3 (`@rathnasgala2/template`)
|
|
586
|
+
(internal)
|
|
587
|
+
- The internal decision record establishing JavaScript ESM for public packages
|
|
588
|
+
(internal)
|
|
589
|
+
- WORKSPACE.md, sections 4-8 and 13 (internal)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"contentSecurityPolicy":"default-src 'none'; base-uri 'none'; object-src 'none'; frame-ancestors 'none'; form-action 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'none'; media-src 'self'; manifest-src 'self'; worker-src 'none'","highlighter":{"grammars":["bash","css","diff","java","javascript","json","jsx","markdown","markup","python","sql","tsx","typescript","yaml"],"package":"@11ty/eleventy-plugin-syntaxhighlight","tokenClasses":["atrule","attr-equals","attr-name","attr-value","bold","boolean","builtin","cdata","char","class-name","comment","constant","content","coord","deleted","deleted-sign","doctype","entity","for-or-select","function","function-variable","important","inserted","inserted-sign","interpolation","interpolation-punctuation","italic","key","keyword","namespace","null","number","operator","prefix","prolog","property","punctuation","regex","rule","selector","shebang","string","symbol","tag","template-punctuation","template-string","title","token","url","variable"],"version":"5.0.2"},"markdownIt":{"options":{"html":false,"linkify":false,"typographer":false},"package":"markdown-it","version":"14.3.1"},"name":"gala-render-policy","sanitizer":{"allowedAttributes":{"a":["href","rel"],"code":["class"],"h1":["id"],"h2":["id"],"h3":["id"],"h4":["id"],"h5":["id"],"h6":["id"],"img":["src","alt"],"ol":["start"],"pre":["class"],"span":["class"]},"allowedTags":["p","br","hr","h1","h2","h3","h4","h5","h6","ul","ol","li","blockquote","pre","code","strong","em","b","i","u","s","del","ins","sub","sup","mark","small","wbr","a","img","table","thead","tbody","tr","th","td","dl","dt","dd","figure","figcaption","span"],"allowedUrlSchemes":["https","http","mailto"],"package":"sanitize-html","version":"2.17.7"},"version":"2.0.0"}
|
|
1
|
+
{"contentSecurityPolicy":"default-src 'none'; base-uri 'none'; object-src 'none'; frame-ancestors 'none'; form-action 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'none'; media-src 'self'; manifest-src 'self'; worker-src 'none'","contentSecurityPolicyHeaderOnlyDirectives":["frame-ancestors 'none'"],"contentSecurityPolicyMeta":"default-src 'none'; base-uri 'none'; object-src 'none'; form-action 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'none'; media-src 'self'; manifest-src 'self'; worker-src 'none'","highlighter":{"grammars":["bash","css","diff","java","javascript","json","jsx","markdown","markup","python","sql","tsx","typescript","yaml"],"package":"@11ty/eleventy-plugin-syntaxhighlight","tokenClasses":["atrule","attr-equals","attr-name","attr-value","bold","boolean","builtin","cdata","char","class-name","comment","constant","content","coord","deleted","deleted-sign","doctype","entity","for-or-select","function","function-variable","important","inserted","inserted-sign","interpolation","interpolation-punctuation","italic","key","keyword","namespace","null","number","operator","prefix","prolog","property","punctuation","regex","rule","selector","shebang","string","symbol","tag","template-punctuation","template-string","title","token","url","variable"],"version":"5.0.2"},"markdownIt":{"options":{"html":false,"linkify":false,"typographer":false},"package":"markdown-it","version":"14.3.1"},"name":"gala-render-policy","sanitizer":{"allowedAttributes":{"a":["href","rel"],"code":["class"],"h1":["id"],"h2":["id"],"h3":["id"],"h4":["id"],"h5":["id"],"h6":["id"],"img":["src","alt"],"ol":["start"],"pre":["class"],"span":["class"]},"allowedTags":["p","br","hr","h1","h2","h3","h4","h5","h6","ul","ol","li","blockquote","pre","code","strong","em","b","i","u","s","del","ins","sub","sup","mark","small","wbr","a","img","table","thead","tbody","tr","th","td","dl","dt","dd","figure","figcaption","span"],"allowedUrlSchemes":["https","http","mailto"],"package":"sanitize-html","version":"2.17.7"},"version":"2.0.0"}
|