@rathnasgala2/template 2.0.0 → 2.1.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 +204 -137
- package/contracts/render-policy.jcs +1 -1
- package/contracts/theme-styling-contract.jcs +1 -1
- package/package.json +4 -5
- package/src/core/errors.js +13 -13
- package/src/core/index.js +102 -66
- 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 +10 -13
- 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 +46 -48
- 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 +16 -20
- package/src/core/internal/search-index.js +12 -14
- package/src/core/internal/semver-range.js +84 -0
- package/src/core/internal/seo.js +21 -23
- package/src/core/internal/sitemap.js +6 -8
- package/src/core/internal/skeleton.js +42 -39
- 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 +294 -67
- package/src/core/internal/uuid.js +4 -5
- package/src/core/manifest.js +0 -0
- package/types/index.d.ts +22 -8
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,11 @@ 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
81
|
* @param {string} options.baseUrl the build input's origin-only `baseUrl`
|
|
84
|
-
* @param {string} options.basePath the build input's `basePath` (
|
|
82
|
+
* @param {string} options.basePath the build input's `basePath` (the
|
|
85
83
|
* derivative path is joined with this before being resolved against
|
|
86
84
|
* `baseUrl`, so a non-root publication's social-image URL is correct)
|
|
87
85
|
* @returns {string | undefined} the absolute derivative image URL, or
|
|
@@ -1,22 +1,20 @@
|
|
|
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';
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Core semantic HTML skeleton and navigation renderer
|
|
3
|
-
* brief S2 section 3 "Semantic skeleton and appearance controller").
|
|
2
|
+
* Core semantic HTML skeleton and navigation renderer.
|
|
4
3
|
*
|
|
5
4
|
* This module renders only inert, static, CSP-`script-src 'self'`-compatible
|
|
6
5
|
* markup: no inline `on*` attribute, no inline `<script>`, no `javascript:`
|
|
7
6
|
* URL. Every user-visible chrome string it emits comes from
|
|
8
7
|
* `internal/messages.js`'s catalog, never an inline literal, so localization
|
|
9
|
-
* can find every string in one place
|
|
8
|
+
* can find every string in one place.
|
|
10
9
|
*
|
|
11
10
|
* Landmark contract every page kind in `internal/page-kinds.js` composes
|
|
12
11
|
* through {@link renderPageBody}:
|
|
@@ -25,43 +24,43 @@
|
|
|
25
24
|
* `conversation`, `newsletter`, `edition-selector`).
|
|
26
25
|
*
|
|
27
26
|
* Core owns nine versioned semantic slots plus the collapsed
|
|
28
|
-
* `article-footer-ad` slot
|
|
27
|
+
* `article-footer-ad` slot: `header-actions`, `article-preamble`,
|
|
29
28
|
* `article-end`, `footer-profile`, `footer-auxiliary`, `account-intent`,
|
|
30
|
-
* `conversation`, `newsletter` and `edition-selector`.
|
|
31
|
-
* system (`build-input.modules` is always `{}`), so every slot
|
|
32
|
-
* except `footer-profile`, which always wraps the
|
|
33
|
-
* always-rendered footer card/author-profile content
|
|
34
|
-
* module content to distinguish it from
|
|
35
|
-
* `data-gala-slot="<name>"` attribute on an otherwise plain,
|
|
36
|
-
* `<div>` — an empty element with no accessible name
|
|
37
|
-
* the accessibility tree, so an empty slot is
|
|
38
|
-
* does not exist. The collapsed
|
|
39
|
-
* carries the `hidden` attribute, so
|
|
40
|
-
* layout, network or telemetry
|
|
41
|
-
*
|
|
42
|
-
* exist
|
|
29
|
+
* `conversation`, `newsletter` and `edition-selector`. This renderer has no
|
|
30
|
+
* module system (`build-input.modules` is always `{}`), so every slot
|
|
31
|
+
* renders empty except `footer-profile`, which always wraps the
|
|
32
|
+
* publication's own always-rendered footer card/author-profile content
|
|
33
|
+
* (there is no separate module content to distinguish it from here). Each
|
|
34
|
+
* slot is one `data-gala-slot="<name>"` attribute on an otherwise plain,
|
|
35
|
+
* non-landmark `<div>` — an empty element with no accessible name
|
|
36
|
+
* contributes nothing to the accessibility tree, so an empty slot is
|
|
37
|
+
* exactly as inert as one that does not exist. The collapsed
|
|
38
|
+
* `article-footer-ad` slot additionally carries the `hidden` attribute, so
|
|
39
|
+
* it produces no visible, accessibility, layout, network or telemetry
|
|
40
|
+
* effect at all until a later module (not built here) removes `hidden` for
|
|
41
|
+
* a selected placement — none exist here (`build-input.placements` is
|
|
42
|
+
* always `[]`).
|
|
43
43
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
44
|
+
* `header-actions` is not always-empty: it is core's own home for the
|
|
45
|
+
* Light/Dark/System appearance control (the three-mode appearance control
|
|
46
|
+
* in the header-actions slot) — core content, not a module, so this slot's
|
|
47
|
+
* earlier "every slot ... renders empty" description does not cover it.
|
|
48
|
+
* Every other slot is unaffected.
|
|
49
49
|
*
|
|
50
50
|
* The exact `selectorAtom`/hook-naming decisions here (`data-gala-slot`
|
|
51
|
-
* attribute, one slot name per value) are
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* decision here rather than pre-authoring that contract file itself.
|
|
51
|
+
* attribute, one slot name per value) are what
|
|
52
|
+
* `contracts/theme-styling-contract.jcs` later locks into its published
|
|
53
|
+
* catalog, so this module documents the decision here rather than
|
|
54
|
+
* pre-authoring that contract file itself.
|
|
56
55
|
*/
|
|
57
56
|
|
|
58
57
|
/**
|
|
59
58
|
* Every `data-gala-slot` value this renderer ever writes (the nine
|
|
60
|
-
* versioned core slots plus the collapsed `article-footer-ad` slot,
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
59
|
+
* versioned core slots plus the collapsed `article-footer-ad` slot), sorted
|
|
60
|
+
* by UTF-8 bytes. `contracts/theme-styling-contract.jcs` exposes one public
|
|
61
|
+
* theme hook per entry here, so this is the single source of truth both
|
|
62
|
+
* `renderSlot` below and that contract builder read from — never a second
|
|
63
|
+
* hand-typed literal list.
|
|
65
64
|
*
|
|
66
65
|
* @type {readonly string[]}
|
|
67
66
|
*/
|
|
@@ -111,8 +110,8 @@ export function escapeHtml(value) {
|
|
|
111
110
|
* collapsed `article-footer-ad` slot
|
|
112
111
|
* @param {object} [options] rendering options
|
|
113
112
|
* @param {boolean} [options.collapsed] when `true`, additionally sets
|
|
114
|
-
* `hidden` (
|
|
115
|
-
* @param {string} [options.html] already-rendered inner markup (
|
|
113
|
+
* `hidden` (the collapsed `article-footer-ad` slot)
|
|
114
|
+
* @param {string} [options.html] already-rendered inner markup (the
|
|
116
115
|
* `header-actions` slot's appearance control); every other slot omits this
|
|
117
116
|
* and stays empty
|
|
118
117
|
* @returns {string} the slot element
|
|
@@ -171,9 +170,13 @@ function renderNavItem(item, currentRoute) {
|
|
|
171
170
|
* route
|
|
172
171
|
* @param {Readonly<Record<string, string | ((...args: string[]) => string)>>} options.messages
|
|
173
172
|
* the resolved message catalog
|
|
174
|
-
* @returns {string} the rendered primary navigation
|
|
175
|
-
*
|
|
176
|
-
*
|
|
173
|
+
* @returns {string} the rendered primary navigation `<nav>`, always present
|
|
174
|
+
* even when `items` is empty (TPL-M3): every page kind renders exactly
|
|
175
|
+
* one primary-navigation landmark unconditionally
|
|
176
|
+
* (`internal/page-kinds.js`'s own module documentation), so a reader
|
|
177
|
+
* navigating by landmark list sees the same, predictable chrome structure
|
|
178
|
+
* on every page regardless of whether that publication has authored any
|
|
179
|
+
* navigation items yet.
|
|
177
180
|
*/
|
|
178
181
|
export function renderPrimaryNavigation({ items, currentRoute, messages }) {
|
|
179
182
|
const label = escapeHtml(
|
|
@@ -306,8 +309,8 @@ export function renderPagination({
|
|
|
306
309
|
* @param {{route: string, alt: string} | undefined} options.logo an optional
|
|
307
310
|
* core logo image, always carrying alternative text
|
|
308
311
|
* @param {string} [options.appearanceControlHtml] the already-rendered
|
|
309
|
-
* Light/Dark/System appearance control
|
|
310
|
-
* `internal/appearance/controller-markup.js`'s `renderAppearanceControl`),
|
|
312
|
+
* Light/Dark/System appearance control
|
|
313
|
+
* (`internal/appearance/controller-markup.js`'s `renderAppearanceControl`),
|
|
311
314
|
* inserted into the `header-actions` slot
|
|
312
315
|
* @returns {string} the rendered header
|
|
313
316
|
*/
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Collect the distinct source records this renderer actually consumed from
|
|
3
|
-
* a validated `build-input:2.0.0` instance, as
|
|
4
|
-
* `
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* and the role is the earliest value in the enum order").
|
|
3
|
+
* a validated `build-input:2.0.0` instance, as `manifestIncludedSource[]`
|
|
4
|
+
* rows: `includedSources` has set equality with the distinct source records
|
|
5
|
+
* placed in the verified-input carrier, a file used by multiple records
|
|
6
|
+
* appears once, and its role is the earliest value in the enum order.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
/** @type {readonly string[]} */
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
* against `node_modules/@rathnasgala2/schemas/schemas/build-input.schema.json`:
|
|
8
8
|
* no `$defs` entry or property named `dir`/`direction` exists anywhere in the
|
|
9
9
|
* schema). The semantic skeleton must still set an accurate `dir` attribute
|
|
10
|
-
*
|
|
11
|
-
* acceptance test A's golden fixture), so this module derives it from the
|
|
10
|
+
* even for localized content with mixed direction, so this module derives it from the
|
|
12
11
|
* tag's primary language subtag against the closed set of right-to-left
|
|
13
12
|
* script languages in Unicode CLDR's exemplar set — the same approach HTML's
|
|
14
13
|
* own `Intl.Locale` `textInfo.direction` uses, reimplemented here as a small
|