@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,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The single source of truth for `contracts/render-policy.jcs`'s content
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The single source of truth for `contracts/render-policy.jcs`'s content:
|
|
3
|
+
* the markdown, sanitizer, highlighter and per-artifact CSP baseline, and
|
|
4
|
+
* the `renderPolicyIdentity` derived from them.
|
|
5
5
|
*
|
|
6
6
|
* `scripts/generate-contracts.mjs` emits this exact document as compact JCS
|
|
7
7
|
* to `contracts/render-policy.jcs`; `src/core/internal/content-security.js`
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
export const RENDER_POLICY_NAME = 'gala-render-policy';
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* Byte-equal to the template lock's `contractVersion
|
|
21
|
-
*
|
|
22
|
-
* `"2.0.0"` (matches `manifest.js`'s `buildInputContractVersion`).
|
|
20
|
+
* Byte-equal to the template lock's `contractVersion`, which every
|
|
21
|
+
* `build-input:2.0.0` fixture in this repository carries as the literal
|
|
22
|
+
* string `"2.0.0"` (matches `manifest.js`'s `buildInputContractVersion`).
|
|
23
23
|
*
|
|
24
24
|
* @type {string}
|
|
25
25
|
*/
|
|
@@ -31,7 +31,7 @@ export const MARKDOWN_IT_PACKAGE = 'markdown-it';
|
|
|
31
31
|
export const MARKDOWN_IT_VERSION = '14.3.1';
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* The exact markdown-it construction options
|
|
34
|
+
* The exact markdown-it construction options this renderer requires.
|
|
35
35
|
*
|
|
36
36
|
* @type {{html: false, linkify: false, typographer: false}}
|
|
37
37
|
*/
|
|
@@ -56,7 +56,7 @@ export const ALLOWED_URL_SCHEMES = Object.freeze(['https', 'http', 'mailto']);
|
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* The closed allowlist of tags this renderer ever emits from author
|
|
59
|
-
* markdown:
|
|
59
|
+
* markdown: semantic text and content tags only.
|
|
60
60
|
* No form, frame, SVG, MathML, script, style, object or template element is
|
|
61
61
|
* ever admitted.
|
|
62
62
|
*
|
|
@@ -110,8 +110,7 @@ export const ALLOWED_TAGS = Object.freeze([
|
|
|
110
110
|
/**
|
|
111
111
|
* The closed per-tag attribute allowlist. `id` is admitted only on headings
|
|
112
112
|
* (this renderer's own generated heading IDs); `style`, every `on*` handler,
|
|
113
|
-
* `srcdoc` and every other undeclared attribute is always stripped
|
|
114
|
-
* section 3).
|
|
113
|
+
* `srcdoc` and every other undeclared attribute is always stripped.
|
|
115
114
|
*
|
|
116
115
|
* @type {Readonly<Record<string, readonly string[]>>}
|
|
117
116
|
*/
|
|
@@ -138,7 +137,7 @@ export const HIGHLIGHTER_VERSION = '5.0.2';
|
|
|
138
137
|
/**
|
|
139
138
|
* The closed grammar-name catalog the owned highlighter accepts. A fence
|
|
140
139
|
* language outside this list always falls back to escaped, unhighlighted
|
|
141
|
-
* code
|
|
140
|
+
* code.
|
|
142
141
|
*
|
|
143
142
|
* @type {readonly string[]}
|
|
144
143
|
*/
|
|
@@ -167,8 +166,8 @@ export const HIGHLIGHT_GRAMMARS = Object.freeze(
|
|
|
167
166
|
* can produce (Prism's documented generic token set plus this
|
|
168
167
|
* repository's own empirical sample across every admitted grammar). `token`
|
|
169
168
|
* itself is always the base class; every other admitted value is a
|
|
170
|
-
* secondary token-class
|
|
171
|
-
*
|
|
169
|
+
* secondary token-class: `span` elements carry `token` plus admitted
|
|
170
|
+
* token-class values.
|
|
172
171
|
*
|
|
173
172
|
* @type {readonly string[]}
|
|
174
173
|
*/
|
|
@@ -241,15 +240,51 @@ export const ADMITTED_LANGUAGE_CLASSES = Object.freeze(
|
|
|
241
240
|
);
|
|
242
241
|
|
|
243
242
|
/**
|
|
244
|
-
* The
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
243
|
+
* The CSP specification explicitly ignores `frame-ancestors`
|
|
244
|
+
* (and `report-uri`/`sandbox`, neither used here) when a policy is
|
|
245
|
+
* delivered via `<meta http-equiv>` — browsers drop the directive and log a
|
|
246
|
+
* console warning on every page load. `frame-ancestors` is therefore
|
|
247
|
+
* split out as the one header-only directive: clickjacking protection for a
|
|
248
|
+
* published artifact is a hosting-layer obligation (a response header, most
|
|
249
|
+
* commonly `Content-Security-Policy` or `X-Frame-Options`), recorded here so
|
|
250
|
+
* every consuming adapter/deployment knows to add it, rather than a directive
|
|
251
|
+
* that silently does nothing inside the `<meta>` tag every page already
|
|
252
|
+
* carries.
|
|
253
|
+
*
|
|
254
|
+
* @type {readonly string[]}
|
|
255
|
+
*/
|
|
256
|
+
export const CSP_HEADER_ONLY_DIRECTIVES = Object.freeze([
|
|
257
|
+
"frame-ancestors 'none'",
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The exact per-artifact CSP baseline actually deliverable inside a
|
|
262
|
+
* `<meta http-equiv="Content-Security-Policy">` tag: {@link CSP_HEADER_ONLY_DIRECTIVES}
|
|
263
|
+
* is excluded. Directive order is byte-exact, minus
|
|
264
|
+
* the excluded directive; every directive's value list is already sorted
|
|
265
|
+
* bytewise and duplicate-free because this renderer materializes no module
|
|
266
|
+
* package, configuration, output or runtime, so this string is
|
|
248
267
|
* byte-identical on every route.
|
|
249
268
|
*
|
|
250
269
|
* @type {string}
|
|
251
270
|
*/
|
|
252
|
-
export const
|
|
271
|
+
export const CONTENT_SECURITY_POLICY_META_BASELINE =
|
|
272
|
+
"default-src 'none'; base-uri 'none'; object-src 'none'; " +
|
|
273
|
+
"form-action 'none'; script-src 'self'; " +
|
|
274
|
+
"style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'none'; " +
|
|
275
|
+
"media-src 'self'; manifest-src 'self'; worker-src 'none'";
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The complete CSP baseline this renderer originally specified as one
|
|
279
|
+
* string — the meta-safe directives plus the header-only directive, in
|
|
280
|
+
* this fixed byte-exact order — kept only as the render-policy identity's own
|
|
281
|
+
* digest input (`renderPolicyDocument`'s `contentSecurityPolicy` field): a
|
|
282
|
+
* hosting adapter that delivers this policy as a response header, rather
|
|
283
|
+
* than a `<meta>` tag, may deliver this complete string unmodified.
|
|
284
|
+
*
|
|
285
|
+
* @type {string}
|
|
286
|
+
*/
|
|
287
|
+
export const CONTENT_SECURITY_POLICY_COMPLETE =
|
|
253
288
|
"default-src 'none'; base-uri 'none'; object-src 'none'; " +
|
|
254
289
|
"frame-ancestors 'none'; form-action 'none'; script-src 'self'; " +
|
|
255
290
|
"style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'none'; " +
|
|
@@ -258,8 +293,7 @@ export const CONTENT_SECURITY_POLICY_BASELINE =
|
|
|
258
293
|
/**
|
|
259
294
|
* Build the exact plain-object document `contracts/render-policy.jcs`
|
|
260
295
|
* publishes: `name`, `version` byte-equal to the template lock's
|
|
261
|
-
* `contractVersion`, a closed sanitizer/parser catalog, and no self-digest
|
|
262
|
-
* (DEC-097 section 5).
|
|
296
|
+
* `contractVersion`, a closed sanitizer/parser catalog, and no self-digest.
|
|
263
297
|
*
|
|
264
298
|
* @returns {Record<string, unknown>} the render-policy document
|
|
265
299
|
*/
|
|
@@ -267,7 +301,9 @@ export function renderPolicyDocument() {
|
|
|
267
301
|
return {
|
|
268
302
|
name: RENDER_POLICY_NAME,
|
|
269
303
|
version: RENDER_POLICY_VERSION,
|
|
270
|
-
contentSecurityPolicy:
|
|
304
|
+
contentSecurityPolicy: CONTENT_SECURITY_POLICY_COMPLETE,
|
|
305
|
+
contentSecurityPolicyMeta: CONTENT_SECURITY_POLICY_META_BASELINE,
|
|
306
|
+
contentSecurityPolicyHeaderOnlyDirectives: [...CSP_HEADER_ONLY_DIRECTIVES],
|
|
271
307
|
markdownIt: {
|
|
272
308
|
package: MARKDOWN_IT_PACKAGE,
|
|
273
309
|
version: MARKDOWN_IT_VERSION,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Deterministic route-segment derivation for generated listing pages (tag,
|
|
3
|
-
* series, archive) from an authored `plainLabel
|
|
4
|
-
* navigation renderer/page kinds).
|
|
3
|
+
* series, archive) from an authored `plainLabel`.
|
|
5
4
|
*
|
|
6
5
|
* `contentFrontmatterNormalized.tags`/`.series` are `plainLabel` values:
|
|
7
6
|
* normalized Unicode text, 1..80 graphemes, with no further route-safety
|
|
@@ -12,12 +11,12 @@
|
|
|
12
11
|
* or series label can therefore contain spaces, uppercase letters, or any
|
|
13
12
|
* non-Latin script.
|
|
14
13
|
*
|
|
15
|
-
* `route.js` already documents that this renderer's route projection
|
|
16
|
-
* deliberately
|
|
14
|
+
* `route.js` already documents that this renderer's route projection
|
|
15
|
+
* deliberately excludes a full Unicode percent-encoding and
|
|
17
16
|
* collision-resolution engine (out of scope; it only ever projects
|
|
18
17
|
* already-schema-validated `canonicalRoute` values, never derives a route
|
|
19
|
-
* from an arbitrary label). This module makes the same scoped choice for
|
|
20
|
-
* one new case
|
|
18
|
+
* from an arbitrary label). This module makes the same scoped choice for
|
|
19
|
+
* the one new case it introduces — turning an arbitrary label into a route
|
|
21
20
|
* segment — and closes the collision risk a lossy ASCII-only slug would
|
|
22
21
|
* otherwise introduce (two distinct Unicode labels both losing all their
|
|
23
22
|
* characters to filtering) by always appending a short, deterministic content
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Route-to-output-path projection and the byte-exact generated redirect
|
|
3
|
-
* document
|
|
4
|
-
* section 6 artifact-manifest redirect subsection).
|
|
3
|
+
* document.
|
|
5
4
|
*
|
|
6
5
|
* This module implements the `directory-index` and `explicit-file` cases of
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* already-schema-validated `canonicalRoute` field in `build-input`).
|
|
6
|
+
* route projection only for routes that are already valid `canonicalRoute`
|
|
7
|
+
* strings (schema-enforced before this module ever runs). A full Unicode
|
|
8
|
+
* percent-encoding/collision engine for arbitrary repository-relative
|
|
9
|
+
* source paths is out of scope here: this renderer never derives routes
|
|
10
|
+
* from source paths — every route it projects comes from an
|
|
11
|
+
* already-schema-validated `canonicalRoute` field in `build-input`.
|
|
14
12
|
*/
|
|
15
13
|
|
|
16
14
|
/**
|
|
@@ -35,6 +33,48 @@ export function joinBasePathAndRoute(basePath, route) {
|
|
|
35
33
|
return `/${segments.join('/')}`;
|
|
36
34
|
}
|
|
37
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Derive the public base path every emitted URL must carry from the build
|
|
38
|
+
* input: the path component of `baseUrl` (for example `/g9/` for a GitHub
|
|
39
|
+
* project site published at `https://owner.github.io/g9/`) joined with the
|
|
40
|
+
* build input's own `basePath`. Only emitted URLs (hrefs, `src`, canonical,
|
|
41
|
+
* feed, sitemap and search-index URLs) use this value; candidate-directory
|
|
42
|
+
* output file paths keep using `basePath` alone, because the artifact is
|
|
43
|
+
* deployed at the root of the hosting site.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} baseUrl the build input's validated `baseUrl`
|
|
46
|
+
* @param {string} basePath the build input's `basePath`
|
|
47
|
+
* @returns {string} the public base path with a leading slash and no double
|
|
48
|
+
* slashes. When `baseUrl` carries a path it also has a trailing slash
|
|
49
|
+
* (`/g9/`); with an origin-only `baseUrl` the result is exactly
|
|
50
|
+
* `basePath`'s legacy joined form (`/` or `/blog`), so origin-only output
|
|
51
|
+
* is byte-identical to earlier releases.
|
|
52
|
+
*/
|
|
53
|
+
export function derivePublicBasePath(baseUrl, basePath) {
|
|
54
|
+
const urlPath = new URL(baseUrl).pathname;
|
|
55
|
+
const joined = joinBasePathAndRoute(
|
|
56
|
+
joinBasePathAndRoute(urlPath, basePath),
|
|
57
|
+
'/',
|
|
58
|
+
);
|
|
59
|
+
return joined !== '/' && urlPath !== '/' ? `${joined}/` : joined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Join a public base path (see {@link derivePublicBasePath}) and a route
|
|
64
|
+
* into one emitted URL path. The site root route is the public base path
|
|
65
|
+
* itself (`/site/` when `baseUrl` carries a path); every other route is
|
|
66
|
+
* joined exactly like {@link joinBasePathAndRoute}.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} publicBasePath the public base path
|
|
69
|
+
* @param {string} route a `canonicalRoute`
|
|
70
|
+
* @returns {string} the emitted absolute URL path
|
|
71
|
+
*/
|
|
72
|
+
export function joinPublicRoute(publicBasePath, route) {
|
|
73
|
+
return route === '/'
|
|
74
|
+
? publicBasePath
|
|
75
|
+
: joinBasePathAndRoute(publicBasePath, route);
|
|
76
|
+
}
|
|
77
|
+
|
|
38
78
|
/**
|
|
39
79
|
* Project one already-joined absolute route to its candidate-directory-
|
|
40
80
|
* relative output file path.
|
|
@@ -75,16 +115,14 @@ export function projectFilePathToRoute(filePath, profile) {
|
|
|
75
115
|
}
|
|
76
116
|
|
|
77
117
|
/**
|
|
78
|
-
* Project a `basePath` to the one fixed generated error-document output
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* the selected base path"; DEC-097's Spaces/Pages/local-directory adapters
|
|
87
|
-
* all bind their provider error-document configuration to this exact key).
|
|
118
|
+
* Project a `basePath` to the one fixed generated error-document output
|
|
119
|
+
* path: exactly `404.html` when `basePath` is `/`; otherwise the base path
|
|
120
|
+
* with its one leading and terminal slash removed, followed by
|
|
121
|
+
* `/404.html`. This is deliberately independent of the selected
|
|
122
|
+
* `RouteNormalizationProfile` (`directory-index` vs `explicit-file`): the
|
|
123
|
+
* generated error document is always this one literal filename, never
|
|
124
|
+
* route-normalized like an ordinary content route — this fixed key is what
|
|
125
|
+
* each hosting adapter's provider error-document configuration binds to.
|
|
88
126
|
*
|
|
89
127
|
* @param {string} basePath the build input's `basePath` (`canonicalRoute`,
|
|
90
128
|
* always starts with `/`)
|
|
@@ -143,7 +181,7 @@ export function escapeRedirectTarget(targetRoute) {
|
|
|
143
181
|
|
|
144
182
|
/**
|
|
145
183
|
* Render the exact UTF-8, no-BOM, LF-terminated generated redirect document
|
|
146
|
-
*
|
|
184
|
+
* this renderer must produce byte-for-byte.
|
|
147
185
|
*
|
|
148
186
|
* @param {string} targetRoute the route this redirect document points to
|
|
149
187
|
* @returns {string} the complete document text
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Static search index generation
|
|
3
|
-
* "Required generated outputs": "static search index").
|
|
2
|
+
* Static search index generation.
|
|
4
3
|
*
|
|
5
|
-
* Scope note (documented, not silently assumed):
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* matching this renderer's established style for an unfixed structural
|
|
4
|
+
* Scope note (documented, not silently assumed): this renderer has no
|
|
5
|
+
* externally fixed search-index document shape to conform to. This module
|
|
6
|
+
* therefore documents its own deterministic shape here, matching this
|
|
7
|
+
* renderer's established style for an unfixed structural
|
|
10
8
|
* decision (see `internal/route-labels.js`'s route-segment grammar and the
|
|
11
9
|
* media pipeline scope decisions in the package README): one JSON document,
|
|
12
10
|
* `{schemaVersion, generatedAt, publication, documents[]}`, sorted by each
|
|
@@ -15,17 +13,17 @@
|
|
|
15
13
|
* `internal/source-inventory.js#compareUtf8Bytes`).
|
|
16
14
|
*
|
|
17
15
|
* `generatedAt` is `buildInput.buildEpoch` — a build-input *field*, sourced
|
|
18
|
-
* deterministically from the selected Git commit's own committer timestamp
|
|
19
|
-
*
|
|
16
|
+
* deterministically from the selected Git commit's own committer timestamp,
|
|
17
|
+
* not read from the wall clock at build time — matching
|
|
20
18
|
* `src/core/manifest.js`'s own `manifest.generatedAt` convention, so two
|
|
21
19
|
* clean builds of the same `build-input` still produce byte-identical index
|
|
22
20
|
* bytes.
|
|
23
21
|
*
|
|
24
|
-
* No runtime search JavaScript accompanies this document
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
22
|
+
* No runtime search JavaScript accompanies this document: this renderer
|
|
23
|
+
* must read completely with JavaScript disabled, and the appearance
|
|
24
|
+
* controller is the only client-side script it ever admits. This module's
|
|
25
|
+
* only output is the index document's bytes; nothing wires a `<script>` to
|
|
26
|
+
* it.
|
|
29
27
|
*
|
|
30
28
|
* Only actual authored content (`article`/`page` records, `status:
|
|
31
29
|
* "published"`) is indexed — never a synthetic listing page
|
|
@@ -34,7 +32,7 @@
|
|
|
34
32
|
* full-text search.
|
|
35
33
|
*/
|
|
36
34
|
|
|
37
|
-
import {
|
|
35
|
+
import { derivePublicBasePath, joinPublicRoute } from './route.js';
|
|
38
36
|
import { compareUtf8Bytes } from './source-inventory.js';
|
|
39
37
|
import { contentLastModified, contentRoute } from './page-kinds.js';
|
|
40
38
|
|
|
@@ -79,7 +77,10 @@ export function buildSearchIndex(validatedInput) {
|
|
|
79
77
|
* @returns {string} the absolute `https://` URL
|
|
80
78
|
*/
|
|
81
79
|
const absoluteUrl = (route) =>
|
|
82
|
-
new URL(
|
|
80
|
+
new URL(
|
|
81
|
+
joinPublicRoute(derivePublicBasePath(baseUrl, basePath), route),
|
|
82
|
+
baseUrl,
|
|
83
|
+
).toString();
|
|
83
84
|
|
|
84
85
|
const documents = content
|
|
85
86
|
.filter(
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A closed subset of npm-compatible SemVer range satisfaction (TPL-H1), used
|
|
3
|
+
* only to check a theme package's own declared `theme.json.templateRange`
|
|
4
|
+
* against this renderer's published version. This is deliberately not a
|
|
5
|
+
* general-purpose SemVer range engine: `@rathnasgala2/schemas`'
|
|
6
|
+
* `theme-contract.schema.json` `templateRange` field (`$defs/semverRange`)
|
|
7
|
+
* admits an open-ended npm range grammar, but every legitimate value this
|
|
8
|
+
* closed-contract ecosystem actually emits is either an exact version or a
|
|
9
|
+
* caret range — the same two forms `package.json` dependency pins in this
|
|
10
|
+
* very repository use — so this module supports exactly those two forms and
|
|
11
|
+
* fails closed (rejects) on anything else, exactly like every other closed
|
|
12
|
+
* catalog in this repository (`nthExpressionProfile`, the pseudo-class
|
|
13
|
+
* catalog, `functionalSelectorArguments`, ...).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const EXACT_VERSION_PATTERN = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
17
|
+
const CARET_RANGE_PATTERN = /^\^(\d+)\.(\d+)\.(\d+)$/;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} version a canonical `major.minor.patch` SemVer string
|
|
21
|
+
* @returns {[number, number, number]} the parsed components
|
|
22
|
+
*/
|
|
23
|
+
function parseExactVersion(version) {
|
|
24
|
+
const match = EXACT_VERSION_PATTERN.exec(version);
|
|
25
|
+
if (!match) {
|
|
26
|
+
throw new Error(`not a canonical major.minor.patch version: ${version}`);
|
|
27
|
+
}
|
|
28
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {[number, number, number]} a
|
|
33
|
+
* @param {[number, number, number]} b
|
|
34
|
+
* @returns {number} negative, zero or positive per standard comparator
|
|
35
|
+
* contract
|
|
36
|
+
*/
|
|
37
|
+
function compareVersions(a, b) {
|
|
38
|
+
for (let index = 0; index < 3; index += 1) {
|
|
39
|
+
if (a[index] !== b[index]) return a[index] - b[index];
|
|
40
|
+
}
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Whether `version` satisfies `range`, supporting only an exact
|
|
46
|
+
* `major.minor.patch` range (equality) or a caret range `^major.minor.patch`
|
|
47
|
+
* (matches `>=major.minor.patch <(major+1).0.0`, or, for a `0.x.y` caret
|
|
48
|
+
* range, npm's own tightened `0.x` semantics: `>=0.minor.patch <0.(minor+1).0`).
|
|
49
|
+
*
|
|
50
|
+
* @param {string} version this renderer's own exact published version
|
|
51
|
+
* @param {string} range the theme's declared `templateRange`
|
|
52
|
+
* @returns {boolean} whether `version` satisfies `range`
|
|
53
|
+
* @throws {Error} when `range` is not one of the two closed forms this
|
|
54
|
+
* module admits
|
|
55
|
+
*/
|
|
56
|
+
export function satisfiesTemplateRange(version, range) {
|
|
57
|
+
const parsedVersion = parseExactVersion(version);
|
|
58
|
+
const exactMatch = EXACT_VERSION_PATTERN.exec(range);
|
|
59
|
+
if (exactMatch) {
|
|
60
|
+
return compareVersions(parsedVersion, parseExactVersion(range)) === 0;
|
|
61
|
+
}
|
|
62
|
+
const caretMatch = CARET_RANGE_PATTERN.exec(range);
|
|
63
|
+
if (caretMatch) {
|
|
64
|
+
const lower = /** @type {[number, number, number]} */ ([
|
|
65
|
+
Number(caretMatch[1]),
|
|
66
|
+
Number(caretMatch[2]),
|
|
67
|
+
Number(caretMatch[3]),
|
|
68
|
+
]);
|
|
69
|
+
if (compareVersions(parsedVersion, lower) < 0) return false;
|
|
70
|
+
const [major, minor] = lower;
|
|
71
|
+
if (major > 0) {
|
|
72
|
+
return parsedVersion[0] === major;
|
|
73
|
+
}
|
|
74
|
+
if (minor > 0) {
|
|
75
|
+
return parsedVersion[0] === 0 && parsedVersion[1] === minor;
|
|
76
|
+
}
|
|
77
|
+
// ^0.0.z: npm pins this to the exact patch version.
|
|
78
|
+
return compareVersions(parsedVersion, lower) === 0;
|
|
79
|
+
}
|
|
80
|
+
throw new Error(
|
|
81
|
+
`templateRange must be an exact "major.minor.patch" version or a ` +
|
|
82
|
+
`"^major.minor.patch" caret range, got ${JSON.stringify(range)}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
package/src/core/internal/seo.js
CHANGED
|
@@ -1,28 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SEO/localization metadata assembly
|
|
3
|
-
*
|
|
4
|
-
* localization metadata, SEO metadata").
|
|
2
|
+
* SEO/localization metadata assembly: canonical share and copy affordances,
|
|
3
|
+
* localization metadata, SEO metadata.
|
|
5
4
|
*
|
|
6
5
|
* This module resolves the per-page facts `internal/page-kinds.js` already
|
|
7
6
|
* decided (`description`, `ogType`, `socialImageRef`, `robotsContent`) into
|
|
8
7
|
* the concrete absolute-URL strings a page's `<head>` needs. It is kept
|
|
9
8
|
* separate from `page-kinds.js` because resolving a `socialImageRef` into an
|
|
10
9
|
* absolute derivative URL needs two facts only `src/core/index.js` has at
|
|
11
|
-
* that point: the
|
|
12
|
-
*
|
|
10
|
+
* that point: the media pipeline's own finished `assets` list (so the exact
|
|
11
|
+
* derivative file extension a given source image produced is known —
|
|
13
12
|
* `page-kinds.js` never decodes an image) and `buildInput.baseUrl` (the
|
|
14
13
|
* origin every absolute URL this renderer emits is resolved against).
|
|
15
14
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
15
|
+
* basePath fix: every derivative-asset URL this module resolves is
|
|
16
|
+
* `basePath`-joined through `internal/route.js`'s shared
|
|
17
|
+
* {@link import('./route.js').joinBasePathAndRoute} before being resolved
|
|
18
|
+
* against `baseUrl`, exactly like every other renderer-derived route
|
|
19
|
+
* (`internal/page-kinds.js`, `internal/print-stylesheet.js`). Without this,
|
|
20
|
+
* {@link resolveSocialImageUrl} would join a bare root-absolute
|
|
22
21
|
* `/${derivativePath}` against `baseUrl`, which 404s under a non-root
|
|
23
|
-
* `basePath` publication (`internal/theme-assets.js`
|
|
24
|
-
*
|
|
25
|
-
* hrefs).
|
|
22
|
+
* `basePath` publication (`internal/theme-assets.js` applies the equivalent
|
|
23
|
+
* fix for the theme/print stylesheet and appearance-script hrefs).
|
|
26
24
|
*
|
|
27
25
|
* hreflang note (documented scope decision, not a silent omission):
|
|
28
26
|
* `build-input:2.0.0` has no translation/locale-variant linkage between
|
|
@@ -31,11 +29,11 @@
|
|
|
31
29
|
* `$defs` entry named anything like `translation`/`localizedContent`/
|
|
32
30
|
* `alternateLanguage` exists anywhere in the schema; `authorNormalized`'s own
|
|
33
31
|
* `localized` array carries only a translated author bio, never a route). A
|
|
34
|
-
* page's own `<html lang>` already carries its resolved language
|
|
35
|
-
*
|
|
36
|
-
* `<link rel="alternate" hreflang="...">` is therefore correctly *empty*
|
|
37
|
-
*
|
|
38
|
-
* input declares translations
|
|
32
|
+
* page's own `<html lang>` already carries its resolved language, which is
|
|
33
|
+
* the entire localization signal this build-input shape declares.
|
|
34
|
+
* `<link rel="alternate" hreflang="...">` is therefore correctly *empty*
|
|
35
|
+
* here — the requirement to emit hreflang/localization metadata where the
|
|
36
|
+
* input declares translations is satisfied vacuously, since the input never
|
|
39
37
|
* declares one. This module has no `hreflang` export for that reason; a
|
|
40
38
|
* later build-input major version that adds translation linkage is the
|
|
41
39
|
* natural place to add one, at which point every page-kind call site below
|
|
@@ -52,14 +50,14 @@ import { joinBasePathAndRoute } from './route.js';
|
|
|
52
50
|
* know this path shape, so a change to either stays a one-place fix).
|
|
53
51
|
*
|
|
54
52
|
* @param {readonly {path: string, mediaType: string}[]} mediaAssets the
|
|
55
|
-
*
|
|
53
|
+
* media pipeline's own finished `assets` list
|
|
56
54
|
* @param {{path: string, sourceDigest: string} | undefined} reference a
|
|
57
55
|
* `resolvedFile`-shaped reference (`publication.defaultImage`,
|
|
58
56
|
* `author.avatar`, `frontmatter.socialImage`, `frontmatter.hero.file`)
|
|
59
57
|
* @returns {string | undefined} the derivative asset's own output-relative
|
|
60
58
|
* path (never the raw source path), or `undefined` when no reference was
|
|
61
59
|
* supplied or the media pipeline produced no matching derivative (an image
|
|
62
|
-
* this renderer's own
|
|
60
|
+
* this renderer's own media pipeline was never asked to process, e.g. a
|
|
63
61
|
* test fixture exercising SEO metadata without the media pipeline)
|
|
64
62
|
*/
|
|
65
63
|
export function resolveOriginalDerivativePath(mediaAssets, reference) {
|
|
@@ -77,11 +75,13 @@ export function resolveOriginalDerivativePath(mediaAssets, reference) {
|
|
|
77
75
|
*
|
|
78
76
|
* @param {object} options resolution options
|
|
79
77
|
* @param {readonly {path: string, mediaType: string}[]} options.mediaAssets
|
|
80
|
-
* the
|
|
78
|
+
* the media pipeline's own finished `assets` list
|
|
81
79
|
* @param {{path: string, sourceDigest: string} | undefined} options.reference
|
|
82
80
|
* the candidate `resolvedFile` reference
|
|
83
|
-
* @param {string} options.baseUrl the build input's
|
|
84
|
-
* @param {string} options.basePath the
|
|
81
|
+
* @param {string} options.baseUrl the build input's `baseUrl`
|
|
82
|
+
* @param {string} options.basePath the public base path
|
|
83
|
+
* (`route.js#derivePublicBasePath`, the `baseUrl` path joined with the
|
|
84
|
+
* build input's `basePath`; the
|
|
85
85
|
* derivative path is joined with this before being resolved against
|
|
86
86
|
* `baseUrl`, so a non-root publication's social-image URL is correct)
|
|
87
87
|
* @returns {string | undefined} the absolute derivative image URL, or
|
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sitemap generation
|
|
3
|
-
*
|
|
4
|
-
* only, deterministic order, `lastmod` from content timestamps not build
|
|
5
|
-
* time").
|
|
2
|
+
* Sitemap generation: published routes only, deterministic order, with
|
|
3
|
+
* `lastmod` derived from content timestamps rather than build time.
|
|
6
4
|
*
|
|
7
5
|
* Every entry comes from `internal/page-kinds.js`'s own
|
|
8
6
|
* {@link import('./page-kinds.js').GeneratedPage} list — the single source
|
|
9
7
|
* of truth for which page kinds exist and what each one's own
|
|
10
8
|
* `robotsContent`/`lastModified` is — filtered to exclude any page this
|
|
11
|
-
* renderer itself marked `noindex` (
|
|
12
|
-
*
|
|
9
|
+
* renderer itself marked `noindex` (robots handling for unlisted content:
|
|
10
|
+
* `status: "unlisted"` content is exactly the case
|
|
13
11
|
* `internal/page-kinds.js` sets `robotsContent` for). The generated `404`
|
|
14
12
|
* error page, the feeds, the sitemap document itself and the static search
|
|
15
13
|
* index are never `GeneratedPage` entries, so they are structurally excluded
|
|
16
14
|
* without a second exclusion list. A `manifestRedirect` route is likewise
|
|
17
15
|
* never in this list; a redirect's fixed byte-for-byte document already
|
|
18
|
-
* carries its own `<link rel="canonical">` to the real content page
|
|
19
|
-
*
|
|
16
|
+
* carries its own `<link rel="canonical">` to the real content page, which
|
|
17
|
+
* is what a crawler should index instead.
|
|
20
18
|
*/
|
|
21
19
|
|
|
22
20
|
import { escapeHtml } from './skeleton.js';
|
|
23
|
-
import {
|
|
21
|
+
import { joinPublicRoute } from './route.js';
|
|
24
22
|
import { compareUtf8Bytes } from './source-inventory.js';
|
|
25
23
|
|
|
26
24
|
/**
|
|
@@ -39,8 +37,9 @@ function isNoindex(page) {
|
|
|
39
37
|
* @param {object} options build options
|
|
40
38
|
* @param {readonly import('./page-kinds.js').GeneratedPage[]} options.generatedPages
|
|
41
39
|
* every page `internal/page-kinds.js` produced
|
|
42
|
-
* @param {string} options.basePath the
|
|
43
|
-
*
|
|
40
|
+
* @param {string} options.basePath the public base path
|
|
41
|
+
* (`route.js#derivePublicBasePath`)
|
|
42
|
+
* @param {string} options.baseUrl the build input's `baseUrl`
|
|
44
43
|
* @returns {string} the exact UTF-8 `sitemap.xml` text
|
|
45
44
|
*/
|
|
46
45
|
export function buildSitemap({ generatedPages, basePath, baseUrl }) {
|
|
@@ -49,7 +48,7 @@ export function buildSitemap({ generatedPages, basePath, baseUrl }) {
|
|
|
49
48
|
* @returns {string} the absolute `https://` URL
|
|
50
49
|
*/
|
|
51
50
|
const absoluteUrl = (route) =>
|
|
52
|
-
new URL(
|
|
51
|
+
new URL(joinPublicRoute(basePath, route), baseUrl).toString();
|
|
53
52
|
|
|
54
53
|
const entries = generatedPages
|
|
55
54
|
.filter((page) => !isNoindex(page))
|