@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 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.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Deterministic Eleventy-backed static publication renderer for Galascribe portable repositories.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -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 audit --audit-level=high",
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
- const homeRoute = joinBasePathAndRoute(validatedInput.basePath, '/');
327
+ // Every emitted URL carries the `baseUrl` path (for example `/g9/` on a
328
+ // GitHub project site); output file paths keep using `basePath` alone.
329
+ const publicBasePath = derivePublicBasePath(
330
+ validatedInput.baseUrl,
331
+ validatedInput.basePath,
332
+ );
333
+ const urlPrefix = derivePublicBasePath(validatedInput.baseUrl, '/');
334
+ const homeRoute = joinPublicRoute(publicBasePath, '/');
326
335
 
327
336
  const appearanceControlHtml = renderAppearanceControl({
328
337
  messages: siteMessages,
@@ -336,6 +345,7 @@ export async function renderPublication(buildInput, options) {
336
345
  const footerNavHtml = renderFooterNavigation({
337
346
  items: validatedInput.navigation.footerItems,
338
347
  messages: siteMessages,
348
+ urlPrefix,
339
349
  });
340
350
  const footerCard = validatedInput.publication.footerCard;
341
351
  // Already verified render-policy-conformant HTML by
@@ -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 = appearanceBootstrapScriptHref(
372
- validatedInput.basePath,
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 = `/${projectFixedAssetPath(
379
- validatedInput.basePath,
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(validatedInput.basePath);
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: validatedInput.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(joinedRoute, validatedInput.baseUrl).toString(),
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.joinedTarget),
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: validatedInput.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 { joinBasePathAndRoute } from './route.js';
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(joinBasePathAndRoute(basePath, route), baseUrl).toString();
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 { joinBasePathAndRoute } from './route.js';
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) => joinBasePathAndRoute(basePath, 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 { joinBasePathAndRoute } from './route.js';
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(joinBasePathAndRoute(basePath, route), baseUrl).toString();
80
+ new URL(
81
+ joinPublicRoute(derivePublicBasePath(baseUrl, basePath), route),
82
+ baseUrl,
83
+ ).toString();
81
84
 
82
85
  const documents = content
83
86
  .filter(
@@ -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 origin-only `baseUrl`
82
- * @param {string} options.basePath the build input's `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 { joinBasePathAndRoute } from './route.js';
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 build input's `basePath`
41
- * @param {string} options.baseUrl the build input's origin-only `baseUrl`
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(joinBasePathAndRoute(basePath, route), baseUrl).toString();
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
  "'": '&#39;',
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} */ (item.type === 'internal' ? item.route : item.url),
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({ items, currentRoute, messages }) {
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({ themeDirectory, basePath }) {
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(`/${outputPath}`)}" integrity="${integrity}" crossorigin="anonymous"${mediaAttribute}>`,
555
+ `<link rel="stylesheet" href="${escapeHtml(themeHref(publicBasePath, relativePath))}" integrity="${integrity}" crossorigin="anonymous"${mediaAttribute}>`,
537
556
  );
538
557
  }
539
558