@rathnasgala2/template 2.2.0 → 3.0.1
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 +90 -61
- package/contracts/render-policy.jcs +1 -1
- package/contracts/theme-styling-contract.jcs +1 -1
- package/package.json +2 -2
- package/src/core/index.js +180 -50
- package/src/core/internal/appearance/base-layer.js +711 -137
- package/src/core/internal/appearance/bootstrap-script.js +570 -83
- package/src/core/internal/appearance/contract.js +6 -5
- package/src/core/internal/appearance/controller-markup.js +46 -55
- package/src/core/internal/appearance/minify-script.js +69 -0
- package/src/core/internal/appearance/styling-contract.js +201 -19
- package/src/core/internal/chrome.js +199 -0
- package/src/core/internal/components.js +371 -0
- package/src/core/internal/eleventy-render.js +13 -4
- package/src/core/internal/feeds.js +74 -6
- package/src/core/internal/icons.js +52 -0
- package/src/core/internal/llms.js +252 -0
- package/src/core/internal/media/pipeline.js +32 -17
- package/src/core/internal/messages.js +104 -0
- package/src/core/internal/page-kinds.js +918 -290
- package/src/core/internal/render-policy-content.js +2 -2
- package/src/core/internal/robots.js +70 -0
- package/src/core/internal/route-labels.js +31 -0
- package/src/core/internal/seo.js +53 -0
- package/src/core/internal/skeleton.js +54 -129
- package/src/core/internal/structured-data.js +247 -0
- package/types/index.d.ts +2 -0
package/src/core/index.js
CHANGED
|
@@ -99,6 +99,10 @@ import {
|
|
|
99
99
|
renderErrorPageBody,
|
|
100
100
|
} from './internal/page-kinds.js';
|
|
101
101
|
import { assertProvenance } from './internal/provenance.js';
|
|
102
|
+
import {
|
|
103
|
+
deriveAuthorSlugs,
|
|
104
|
+
routeSegmentForLabel,
|
|
105
|
+
} from './internal/route-labels.js';
|
|
102
106
|
import {
|
|
103
107
|
derivePublicBasePath,
|
|
104
108
|
errorDocumentPath,
|
|
@@ -109,7 +113,9 @@ import {
|
|
|
109
113
|
renderRedirectDocument,
|
|
110
114
|
} from './internal/route.js';
|
|
111
115
|
import { buildSearchIndex } from './internal/search-index.js';
|
|
112
|
-
import {
|
|
116
|
+
import { resolveSocialImage, twitterCardType } from './internal/seo.js';
|
|
117
|
+
import { buildLlmsDocuments } from './internal/llms.js';
|
|
118
|
+
import { buildRobotsTxt, readAiCrawlerPolicy } from './internal/robots.js';
|
|
113
119
|
import { buildSitemap } from './internal/sitemap.js';
|
|
114
120
|
import {
|
|
115
121
|
defaultThemeStylesheetLinksHtml,
|
|
@@ -117,12 +123,12 @@ import {
|
|
|
117
123
|
} from './internal/theme-assets.js';
|
|
118
124
|
import { resolveTextDirection } from './internal/text-direction.js';
|
|
119
125
|
import {
|
|
120
|
-
|
|
121
|
-
renderFooterNavigation,
|
|
122
|
-
renderHeader,
|
|
126
|
+
escapeHtml,
|
|
123
127
|
renderPageBody,
|
|
124
128
|
renderPrimaryNavigation,
|
|
125
129
|
} from './internal/skeleton.js';
|
|
130
|
+
import { renderFooter, renderHeader } from './internal/chrome.js';
|
|
131
|
+
import { createComponents, createMediaImage } from './internal/components.js';
|
|
126
132
|
import {
|
|
127
133
|
collectIncludedSources,
|
|
128
134
|
compareUtf8Bytes,
|
|
@@ -135,6 +141,13 @@ const SITEMAP_ROUTE = '/sitemap.xml';
|
|
|
135
141
|
/** @type {string} the un-joined route the static search index is published
|
|
136
142
|
* at. */
|
|
137
143
|
const SEARCH_INDEX_ROUTE = '/search-index.json';
|
|
144
|
+
/** @type {string} the output-root path of `robots.txt` (never `basePath`-
|
|
145
|
+
* joined: only a host-root robots file is honoured by crawlers). */
|
|
146
|
+
const ROBOTS_PATH = 'robots.txt';
|
|
147
|
+
/** @type {string} the un-joined route of the llmstxt.org index. */
|
|
148
|
+
const LLMS_ROUTE = '/llms.txt';
|
|
149
|
+
/** @type {string} the un-joined route of the full-text companion. */
|
|
150
|
+
const LLMS_FULL_ROUTE = '/llms-full.txt';
|
|
138
151
|
|
|
139
152
|
/** @type {string} */
|
|
140
153
|
const BUILD_INPUT_SCHEMA_ID = 'urn:gala:schema:build-input:2.0.0';
|
|
@@ -336,42 +349,116 @@ export async function renderPublication(buildInput, options) {
|
|
|
336
349
|
const appearanceControlHtml = renderAppearanceControl({
|
|
337
350
|
messages: siteMessages,
|
|
338
351
|
});
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
352
|
+
|
|
353
|
+
// Run before page assembly, not after: resolving each page's own images
|
|
354
|
+
// (`og:image`, hero, avatar, brand mark) needs the media pipeline's own
|
|
355
|
+
// finished `assets` list (only it knows which derivative file extension a
|
|
356
|
+
// given source image's `sourceDigest` produced — see `internal/seo.js`).
|
|
357
|
+
// This only moves the in-memory computation earlier; `mediaFiles`' actual
|
|
358
|
+
// bytes are still written to `outputDirectory` only after the route
|
|
359
|
+
// listing below, so `outputDirectory/assets/media/` still can never be
|
|
360
|
+
// mistaken for an Eleventy-rendered HTML route.
|
|
361
|
+
const {
|
|
362
|
+
assets: mediaAssets,
|
|
363
|
+
files: mediaFiles,
|
|
364
|
+
dimensions: mediaDimensions,
|
|
365
|
+
} = await processMedia(validatedInput, {
|
|
366
|
+
sourceDirectory: path.resolve(options.sourceDirectory),
|
|
344
367
|
});
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
368
|
+
const mediaImage = createMediaImage({
|
|
369
|
+
mediaAssets,
|
|
370
|
+
publicBasePath,
|
|
371
|
+
mediaDimensions,
|
|
349
372
|
});
|
|
350
|
-
const
|
|
373
|
+
const brandMark = mediaImage(validatedInput.appearance.brandMark);
|
|
374
|
+
const searchIndexHref = joinBasePathAndRoute(
|
|
375
|
+
publicBasePath,
|
|
376
|
+
SEARCH_INDEX_ROUTE,
|
|
377
|
+
);
|
|
378
|
+
const rssHref = joinBasePathAndRoute(publicBasePath, RSS_FEED_ROUTE);
|
|
379
|
+
const publication = validatedInput.publication;
|
|
380
|
+
const footerCard = publication.footerCard;
|
|
351
381
|
// Already verified render-policy-conformant HTML by
|
|
352
382
|
// assertRenderPolicyCompliance's assertPolicyConformantHtml call above —
|
|
353
383
|
// inserted as-is, never re-parsed as Markdown.
|
|
354
384
|
const footerProfileHtml = footerCard?.enabled ? footerCard.body.body : '';
|
|
385
|
+
const authorsById = new Map(
|
|
386
|
+
validatedInput.authors.map((author) => [author.id, author]),
|
|
387
|
+
);
|
|
388
|
+
const footerAuthor =
|
|
389
|
+
authorsById.get(publication.contactAuthorId ?? '') ??
|
|
390
|
+
validatedInput.authors[0];
|
|
391
|
+
const authorSlugs = deriveAuthorSlugs(validatedInput.authors);
|
|
392
|
+
const authorHref = (
|
|
393
|
+
/** @type {import('../../types/index.d.ts').AuthorNormalized} */ author,
|
|
394
|
+
) =>
|
|
395
|
+
joinPublicRoute(
|
|
396
|
+
publicBasePath,
|
|
397
|
+
`/authors/${authorSlugs.get(author.id) ?? author.id}`,
|
|
398
|
+
);
|
|
399
|
+
const footerComponents = createComponents({
|
|
400
|
+
messages: siteMessages,
|
|
401
|
+
site: (route) => joinPublicRoute(publicBasePath, route),
|
|
402
|
+
mediaImage,
|
|
403
|
+
authorHref,
|
|
404
|
+
authorsById,
|
|
405
|
+
recordHref: (record) =>
|
|
406
|
+
joinPublicRoute(publicBasePath, contentRoute(record.frontmatter)),
|
|
407
|
+
tagHref: (tag) =>
|
|
408
|
+
joinPublicRoute(publicBasePath, `/tags/${routeSegmentForLabel(tag)}`),
|
|
409
|
+
});
|
|
410
|
+
const footerAuthorHtml = footerAuthor
|
|
411
|
+
? `<p class="g-label">${footerComponents.text('writtenByLabel')}</p>` +
|
|
412
|
+
`<div class="g-byline">${footerComponents.avatar(footerAuthor)}<div class="g-byline-text">` +
|
|
413
|
+
`<a href="${escapeHtml(authorHref(footerAuthor))}">${escapeHtml(footerAuthor.displayName)}</a>` +
|
|
414
|
+
`</div></div>`
|
|
415
|
+
: '';
|
|
416
|
+
const latestYear = validatedInput.content
|
|
417
|
+
.map((record) => record.frontmatter.publishedAt.slice(0, 4))
|
|
418
|
+
.sort()
|
|
419
|
+
.at(-1);
|
|
355
420
|
const footerHtml = renderFooter({
|
|
356
|
-
|
|
357
|
-
|
|
421
|
+
homeRoute,
|
|
422
|
+
publicationName: publication.title,
|
|
423
|
+
description: publication.description,
|
|
424
|
+
brandMark,
|
|
425
|
+
socialLinks: publication.socialLinks,
|
|
426
|
+
rssHref,
|
|
427
|
+
primaryItems: validatedInput.navigation.items,
|
|
428
|
+
footerItems: validatedInput.navigation.footerItems,
|
|
429
|
+
urlPrefix,
|
|
430
|
+
authorHtml: footerAuthorHtml,
|
|
358
431
|
footerProfileHtml,
|
|
359
|
-
copyrightText:
|
|
432
|
+
copyrightText: `${siteMessages.footerCopyrightPrefix} ${latestYear ? `${latestYear} ` : ''}${publication.title}`,
|
|
360
433
|
messages: siteMessages,
|
|
361
434
|
});
|
|
362
435
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
436
|
+
/**
|
|
437
|
+
* One page's `<header>`, with the primary navigation marking that page's
|
|
438
|
+
* own route current.
|
|
439
|
+
*
|
|
440
|
+
* @param {string | undefined} currentRoute the page's joined route
|
|
441
|
+
* @returns {string}
|
|
442
|
+
*/
|
|
443
|
+
const headerFor = (currentRoute) =>
|
|
444
|
+
renderHeader({
|
|
445
|
+
homeRoute,
|
|
446
|
+
publicationName: publication.title,
|
|
447
|
+
tagline: publication.description,
|
|
448
|
+
brandMark,
|
|
449
|
+
navHtml: renderPrimaryNavigation({
|
|
450
|
+
items: validatedInput.navigation.items,
|
|
451
|
+
currentRoute,
|
|
452
|
+
messages: siteMessages,
|
|
453
|
+
urlPrefix,
|
|
454
|
+
}),
|
|
455
|
+
navItems: validatedInput.navigation.items,
|
|
456
|
+
currentRoute,
|
|
457
|
+
urlPrefix,
|
|
458
|
+
searchIndexHref,
|
|
459
|
+
appearanceControlHtml,
|
|
460
|
+
messages: siteMessages,
|
|
461
|
+
});
|
|
375
462
|
|
|
376
463
|
// The site-wide feeds, resolved once so every page's feed-discovery
|
|
377
464
|
// `<link>` tags and the feed documents themselves agree on the exact same
|
|
@@ -410,39 +497,37 @@ export async function renderPublication(buildInput, options) {
|
|
|
410
497
|
/** @type {{joinedSource: string, joinedTarget: string, publicTarget: string, filePath: string}[]} */
|
|
411
498
|
const redirectProjections = [];
|
|
412
499
|
|
|
413
|
-
const generatedPages = buildGeneratedPages(validatedInput
|
|
500
|
+
const generatedPages = buildGeneratedPages(validatedInput, {
|
|
501
|
+
mediaAssets,
|
|
502
|
+
mediaDimensions,
|
|
503
|
+
});
|
|
414
504
|
generatedPages.forEach((page, index) => {
|
|
415
505
|
const joinedRoute = joinBasePathAndRoute(
|
|
416
506
|
validatedInput.basePath,
|
|
417
507
|
page.route,
|
|
418
508
|
);
|
|
419
|
-
const navHtml = renderPrimaryNavigation({
|
|
420
|
-
items: validatedInput.navigation.items,
|
|
421
|
-
currentRoute: joinedRoute,
|
|
422
|
-
messages: siteMessages,
|
|
423
|
-
urlPrefix,
|
|
424
|
-
});
|
|
425
509
|
const bodyHtml = renderPageBody({
|
|
426
510
|
messages: siteMessages,
|
|
427
|
-
headerHtml,
|
|
428
|
-
navHtml,
|
|
511
|
+
headerHtml: headerFor(joinedRoute),
|
|
429
512
|
footerHtml,
|
|
430
|
-
breadcrumbHtml: page.breadcrumbHtml,
|
|
431
513
|
mainHtml: page.bodyHtml,
|
|
432
514
|
});
|
|
433
|
-
const
|
|
515
|
+
const ogImageInfo = resolveSocialImage({
|
|
434
516
|
mediaAssets,
|
|
517
|
+
mediaDimensions,
|
|
435
518
|
reference: page.socialImageRef,
|
|
436
519
|
baseUrl: validatedInput.baseUrl,
|
|
437
520
|
basePath: publicBasePath,
|
|
438
521
|
});
|
|
522
|
+
const ogImage = ogImageInfo?.url;
|
|
439
523
|
contentPages.push({
|
|
440
524
|
virtualPath: `pages/${page.kind}-${index}.html`,
|
|
441
525
|
content: bodyHtml,
|
|
442
526
|
permalink: projectRouteToFilePath(joinedRoute, routeProfile),
|
|
443
527
|
layout: 'skeleton.njk',
|
|
444
528
|
data: {
|
|
445
|
-
title: page.title,
|
|
529
|
+
title: page.documentTitle ?? page.title,
|
|
530
|
+
ogTitle: page.title,
|
|
446
531
|
lang: page.language,
|
|
447
532
|
dir: page.direction,
|
|
448
533
|
pageKind: page.kind,
|
|
@@ -455,6 +540,14 @@ export async function renderPublication(buildInput, options) {
|
|
|
455
540
|
siteName,
|
|
456
541
|
ogType: page.ogType ?? 'website',
|
|
457
542
|
ogImage,
|
|
543
|
+
ogImageWidth: ogImageInfo?.width,
|
|
544
|
+
ogImageHeight: ogImageInfo?.height,
|
|
545
|
+
ogImageAlt: ogImage ? page.socialImageAlt : undefined,
|
|
546
|
+
articlePublished: page.articleMeta?.publishedTime,
|
|
547
|
+
articleModified: page.articleMeta?.modifiedTime,
|
|
548
|
+
articleAuthors: page.articleMeta?.authorUrls ?? [],
|
|
549
|
+
articleTags: page.articleMeta?.tags ?? [],
|
|
550
|
+
jsonLd: page.jsonLd,
|
|
458
551
|
twitterCard: twitterCardType(Boolean(ogImage)),
|
|
459
552
|
atomFeedUrl: feeds.atomSelfUrl,
|
|
460
553
|
rssFeedUrl: feeds.rssSelfUrl,
|
|
@@ -503,16 +596,9 @@ export async function renderPublication(buildInput, options) {
|
|
|
503
596
|
// page kind's own pure body function. Pushed after the guard above so the
|
|
504
597
|
// guard's own "is there anything authored to render" check is unaffected
|
|
505
598
|
// by this always-present synthetic page.
|
|
506
|
-
const errorNavHtml = renderPrimaryNavigation({
|
|
507
|
-
items: validatedInput.navigation.items,
|
|
508
|
-
currentRoute: undefined,
|
|
509
|
-
messages: siteMessages,
|
|
510
|
-
urlPrefix,
|
|
511
|
-
});
|
|
512
599
|
const errorBodyHtml = renderPageBody({
|
|
513
600
|
messages: siteMessages,
|
|
514
|
-
headerHtml,
|
|
515
|
-
navHtml: errorNavHtml,
|
|
601
|
+
headerHtml: headerFor(undefined),
|
|
516
602
|
footerHtml,
|
|
517
603
|
mainHtml: renderErrorPageBody({ messages: siteMessages, homeRoute }),
|
|
518
604
|
});
|
|
@@ -523,7 +609,8 @@ export async function renderPublication(buildInput, options) {
|
|
|
523
609
|
permalink: errorFilePath,
|
|
524
610
|
layout: 'skeleton.njk',
|
|
525
611
|
data: {
|
|
526
|
-
title: /** @type {string} */ (siteMessages.errorPageHeading)
|
|
612
|
+
title: `${/** @type {string} */ (siteMessages.errorPageHeading)} | ${siteName}`,
|
|
613
|
+
ogTitle: /** @type {string} */ (siteMessages.errorPageHeading),
|
|
527
614
|
lang: validatedInput.publication.defaultLanguage,
|
|
528
615
|
dir: resolveTextDirection(validatedInput.publication.defaultLanguage),
|
|
529
616
|
pageKind: 'error',
|
|
@@ -569,7 +656,38 @@ export async function renderPublication(buildInput, options) {
|
|
|
569
656
|
baseUrl: validatedInput.baseUrl,
|
|
570
657
|
});
|
|
571
658
|
const searchIndexJson = buildSearchIndex(validatedInput);
|
|
659
|
+
const absoluteSiteUrl = (/** @type {string} */ route) =>
|
|
660
|
+
new URL(
|
|
661
|
+
joinPublicRoute(publicBasePath, route),
|
|
662
|
+
validatedInput.baseUrl,
|
|
663
|
+
).toString();
|
|
664
|
+
const robotsTxt = buildRobotsTxt({
|
|
665
|
+
aiPolicy: readAiCrawlerPolicy(validatedInput.publication),
|
|
666
|
+
sitemapUrl: absoluteSiteUrl(SITEMAP_ROUTE),
|
|
667
|
+
});
|
|
668
|
+
const llmsFilePath = projectFixedAssetPath(
|
|
669
|
+
validatedInput.basePath,
|
|
670
|
+
LLMS_ROUTE,
|
|
671
|
+
);
|
|
672
|
+
const llmsFullFilePath = projectFixedAssetPath(
|
|
673
|
+
validatedInput.basePath,
|
|
674
|
+
LLMS_FULL_ROUTE,
|
|
675
|
+
);
|
|
676
|
+
const { llmsTxt, llmsFullTxt } = buildLlmsDocuments(validatedInput, {
|
|
677
|
+
rss: feeds.rssSelfUrl,
|
|
678
|
+
atom: feeds.atomSelfUrl,
|
|
679
|
+
sitemap: absoluteSiteUrl(SITEMAP_ROUTE),
|
|
680
|
+
searchIndex: absoluteSiteUrl(SEARCH_INDEX_ROUTE),
|
|
681
|
+
full: absoluteSiteUrl(LLMS_FULL_ROUTE),
|
|
682
|
+
});
|
|
572
683
|
const assetPages = [
|
|
684
|
+
{ virtualPath: 'robots.html', content: robotsTxt, permalink: ROBOTS_PATH },
|
|
685
|
+
{ virtualPath: 'llms.html', content: llmsTxt, permalink: llmsFilePath },
|
|
686
|
+
{
|
|
687
|
+
virtualPath: 'llms-full.html',
|
|
688
|
+
content: llmsFullTxt,
|
|
689
|
+
permalink: llmsFullFilePath,
|
|
690
|
+
},
|
|
573
691
|
{
|
|
574
692
|
virtualPath: 'feeds/atom.html',
|
|
575
693
|
content: feeds.atomXml,
|
|
@@ -623,6 +741,18 @@ export async function renderPublication(buildInput, options) {
|
|
|
623
741
|
searchIndexFilePath,
|
|
624
742
|
{ routeClass: 'asset', mediaType: 'application/json; charset=utf-8' },
|
|
625
743
|
],
|
|
744
|
+
[
|
|
745
|
+
ROBOTS_PATH,
|
|
746
|
+
{ routeClass: 'asset', mediaType: 'text/plain; charset=utf-8' },
|
|
747
|
+
],
|
|
748
|
+
[
|
|
749
|
+
llmsFilePath,
|
|
750
|
+
{ routeClass: 'asset', mediaType: 'text/plain; charset=utf-8' },
|
|
751
|
+
],
|
|
752
|
+
[
|
|
753
|
+
llmsFullFilePath,
|
|
754
|
+
{ routeClass: 'asset', mediaType: 'text/plain; charset=utf-8' },
|
|
755
|
+
],
|
|
626
756
|
]);
|
|
627
757
|
|
|
628
758
|
const outputFilePaths = await listFilesSortedByUtf8Bytes(outputDirectory);
|