@rathnasgala2/template 2.1.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 +7 -0
- package/package.json +2 -2
- package/src/core/index.js +32 -13
- package/src/core/internal/feeds.js +5 -2
- package/src/core/internal/page-kinds.js +4 -3
- package/src/core/internal/route.js +42 -0
- package/src/core/internal/search-index.js +5 -2
- package/src/core/internal/seo.js +4 -2
- package/src/core/internal/sitemap.js +5 -4
- package/src/core/internal/skeleton.js +23 -7
- package/src/core/internal/theme-assets.js +21 -2
package/README.md
CHANGED
|
@@ -354,6 +354,13 @@ allowlist: raster formats are admitted directly, and SVG is admitted only after
|
|
|
354
354
|
in scope and expected** (TPL-H6 decision): this sanitizer is what makes a
|
|
355
355
|
theme-declared SVG icon mark safe to publish.
|
|
356
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.
|
|
357
364
|
- **basePath fix (LOCAL-7 follow-up; independent-review finding B2).** Before
|
|
358
365
|
theme integration existed, the appearance bootstrap script's `<script src>`
|
|
359
366
|
and the social-image `og:image`/`twitter:image` URL were both root-absolute,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rathnasgala2/template",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Deterministic Eleventy-backed static publication renderer for Galascribe portable repositories.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"workflows:check": "node scripts/check-workflow-pins.mjs",
|
|
66
66
|
"workflows:drift": "node scripts/check-workflow-drift.mjs",
|
|
67
67
|
"schema-pin:check": "node scripts/check-no-local-schema-pin.mjs",
|
|
68
|
-
"audit": "npm
|
|
68
|
+
"audit": "node scripts/npm-audit.mjs",
|
|
69
69
|
"verify": "npm run format:check && npm run lint && npm run typecheck && npm run architecture && npm run duplication && npm run contracts:check && npm run schema-pin:check && npm test && npm run licenses:check && npm run sbom:check && npm run workflows:check && npm run audit"
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
package/src/core/index.js
CHANGED
|
@@ -100,7 +100,9 @@ import {
|
|
|
100
100
|
} from './internal/page-kinds.js';
|
|
101
101
|
import { assertProvenance } from './internal/provenance.js';
|
|
102
102
|
import {
|
|
103
|
+
derivePublicBasePath,
|
|
103
104
|
errorDocumentPath,
|
|
105
|
+
joinPublicRoute,
|
|
104
106
|
joinBasePathAndRoute,
|
|
105
107
|
projectFixedAssetPath,
|
|
106
108
|
projectRouteToFilePath,
|
|
@@ -322,7 +324,14 @@ export async function renderPublication(buildInput, options) {
|
|
|
322
324
|
// that navigation/footer text does not flip language from page to page
|
|
323
325
|
// (see `internal/messages.js`).
|
|
324
326
|
const siteMessages = getMessages(validatedInput.publication.defaultLanguage);
|
|
325
|
-
|
|
327
|
+
// Every emitted URL carries the `baseUrl` path (for example `/g9/` on a
|
|
328
|
+
// GitHub project site); output file paths keep using `basePath` alone.
|
|
329
|
+
const publicBasePath = derivePublicBasePath(
|
|
330
|
+
validatedInput.baseUrl,
|
|
331
|
+
validatedInput.basePath,
|
|
332
|
+
);
|
|
333
|
+
const urlPrefix = derivePublicBasePath(validatedInput.baseUrl, '/');
|
|
334
|
+
const homeRoute = joinPublicRoute(publicBasePath, '/');
|
|
326
335
|
|
|
327
336
|
const appearanceControlHtml = renderAppearanceControl({
|
|
328
337
|
messages: siteMessages,
|
|
@@ -336,6 +345,7 @@ export async function renderPublication(buildInput, options) {
|
|
|
336
345
|
const footerNavHtml = renderFooterNavigation({
|
|
337
346
|
items: validatedInput.navigation.footerItems,
|
|
338
347
|
messages: siteMessages,
|
|
348
|
+
urlPrefix,
|
|
339
349
|
});
|
|
340
350
|
const footerCard = validatedInput.publication.footerCard;
|
|
341
351
|
// Already verified render-policy-conformant HTML by
|
|
@@ -368,17 +378,16 @@ export async function renderPublication(buildInput, options) {
|
|
|
368
378
|
// self URLs.
|
|
369
379
|
const feeds = buildFeeds(validatedInput);
|
|
370
380
|
const siteName = validatedInput.publication.title;
|
|
371
|
-
const appearanceScriptHrefValue =
|
|
372
|
-
|
|
373
|
-
);
|
|
381
|
+
const appearanceScriptHrefValue =
|
|
382
|
+
appearanceBootstrapScriptHref(publicBasePath);
|
|
374
383
|
// TPL-H3/TPL-M7: the template-owned gala-base stylesheet's own basePath-
|
|
375
384
|
// joined href, computed the same way as the appearance script href above
|
|
376
385
|
// so it can never drift from the physical file/manifest path written
|
|
377
386
|
// below.
|
|
378
|
-
const baseStylesheetHrefValue =
|
|
379
|
-
|
|
387
|
+
const baseStylesheetHrefValue = joinBasePathAndRoute(
|
|
388
|
+
publicBasePath,
|
|
380
389
|
`/${GALA_BASE_STYLESHEET_PATH}`,
|
|
381
|
-
)
|
|
390
|
+
);
|
|
382
391
|
|
|
383
392
|
// The selected theme package's copied stylesheets/passive assets and the
|
|
384
393
|
// ordered `<link>` markup every generated page's `<head>` inserts.
|
|
@@ -389,15 +398,16 @@ export async function renderPublication(buildInput, options) {
|
|
|
389
398
|
? await loadThemeAssets({
|
|
390
399
|
themeDirectory: path.resolve(options.themeDirectory),
|
|
391
400
|
basePath: validatedInput.basePath,
|
|
401
|
+
publicBasePath,
|
|
392
402
|
})
|
|
393
403
|
: undefined;
|
|
394
404
|
const themeStylesheetLinksHtml = themeAssetsResult
|
|
395
405
|
? themeAssetsResult.linkTagsHtml
|
|
396
|
-
: defaultThemeStylesheetLinksHtml(
|
|
406
|
+
: defaultThemeStylesheetLinksHtml(publicBasePath);
|
|
397
407
|
|
|
398
408
|
/** @type {{virtualPath: string, content: string, permalink: string, layout?: string, data?: Record<string, unknown>}[]} */
|
|
399
409
|
const contentPages = [];
|
|
400
|
-
/** @type {{joinedSource: string, joinedTarget: string, filePath: string}[]} */
|
|
410
|
+
/** @type {{joinedSource: string, joinedTarget: string, publicTarget: string, filePath: string}[]} */
|
|
401
411
|
const redirectProjections = [];
|
|
402
412
|
|
|
403
413
|
const generatedPages = buildGeneratedPages(validatedInput);
|
|
@@ -410,6 +420,7 @@ export async function renderPublication(buildInput, options) {
|
|
|
410
420
|
items: validatedInput.navigation.items,
|
|
411
421
|
currentRoute: joinedRoute,
|
|
412
422
|
messages: siteMessages,
|
|
423
|
+
urlPrefix,
|
|
413
424
|
});
|
|
414
425
|
const bodyHtml = renderPageBody({
|
|
415
426
|
messages: siteMessages,
|
|
@@ -423,7 +434,7 @@ export async function renderPublication(buildInput, options) {
|
|
|
423
434
|
mediaAssets,
|
|
424
435
|
reference: page.socialImageRef,
|
|
425
436
|
baseUrl: validatedInput.baseUrl,
|
|
426
|
-
basePath:
|
|
437
|
+
basePath: publicBasePath,
|
|
427
438
|
});
|
|
428
439
|
contentPages.push({
|
|
429
440
|
virtualPath: `pages/${page.kind}-${index}.html`,
|
|
@@ -437,7 +448,10 @@ export async function renderPublication(buildInput, options) {
|
|
|
437
448
|
pageKind: page.kind,
|
|
438
449
|
description: page.description,
|
|
439
450
|
robotsContent: page.robotsContent,
|
|
440
|
-
canonicalUrl: new URL(
|
|
451
|
+
canonicalUrl: new URL(
|
|
452
|
+
joinPublicRoute(publicBasePath, page.route),
|
|
453
|
+
validatedInput.baseUrl,
|
|
454
|
+
).toString(),
|
|
441
455
|
siteName,
|
|
442
456
|
ogType: page.ogType ?? 'website',
|
|
443
457
|
ogImage,
|
|
@@ -464,6 +478,10 @@ export async function renderPublication(buildInput, options) {
|
|
|
464
478
|
redirectProjections.push({
|
|
465
479
|
joinedSource,
|
|
466
480
|
joinedTarget: joined,
|
|
481
|
+
publicTarget: joinPublicRoute(
|
|
482
|
+
publicBasePath,
|
|
483
|
+
contentRoute(record.frontmatter),
|
|
484
|
+
),
|
|
467
485
|
filePath: projectRouteToFilePath(joinedSource, routeProfile),
|
|
468
486
|
});
|
|
469
487
|
}
|
|
@@ -471,7 +489,7 @@ export async function renderPublication(buildInput, options) {
|
|
|
471
489
|
|
|
472
490
|
const redirectPages = redirectProjections.map((projection, index) => ({
|
|
473
491
|
virtualPath: `redirects/${index}.html`,
|
|
474
|
-
content: renderRedirectDocument(projection.
|
|
492
|
+
content: renderRedirectDocument(projection.publicTarget),
|
|
475
493
|
permalink: projection.filePath,
|
|
476
494
|
}));
|
|
477
495
|
|
|
@@ -489,6 +507,7 @@ export async function renderPublication(buildInput, options) {
|
|
|
489
507
|
items: validatedInput.navigation.items,
|
|
490
508
|
currentRoute: undefined,
|
|
491
509
|
messages: siteMessages,
|
|
510
|
+
urlPrefix,
|
|
492
511
|
});
|
|
493
512
|
const errorBodyHtml = renderPageBody({
|
|
494
513
|
messages: siteMessages,
|
|
@@ -546,7 +565,7 @@ export async function renderPublication(buildInput, options) {
|
|
|
546
565
|
);
|
|
547
566
|
const sitemapXml = buildSitemap({
|
|
548
567
|
generatedPages,
|
|
549
|
-
basePath:
|
|
568
|
+
basePath: publicBasePath,
|
|
550
569
|
baseUrl: validatedInput.baseUrl,
|
|
551
570
|
});
|
|
552
571
|
const searchIndexJson = buildSearchIndex(validatedInput);
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { escapeHtml } from './skeleton.js';
|
|
22
|
-
import {
|
|
22
|
+
import { derivePublicBasePath, joinPublicRoute } from './route.js';
|
|
23
23
|
import {
|
|
24
24
|
contentLastModified,
|
|
25
25
|
contentRoute,
|
|
@@ -66,7 +66,10 @@ export function buildFeeds(validatedInput) {
|
|
|
66
66
|
* @returns {string} the absolute `https://` URL
|
|
67
67
|
*/
|
|
68
68
|
const absoluteUrl = (route) =>
|
|
69
|
-
new URL(
|
|
69
|
+
new URL(
|
|
70
|
+
joinPublicRoute(derivePublicBasePath(baseUrl, basePath), route),
|
|
71
|
+
baseUrl,
|
|
72
|
+
).toString();
|
|
70
73
|
|
|
71
74
|
const siteUrl = absoluteUrl('/');
|
|
72
75
|
const atomSelfUrl = absoluteUrl(ATOM_FEED_ROUTE);
|
|
@@ -61,7 +61,7 @@ import {
|
|
|
61
61
|
import { getMessages } from './messages.js';
|
|
62
62
|
import { resolveTextDirection } from './text-direction.js';
|
|
63
63
|
import { routeSegmentForLabel } from './route-labels.js';
|
|
64
|
-
import {
|
|
64
|
+
import { derivePublicBasePath, joinPublicRoute } from './route.js';
|
|
65
65
|
|
|
66
66
|
/** Listing page size (index and per-year archive pages). Scoped decision:
|
|
67
67
|
* this renderer has no externally fixed page size; 10 is a deterministic,
|
|
@@ -264,7 +264,8 @@ function latestContentTimestamp(records) {
|
|
|
264
264
|
* order
|
|
265
265
|
*/
|
|
266
266
|
export function buildGeneratedPages(validatedInput) {
|
|
267
|
-
const { publication, authors, content, basePath } = validatedInput;
|
|
267
|
+
const { publication, authors, content, basePath, baseUrl } = validatedInput;
|
|
268
|
+
const publicBasePath = derivePublicBasePath(baseUrl, basePath);
|
|
268
269
|
const messages = getMessages(publication.defaultLanguage);
|
|
269
270
|
const authorsById = new Map(authors.map((author) => [author.id, author]));
|
|
270
271
|
|
|
@@ -272,7 +273,7 @@ export function buildGeneratedPages(validatedInput) {
|
|
|
272
273
|
* @param {string} route an un-joined route
|
|
273
274
|
* @returns {string} the `basePath`-joined absolute route
|
|
274
275
|
*/
|
|
275
|
-
const site = (route) =>
|
|
276
|
+
const site = (route) => joinPublicRoute(publicBasePath, route);
|
|
276
277
|
|
|
277
278
|
/**
|
|
278
279
|
* @param {import('../../../types/index.d.ts').ContentBuildRecord} record
|
|
@@ -33,6 +33,48 @@ export function joinBasePathAndRoute(basePath, route) {
|
|
|
33
33
|
return `/${segments.join('/')}`;
|
|
34
34
|
}
|
|
35
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
|
+
|
|
36
78
|
/**
|
|
37
79
|
* Project one already-joined absolute route to its candidate-directory-
|
|
38
80
|
* relative output file path.
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* full-text search.
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
-
import {
|
|
35
|
+
import { derivePublicBasePath, joinPublicRoute } from './route.js';
|
|
36
36
|
import { compareUtf8Bytes } from './source-inventory.js';
|
|
37
37
|
import { contentLastModified, contentRoute } from './page-kinds.js';
|
|
38
38
|
|
|
@@ -77,7 +77,10 @@ export function buildSearchIndex(validatedInput) {
|
|
|
77
77
|
* @returns {string} the absolute `https://` URL
|
|
78
78
|
*/
|
|
79
79
|
const absoluteUrl = (route) =>
|
|
80
|
-
new URL(
|
|
80
|
+
new URL(
|
|
81
|
+
joinPublicRoute(derivePublicBasePath(baseUrl, basePath), route),
|
|
82
|
+
baseUrl,
|
|
83
|
+
).toString();
|
|
81
84
|
|
|
82
85
|
const documents = content
|
|
83
86
|
.filter(
|
package/src/core/internal/seo.js
CHANGED
|
@@ -78,8 +78,10 @@ export function resolveOriginalDerivativePath(mediaAssets, reference) {
|
|
|
78
78
|
* the media pipeline's own finished `assets` list
|
|
79
79
|
* @param {{path: string, sourceDigest: string} | undefined} options.reference
|
|
80
80
|
* the candidate `resolvedFile` reference
|
|
81
|
-
* @param {string} options.baseUrl the build input's
|
|
82
|
-
* @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
|
|
83
85
|
* derivative path is joined with this before being resolved against
|
|
84
86
|
* `baseUrl`, so a non-root publication's social-image URL is correct)
|
|
85
87
|
* @returns {string | undefined} the absolute derivative image URL, or
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { escapeHtml } from './skeleton.js';
|
|
21
|
-
import {
|
|
21
|
+
import { joinPublicRoute } from './route.js';
|
|
22
22
|
import { compareUtf8Bytes } from './source-inventory.js';
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -37,8 +37,9 @@ function isNoindex(page) {
|
|
|
37
37
|
* @param {object} options build options
|
|
38
38
|
* @param {readonly import('./page-kinds.js').GeneratedPage[]} options.generatedPages
|
|
39
39
|
* every page `internal/page-kinds.js` produced
|
|
40
|
-
* @param {string} options.basePath the
|
|
41
|
-
*
|
|
40
|
+
* @param {string} options.basePath the public base path
|
|
41
|
+
* (`route.js#derivePublicBasePath`)
|
|
42
|
+
* @param {string} options.baseUrl the build input's `baseUrl`
|
|
42
43
|
* @returns {string} the exact UTF-8 `sitemap.xml` text
|
|
43
44
|
*/
|
|
44
45
|
export function buildSitemap({ generatedPages, basePath, baseUrl }) {
|
|
@@ -47,7 +48,7 @@ export function buildSitemap({ generatedPages, basePath, baseUrl }) {
|
|
|
47
48
|
* @returns {string} the absolute `https://` URL
|
|
48
49
|
*/
|
|
49
50
|
const absoluteUrl = (route) =>
|
|
50
|
-
new URL(
|
|
51
|
+
new URL(joinPublicRoute(basePath, route), baseUrl).toString();
|
|
51
52
|
|
|
52
53
|
const entries = generatedPages
|
|
53
54
|
.filter((page) => !isNoindex(page))
|
|
@@ -88,6 +88,8 @@ const HTML_ESCAPES = Object.freeze({
|
|
|
88
88
|
"'": ''',
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
+
import { joinPublicRoute } from './route.js';
|
|
92
|
+
|
|
91
93
|
/**
|
|
92
94
|
* Escape a string for safe insertion into HTML text or a quoted attribute
|
|
93
95
|
* value.
|
|
@@ -142,19 +144,25 @@ export function renderSkipLink(messages) {
|
|
|
142
144
|
* `navigationItem` (or `navigationLeaf`, which never has children)
|
|
143
145
|
* @param {string | undefined} currentRoute the current page's own joined
|
|
144
146
|
* route, so the matching link can carry `aria-current="page"`
|
|
147
|
+
* @param {string} urlPrefix the `baseUrl` path prefix prepended to an
|
|
148
|
+
* internal route's href (`/` when `baseUrl` has no path)
|
|
145
149
|
* @returns {string} the rendered `<li>`
|
|
146
150
|
*/
|
|
147
|
-
function renderNavItem(item, currentRoute) {
|
|
151
|
+
function renderNavItem(item, currentRoute, urlPrefix) {
|
|
148
152
|
const label = escapeHtml(item.label);
|
|
149
153
|
const href = escapeHtml(
|
|
150
|
-
/** @type {string} */ (
|
|
154
|
+
/** @type {string} */ (
|
|
155
|
+
item.type === 'internal'
|
|
156
|
+
? joinPublicRoute(urlPrefix, /** @type {string} */ (item.route))
|
|
157
|
+
: item.url
|
|
158
|
+
),
|
|
151
159
|
);
|
|
152
160
|
const isCurrent = item.type === 'internal' && item.route === currentRoute;
|
|
153
161
|
const currentAttribute = isCurrent ? ' aria-current="page"' : '';
|
|
154
162
|
const children = item.children ?? [];
|
|
155
163
|
const childList =
|
|
156
164
|
children.length > 0
|
|
157
|
-
? `<ul>${children.map((child) => renderNavItem(child, currentRoute)).join('')}</ul>`
|
|
165
|
+
? `<ul>${children.map((child) => renderNavItem(child, currentRoute, urlPrefix)).join('')}</ul>`
|
|
158
166
|
: '';
|
|
159
167
|
return `<li><a href="${href}"${currentAttribute}>${label}</a>${childList}</li>`;
|
|
160
168
|
}
|
|
@@ -170,6 +178,8 @@ function renderNavItem(item, currentRoute) {
|
|
|
170
178
|
* route
|
|
171
179
|
* @param {Readonly<Record<string, string | ((...args: string[]) => string)>>} options.messages
|
|
172
180
|
* the resolved message catalog
|
|
181
|
+
* @param {string} [options.urlPrefix] the `baseUrl` path prefix for internal
|
|
182
|
+
* hrefs (default `/`)
|
|
173
183
|
* @returns {string} the rendered primary navigation `<nav>`, always present
|
|
174
184
|
* even when `items` is empty (TPL-M3): every page kind renders exactly
|
|
175
185
|
* one primary-navigation landmark unconditionally
|
|
@@ -178,12 +188,17 @@ function renderNavItem(item, currentRoute) {
|
|
|
178
188
|
* on every page regardless of whether that publication has authored any
|
|
179
189
|
* navigation items yet.
|
|
180
190
|
*/
|
|
181
|
-
export function renderPrimaryNavigation({
|
|
191
|
+
export function renderPrimaryNavigation({
|
|
192
|
+
items,
|
|
193
|
+
currentRoute,
|
|
194
|
+
messages,
|
|
195
|
+
urlPrefix = '/',
|
|
196
|
+
}) {
|
|
182
197
|
const label = escapeHtml(
|
|
183
198
|
/** @type {string} */ (messages.primaryNavigationLabel),
|
|
184
199
|
);
|
|
185
200
|
const itemsHtml = items
|
|
186
|
-
.map((item) => renderNavItem(item, currentRoute))
|
|
201
|
+
.map((item) => renderNavItem(item, currentRoute, urlPrefix))
|
|
187
202
|
.join('');
|
|
188
203
|
return `<nav aria-label="${label}"><ul>${itemsHtml}</ul></nav>`;
|
|
189
204
|
}
|
|
@@ -196,14 +211,15 @@ export function renderPrimaryNavigation({ items, currentRoute, messages }) {
|
|
|
196
211
|
* `navigationNormalized.footerItems` (0..50 root items)
|
|
197
212
|
* @param {Readonly<Record<string, string | ((...args: string[]) => string)>>} options.messages
|
|
198
213
|
* the resolved message catalog
|
|
214
|
+
* @param {string} [options.urlPrefix] the `baseUrl` path prefix (default `/`)
|
|
199
215
|
* @returns {string} the rendered footer navigation
|
|
200
216
|
*/
|
|
201
|
-
export function renderFooterNavigation({ items, messages }) {
|
|
217
|
+
export function renderFooterNavigation({ items, messages, urlPrefix = '/' }) {
|
|
202
218
|
const label = escapeHtml(
|
|
203
219
|
/** @type {string} */ (messages.footerNavigationLabel),
|
|
204
220
|
);
|
|
205
221
|
const itemsHtml = items
|
|
206
|
-
.map((item) => renderNavItem(item, undefined))
|
|
222
|
+
.map((item) => renderNavItem(item, undefined, urlPrefix))
|
|
207
223
|
.join('');
|
|
208
224
|
return `<nav aria-label="${label}"><ul>${itemsHtml}</ul></nav>`;
|
|
209
225
|
}
|
|
@@ -277,6 +277,19 @@ function joinedThemeOutputPath(basePath, themeRelativePath) {
|
|
|
277
277
|
);
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
+
/**
|
|
281
|
+
* The public `<link href>` for one theme file: the same joined path as
|
|
282
|
+
* {@link joinedThemeOutputPath}, but under the public base path.
|
|
283
|
+
*
|
|
284
|
+
* @param {string} publicBasePath the public base path
|
|
285
|
+
* @param {string} themeRelativePath the file's path relative to
|
|
286
|
+
* `THEME_ASSET_DIRECTORY`
|
|
287
|
+
* @returns {string} the root-absolute href
|
|
288
|
+
*/
|
|
289
|
+
function themeHref(publicBasePath, themeRelativePath) {
|
|
290
|
+
return `/${joinedThemeOutputPath(publicBasePath, themeRelativePath)}`;
|
|
291
|
+
}
|
|
292
|
+
|
|
280
293
|
/** @type {{maximumFileBytes: number, maximumTotalBytes: number, maximumFiles: number}} */
|
|
281
294
|
const FALLBACK_BUDGETS = Object.freeze({
|
|
282
295
|
maximumFileBytes: DEFAULT_MAXIMUM_PASSIVE_ASSET_BYTES,
|
|
@@ -471,6 +484,8 @@ function subresourceIntegrityAttribute(bytes) {
|
|
|
471
484
|
* file set `theme.json.stylesheets`/`assets` and `package.json.files`
|
|
472
485
|
* name)
|
|
473
486
|
* @param {string} options.basePath the build input's `basePath`
|
|
487
|
+
* @param {string} [options.publicBasePath] the base path emitted `<link href>`
|
|
488
|
+
* values use (`route.js#derivePublicBasePath`); defaults to `basePath`
|
|
474
489
|
* @returns {Promise<{
|
|
475
490
|
* files: {path: string, bytes: Buffer}[],
|
|
476
491
|
* assets: import('../../../types/index.d.ts').ManifestAssetEntry[],
|
|
@@ -478,7 +493,11 @@ function subresourceIntegrityAttribute(bytes) {
|
|
|
478
493
|
* }>} the files to write (already `basePath`-joined), their manifest rows
|
|
479
494
|
* (same joined paths), and the ready-to-insert `<link>` markup
|
|
480
495
|
*/
|
|
481
|
-
export async function loadThemeAssets({
|
|
496
|
+
export async function loadThemeAssets({
|
|
497
|
+
themeDirectory,
|
|
498
|
+
basePath,
|
|
499
|
+
publicBasePath = basePath,
|
|
500
|
+
}) {
|
|
482
501
|
const { bytes: themeJsonBytes } = await readDeclaredThemeFile(
|
|
483
502
|
themeDirectory,
|
|
484
503
|
'theme.json',
|
|
@@ -533,7 +552,7 @@ export async function loadThemeAssets({ themeDirectory, basePath }) {
|
|
|
533
552
|
// base64, computed fresh from these exact bytes.
|
|
534
553
|
const integrity = subresourceIntegrityAttribute(bytes);
|
|
535
554
|
linkTags.push(
|
|
536
|
-
`<link rel="stylesheet" href="${escapeHtml(
|
|
555
|
+
`<link rel="stylesheet" href="${escapeHtml(themeHref(publicBasePath, relativePath))}" integrity="${integrity}" crossorigin="anonymous"${mediaAttribute}>`,
|
|
537
556
|
);
|
|
538
557
|
}
|
|
539
558
|
|