@rathnasgala2/template 2.2.0 → 3.0.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 +88 -59
- package/contracts/render-policy.jcs +1 -1
- package/contracts/theme-styling-contract.jcs +1 -1
- package/package.json +1 -1
- 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 +199 -17
- package/src/core/internal/chrome.js +199 -0
- package/src/core/internal/components.js +379 -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 +78 -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
|
@@ -58,9 +58,21 @@ import {
|
|
|
58
58
|
renderPagination,
|
|
59
59
|
renderSlot,
|
|
60
60
|
} from './skeleton.js';
|
|
61
|
+
import {
|
|
62
|
+
createComponents,
|
|
63
|
+
createMediaImage,
|
|
64
|
+
extractH2Headings,
|
|
65
|
+
lazyBodyImages,
|
|
66
|
+
plainText,
|
|
67
|
+
renderImage,
|
|
68
|
+
renderNewsletterPanel,
|
|
69
|
+
} from './components.js';
|
|
70
|
+
import { truncateAtWord } from './seo.js';
|
|
71
|
+
import { createStructuredData } from './structured-data.js';
|
|
72
|
+
import { icon } from './icons.js';
|
|
61
73
|
import { getMessages } from './messages.js';
|
|
62
74
|
import { resolveTextDirection } from './text-direction.js';
|
|
63
|
-
import { routeSegmentForLabel } from './route-labels.js';
|
|
75
|
+
import { deriveAuthorSlugs, routeSegmentForLabel } from './route-labels.js';
|
|
64
76
|
import { derivePublicBasePath, joinPublicRoute } from './route.js';
|
|
65
77
|
|
|
66
78
|
/** Listing page size (index and per-year archive pages). Scoped decision:
|
|
@@ -132,23 +144,6 @@ function paginate(items, pageSize) {
|
|
|
132
144
|
return pages;
|
|
133
145
|
}
|
|
134
146
|
|
|
135
|
-
/**
|
|
136
|
-
* Resolve an author's display name by ID, failing closed on a dangling
|
|
137
|
-
* reference (an adapter defect: `resolvedAuthorIds` is schema-guaranteed to
|
|
138
|
-
* resolve).
|
|
139
|
-
*
|
|
140
|
-
* @param {ReadonlyMap<string, import('../../../types/index.d.ts').AuthorNormalized>} authorsById
|
|
141
|
-
* @param {string} authorId
|
|
142
|
-
* @returns {import('../../../types/index.d.ts').AuthorNormalized}
|
|
143
|
-
*/
|
|
144
|
-
function requireAuthor(authorsById, authorId) {
|
|
145
|
-
const author = authorsById.get(authorId);
|
|
146
|
-
if (!author) {
|
|
147
|
-
throw new Error(`internal: no author record for id ${authorId}`);
|
|
148
|
-
}
|
|
149
|
-
return author;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
147
|
/**
|
|
153
148
|
* @typedef {object} GeneratedPage
|
|
154
149
|
* @property {'profile' | 'author' | 'article' | 'page' | 'index' | 'tag' | 'series' | 'archive'} kind
|
|
@@ -157,13 +152,21 @@ function requireAuthor(authorsById, authorId) {
|
|
|
157
152
|
* @property {string} title the page's `<title>` text
|
|
158
153
|
* @property {string} language the page's BCP-47 language tag
|
|
159
154
|
* @property {string} [direction] the page's resolved base text direction (assigned by buildGeneratedPages's own return mapping)
|
|
160
|
-
* @property {string} bodyHtml the page's complete `<main>` inner HTML
|
|
161
|
-
*
|
|
162
|
-
* @property {string} [breadcrumbHtml] an optional already-rendered breadcrumb
|
|
155
|
+
* @property {string} bodyHtml the page's complete `<main>` inner HTML
|
|
156
|
+
* (breadcrumb included), containing exactly one `<h1>`
|
|
163
157
|
* @property {string} [description] an optional plain-text
|
|
164
158
|
* description this page's SEO/Open-Graph/Twitter `<meta>` tags are built
|
|
165
159
|
* from (never HTML, never the sanitized body — a distinct authored or
|
|
166
160
|
* publication-level plain-text field)
|
|
161
|
+
* @property {string} [documentTitle] the `<title>` element text
|
|
162
|
+
* (`<Page> | <Publication>`; the home page is
|
|
163
|
+
* `<Publication> – <tagline>`); `title` stays the bare page name used for
|
|
164
|
+
* `og:title` and structured data
|
|
165
|
+
* @property {string} [jsonLd] the page's one embed-safe JSON-LD document
|
|
166
|
+
* (see `internal/structured-data.js`)
|
|
167
|
+
* @property {string} [socialImageAlt] alternative text for the social image
|
|
168
|
+
* @property {{publishedTime: string, modifiedTime?: string, authorUrls: string[], tags: string[]}} [articleMeta]
|
|
169
|
+
* the `article:*` Open Graph facts of an article page
|
|
167
170
|
* @property {'website' | 'article' | 'profile'} [ogType]
|
|
168
171
|
* the Open Graph `og:type` this page kind maps to; defaults to `'website'`
|
|
169
172
|
* when omitted
|
|
@@ -255,15 +258,104 @@ function latestContentTimestamp(records) {
|
|
|
255
258
|
);
|
|
256
259
|
}
|
|
257
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Group records by derived keys, preserving the input order within each
|
|
263
|
+
* bucket.
|
|
264
|
+
*
|
|
265
|
+
* @param {readonly import('../../../types/index.d.ts').ContentBuildRecord[]} records
|
|
266
|
+
* @param {(record: import('../../../types/index.d.ts').ContentBuildRecord) => readonly string[]} keysOf
|
|
267
|
+
* @returns {Map<string, import('../../../types/index.d.ts').ContentBuildRecord[]>}
|
|
268
|
+
*/
|
|
269
|
+
function groupBy(records, keysOf) {
|
|
270
|
+
/** @type {Map<string, import('../../../types/index.d.ts').ContentBuildRecord[]>} */
|
|
271
|
+
const groups = new Map();
|
|
272
|
+
for (const record of records) {
|
|
273
|
+
for (const key of keysOf(record)) {
|
|
274
|
+
const bucket = groups.get(key) ?? [];
|
|
275
|
+
bucket.push(record);
|
|
276
|
+
groups.set(key, bucket);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return groups;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Number of articles after the featured one shown as cards on the home page. */
|
|
283
|
+
export const LATEST_CARD_COUNT = 6;
|
|
284
|
+
/** Number of related articles shown in "Keep reading". */
|
|
285
|
+
export const RELATED_COUNT = 3;
|
|
286
|
+
/** Minimum h2 headings an article needs before a contents list renders. */
|
|
287
|
+
export const CONTENTS_MIN_HEADINGS = 2;
|
|
288
|
+
/** Longest generated or truncated `<meta name="description">`. */
|
|
289
|
+
export const DESCRIPTION_MAX = 155;
|
|
290
|
+
/** Longest home-page `<title>` before the tagline is shortened. */
|
|
291
|
+
export const HOME_TITLE_MAX = 60;
|
|
292
|
+
/** Maximum tag pills on the home page. */
|
|
293
|
+
const HOME_TAG_PILL_LIMIT = 12;
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Select the "Keep reading" articles: other published articles ranked by
|
|
297
|
+
* the number of shared tags (most first), then by recency (the input order),
|
|
298
|
+
* at most {@link RELATED_COUNT}.
|
|
299
|
+
*
|
|
300
|
+
* @param {import('../../../types/index.d.ts').ContentBuildRecord} record
|
|
301
|
+
* @param {readonly import('../../../types/index.d.ts').ContentBuildRecord[]} published
|
|
302
|
+
* every published article, newest first
|
|
303
|
+
* @returns {import('../../../types/index.d.ts').ContentBuildRecord[]}
|
|
304
|
+
*/
|
|
305
|
+
export function selectRelatedArticles(record, published) {
|
|
306
|
+
const tags = new Set(record.frontmatter.tags);
|
|
307
|
+
return published
|
|
308
|
+
.filter((other) => other.frontmatter.id !== record.frontmatter.id)
|
|
309
|
+
.map((other, order) => ({
|
|
310
|
+
other,
|
|
311
|
+
order,
|
|
312
|
+
shared: other.frontmatter.tags.filter((tag) => tags.has(tag)).length,
|
|
313
|
+
}))
|
|
314
|
+
.sort((a, b) => b.shared - a.shared || a.order - b.order)
|
|
315
|
+
.slice(0, RELATED_COUNT)
|
|
316
|
+
.map((entry) => entry.other);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* The home page `<title>`: `<Publication> – <tagline>`, with the tagline cut
|
|
321
|
+
* at a word boundary (and an ellipsis) so the whole stays within
|
|
322
|
+
* {@link HOME_TITLE_MAX} characters; the publication name alone when there is
|
|
323
|
+
* no tagline or too little room for a meaningful one.
|
|
324
|
+
*
|
|
325
|
+
* @param {string} name the publication title
|
|
326
|
+
* @param {string | undefined} tagline the publication description
|
|
327
|
+
* @returns {string} the home page title
|
|
328
|
+
*/
|
|
329
|
+
export function homeDocumentTitle(name, tagline) {
|
|
330
|
+
const separator = ' \u2013 ';
|
|
331
|
+
const line = (tagline ?? '').replace(/\s+/g, ' ').trim();
|
|
332
|
+
if (!line) return name;
|
|
333
|
+
if (Array.from(`${name}${separator}${line}`).length <= HOME_TITLE_MAX) {
|
|
334
|
+
return `${name}${separator}${line}`;
|
|
335
|
+
}
|
|
336
|
+
const room = HOME_TITLE_MAX - Array.from(`${name}${separator}`).length - 1;
|
|
337
|
+
if (room < 12) return name;
|
|
338
|
+
const clipped = Array.from(line).slice(0, room).join('');
|
|
339
|
+
const cut = /\s/.test(clipped) ? clipped.replace(/\s+\S*$/, '') : clipped;
|
|
340
|
+
return `${name}${separator}${cut.replace(/[\s,;:.\u2013-]+$/, '')}\u2026`;
|
|
341
|
+
}
|
|
342
|
+
|
|
258
343
|
/**
|
|
259
344
|
* Build every generated page (every kind except `error`) from a validated
|
|
260
345
|
* `build-input:2.0.0` instance.
|
|
261
346
|
*
|
|
262
347
|
* @param {import('../../../types/index.d.ts').NormalizedBuildInput} validatedInput
|
|
348
|
+
* @param {object} [options] rendering options
|
|
349
|
+
* @param {readonly {path: string, mediaType: string}[]} [options.mediaAssets]
|
|
350
|
+
* the media pipeline's finished `assets` list, used to resolve every
|
|
351
|
+
* image reference (hero, avatar) to its emitted URL; an image with no
|
|
352
|
+
* processed derivative renders as the placeholder/monogram fallback
|
|
353
|
+
* @param {Readonly<Record<string, {width: number, height: number}>>} [options.mediaDimensions]
|
|
354
|
+
* the media pipeline's pixel dimensions per derivative path
|
|
263
355
|
* @returns {GeneratedPage[]} every generated page, in a stable, deterministic
|
|
264
356
|
* order
|
|
265
357
|
*/
|
|
266
|
-
export function buildGeneratedPages(validatedInput) {
|
|
358
|
+
export function buildGeneratedPages(validatedInput, options = {}) {
|
|
267
359
|
const { publication, authors, content, basePath, baseUrl } = validatedInput;
|
|
268
360
|
const publicBasePath = derivePublicBasePath(baseUrl, basePath);
|
|
269
361
|
const messages = getMessages(publication.defaultLanguage);
|
|
@@ -274,38 +366,254 @@ export function buildGeneratedPages(validatedInput) {
|
|
|
274
366
|
* @returns {string} the `basePath`-joined absolute route
|
|
275
367
|
*/
|
|
276
368
|
const site = (route) => joinPublicRoute(publicBasePath, route);
|
|
369
|
+
const tagHref = (/** @type {string} */ tag) =>
|
|
370
|
+
site(`/tags/${routeSegmentForLabel(tag)}`);
|
|
371
|
+
const seriesHref = (/** @type {string} */ series) =>
|
|
372
|
+
site(`/series/${routeSegmentForLabel(series)}`);
|
|
373
|
+
const recordHref = (
|
|
374
|
+
/** @type {import('../../../types/index.d.ts').ContentBuildRecord} */ record,
|
|
375
|
+
) => site(contentRoute(record.frontmatter));
|
|
376
|
+
const authorSlugs = deriveAuthorSlugs(authors);
|
|
377
|
+
const authorRoute = (
|
|
378
|
+
/** @type {import('../../../types/index.d.ts').AuthorNormalized} */ author,
|
|
379
|
+
) => `/authors/${authorSlugs.get(author.id) ?? author.id}`;
|
|
380
|
+
const authorHref = (
|
|
381
|
+
/** @type {import('../../../types/index.d.ts').AuthorNormalized} */ author,
|
|
382
|
+
) => site(authorRoute(author));
|
|
383
|
+
const mediaImage = createMediaImage({
|
|
384
|
+
mediaAssets: options.mediaAssets ?? [],
|
|
385
|
+
publicBasePath,
|
|
386
|
+
mediaDimensions: options.mediaDimensions,
|
|
387
|
+
});
|
|
388
|
+
const ui = createComponents({
|
|
389
|
+
messages,
|
|
390
|
+
site,
|
|
391
|
+
mediaImage,
|
|
392
|
+
authorHref,
|
|
393
|
+
authorsById,
|
|
394
|
+
recordHref,
|
|
395
|
+
tagHref,
|
|
396
|
+
});
|
|
397
|
+
const { text, call, chip, byline, card, avatar } = ui;
|
|
277
398
|
|
|
399
|
+
const msg = (/** @type {string} */ key, /** @type {string[]} */ ...args) =>
|
|
400
|
+
/** @type {(...a: string[]) => string} */ (messages[key])(...args);
|
|
401
|
+
const publicationTitle = publication.title;
|
|
402
|
+
/**
|
|
403
|
+
* @param {string} name a bare page name
|
|
404
|
+
* @returns {string} `<Page> | <Publication>`
|
|
405
|
+
*/
|
|
406
|
+
const documentTitleOf = (name) => `${name} | ${publicationTitle}`;
|
|
407
|
+
const absolute = (/** @type {string} */ joinedRoute) =>
|
|
408
|
+
new URL(joinedRoute, baseUrl).toString();
|
|
409
|
+
const absoluteImage = (
|
|
410
|
+
/** @type {{path: string, sourceDigest: string} | undefined} */ ref,
|
|
411
|
+
) => {
|
|
412
|
+
const image = mediaImage(ref);
|
|
413
|
+
return image ? { ...image, url: absolute(image.url) } : undefined;
|
|
414
|
+
};
|
|
415
|
+
const sd = createStructuredData({
|
|
416
|
+
publication,
|
|
417
|
+
absolute,
|
|
418
|
+
brandMark: absoluteImage(validatedInput.appearance.brandMark),
|
|
419
|
+
});
|
|
420
|
+
/** The publication-default social image and its alt, when one is set. */
|
|
421
|
+
const defaultSocial = publication.defaultImage
|
|
422
|
+
? {
|
|
423
|
+
socialImageRef: publication.defaultImage,
|
|
424
|
+
socialImageAlt: publicationTitle,
|
|
425
|
+
}
|
|
426
|
+
: {};
|
|
427
|
+
/**
|
|
428
|
+
* @param {import('../../../types/index.d.ts').ContentFrontmatterNormalized} fm
|
|
429
|
+
* @returns {{socialImageRef?: {path: string, sourceDigest: string}, socialImageAlt?: string}}
|
|
430
|
+
*/
|
|
431
|
+
const socialOfContent = (fm) =>
|
|
432
|
+
fm.socialImage
|
|
433
|
+
? { socialImageRef: fm.socialImage, socialImageAlt: fm.title }
|
|
434
|
+
: fm.hero
|
|
435
|
+
? {
|
|
436
|
+
socialImageRef: fm.hero.file,
|
|
437
|
+
socialImageAlt:
|
|
438
|
+
fm.hero.role === 'informative' && fm.hero.alt
|
|
439
|
+
? fm.hero.alt
|
|
440
|
+
: fm.title,
|
|
441
|
+
}
|
|
442
|
+
: defaultSocial;
|
|
278
443
|
/**
|
|
279
444
|
* @param {import('../../../types/index.d.ts').ContentBuildRecord} record
|
|
280
|
-
* @returns {string}
|
|
445
|
+
* @returns {string} the page description: authored, else the body's
|
|
446
|
+
* opening words
|
|
281
447
|
*/
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
)
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
`<p>${byLine}</p>${time}${tags}</li>`
|
|
448
|
+
const contentDescription = (record) =>
|
|
449
|
+
record.frontmatter.description ||
|
|
450
|
+
truncateAtWord(plainText(record.body), DESCRIPTION_MAX);
|
|
451
|
+
|
|
452
|
+
const text0 = (/** @type {string} */ key) =>
|
|
453
|
+
/** @type {string} */ (messages[key]);
|
|
454
|
+
const homeStep = {
|
|
455
|
+
label: /** @type {string} */ (messages.homeLinkLabel),
|
|
456
|
+
route: site('/'),
|
|
457
|
+
};
|
|
458
|
+
const crumbs = (
|
|
459
|
+
/** @type {import('./skeleton.js').BreadcrumbStep[][]} */ ...groups
|
|
460
|
+
) => renderBreadcrumbs({ trail: [homeStep, ...groups.flat()], messages });
|
|
461
|
+
|
|
462
|
+
const publishedArticles = selectPublishedArticles(content);
|
|
463
|
+
const byTag = groupBy(publishedArticles, (r) => r.frontmatter.tags);
|
|
464
|
+
const bySeries = groupBy(
|
|
465
|
+
content.filter((r) => r.frontmatter.status === 'published'),
|
|
466
|
+
(r) => (r.frontmatter.series ? [r.frontmatter.series] : []),
|
|
467
|
+
);
|
|
468
|
+
for (const bucket of bySeries.values()) {
|
|
469
|
+
bucket.sort(
|
|
470
|
+
(a, b) =>
|
|
471
|
+
(a.frontmatter.seriesOrder ?? 0) - (b.frontmatter.seriesOrder ?? 0),
|
|
307
472
|
);
|
|
473
|
+
}
|
|
474
|
+
const sortedTags = [...byTag.keys()].sort();
|
|
475
|
+
const sortedSeries = [...bySeries.keys()].sort();
|
|
476
|
+
const byYear = groupBy(publishedArticles, (r) => [
|
|
477
|
+
r.frontmatter.publishedAt.slice(0, 4),
|
|
478
|
+
]);
|
|
479
|
+
const sortedYears = [...byYear.keys()].sort().reverse();
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* @param {import('../../../types/index.d.ts').ContentBuildRecord} record
|
|
483
|
+
* @returns {string | undefined} "Part N of M" when the record is a
|
|
484
|
+
* published member of its series
|
|
485
|
+
*/
|
|
486
|
+
const partLabelOf = (record) => {
|
|
487
|
+
const series = record.frontmatter.series;
|
|
488
|
+
const members = series ? bySeries.get(series) : undefined;
|
|
489
|
+
const index = members ? members.indexOf(record) : -1;
|
|
490
|
+
return members && index >= 0
|
|
491
|
+
? /** @type {(a: string, b: string) => string} */ (
|
|
492
|
+
messages.seriesPartLabel
|
|
493
|
+
)(String(index + 1), String(members.length))
|
|
494
|
+
: undefined;
|
|
308
495
|
};
|
|
496
|
+
/**
|
|
497
|
+
* @param {import('../../../types/index.d.ts').ContentBuildRecord} record
|
|
498
|
+
* @param {'grid' | 'row'} [variant]
|
|
499
|
+
* @param {number} [headingLevel] the title's heading level
|
|
500
|
+
* @returns {string}
|
|
501
|
+
*/
|
|
502
|
+
const cardOf = (record, variant = 'grid', headingLevel = 3) =>
|
|
503
|
+
card(record, { partLabel: partLabelOf(record), headingLevel }, variant);
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* @param {string} href
|
|
507
|
+
* @param {string} label
|
|
508
|
+
* @param {number | undefined} count
|
|
509
|
+
* @param {string} iconName
|
|
510
|
+
* @param {boolean} [current]
|
|
511
|
+
* @returns {string}
|
|
512
|
+
*/
|
|
513
|
+
const pill = (href, label, count, iconName, current = false) =>
|
|
514
|
+
`<li><a class="g-pill" href="${escapeHtml(href)}"${current ? ' aria-current="page"' : ''}>${icon(iconName)}` +
|
|
515
|
+
`<span>${escapeHtml(label)}</span>` +
|
|
516
|
+
(count === undefined ? '' : `<span class="g-pill-count">${count}</span>`) +
|
|
517
|
+
`</a></li>`;
|
|
518
|
+
/**
|
|
519
|
+
* @param {string} [currentTag]
|
|
520
|
+
* @returns {string}
|
|
521
|
+
*/
|
|
522
|
+
const tagPills = (currentTag = '') =>
|
|
523
|
+
`<ul class="g-pills">${sortedTags
|
|
524
|
+
.map((tag) =>
|
|
525
|
+
pill(
|
|
526
|
+
tagHref(tag),
|
|
527
|
+
tag,
|
|
528
|
+
byTag.get(tag)?.length,
|
|
529
|
+
'hash',
|
|
530
|
+
tag === currentTag,
|
|
531
|
+
),
|
|
532
|
+
)
|
|
533
|
+
.join('')}</ul>`;
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* The header block shared by tag, series, archive and author pages and
|
|
537
|
+
* their root listings.
|
|
538
|
+
*
|
|
539
|
+
* @param {object} head
|
|
540
|
+
* @param {string} head.breadcrumbHtml
|
|
541
|
+
* @param {string} head.kicker the label before the count
|
|
542
|
+
* @param {number | undefined} head.count article count
|
|
543
|
+
* @param {string} head.title the page's `<h1>` text
|
|
544
|
+
* @param {string} [head.descriptionHtml] already-rendered intro markup
|
|
545
|
+
* @param {string} [head.markHtml] a leading avatar instead of an icon
|
|
546
|
+
* @param {string} [head.iconName]
|
|
547
|
+
* @param {string} [head.extraHtml] markup after the intro (pills)
|
|
548
|
+
* @returns {string}
|
|
549
|
+
*/
|
|
550
|
+
const listingHead = ({
|
|
551
|
+
breadcrumbHtml,
|
|
552
|
+
kicker,
|
|
553
|
+
count,
|
|
554
|
+
title,
|
|
555
|
+
descriptionHtml = '',
|
|
556
|
+
markHtml,
|
|
557
|
+
iconName = 'hash',
|
|
558
|
+
extraHtml = '',
|
|
559
|
+
}) =>
|
|
560
|
+
`<section class="g-wrap g-topic-hero">${breadcrumbHtml}` +
|
|
561
|
+
`<div class="g-topic-head"><span class="g-topic-icon">${markHtml ?? icon(iconName)}</span>` +
|
|
562
|
+
`<div><p class="g-label">${escapeHtml(kicker)}` +
|
|
563
|
+
(count === undefined
|
|
564
|
+
? ''
|
|
565
|
+
: `<span class="g-dot" aria-hidden="true"></span>${call('articleCountLabel', String(count))}`) +
|
|
566
|
+
`</p><h1>${escapeHtml(title)}</h1></div></div>` +
|
|
567
|
+
descriptionHtml +
|
|
568
|
+
extraHtml +
|
|
569
|
+
`</section>`;
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* @param {readonly import('../../../types/index.d.ts').ContentBuildRecord[]} items
|
|
573
|
+
* @param {string} [afterHtml] markup after the list (pagination)
|
|
574
|
+
* @returns {string}
|
|
575
|
+
*/
|
|
576
|
+
const rowList = (items, afterHtml = '') =>
|
|
577
|
+
`<section class="g-wrap g-section">` +
|
|
578
|
+
(items.length > 0
|
|
579
|
+
? `<div class="g-list">${items.map((record) => cardOf(record, 'row', 2)).join('')}</div>`
|
|
580
|
+
: `<p>${text('emptyListingLabel')}</p>`) +
|
|
581
|
+
afterHtml +
|
|
582
|
+
`</section>`;
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* One listing page's metadata: bare title, `<title>`, description,
|
|
586
|
+
* social image and JSON-LD.
|
|
587
|
+
*
|
|
588
|
+
* @param {object} spec
|
|
589
|
+
* @param {string} spec.name the bare page name
|
|
590
|
+
* @param {string} spec.description the meta description
|
|
591
|
+
* @param {string} spec.route the un-joined route
|
|
592
|
+
* @param {readonly {label: string, route?: string}[]} [spec.parents]
|
|
593
|
+
* breadcrumb steps between home and this page (joined routes)
|
|
594
|
+
* @returns {Pick<GeneratedPage, 'title' | 'documentTitle' | 'description' | 'ogType' | 'socialImageRef' | 'socialImageAlt' | 'jsonLd'>}
|
|
595
|
+
*/
|
|
596
|
+
const listingMeta = ({ name, description, route, parents = [] }) => ({
|
|
597
|
+
title: name,
|
|
598
|
+
documentTitle: documentTitleOf(name),
|
|
599
|
+
description,
|
|
600
|
+
ogType: 'website',
|
|
601
|
+
...defaultSocial,
|
|
602
|
+
jsonLd: sd.serialize(
|
|
603
|
+
sd.collection({
|
|
604
|
+
name,
|
|
605
|
+
description,
|
|
606
|
+
url: absolute(site(route)),
|
|
607
|
+
language: publication.defaultLanguage,
|
|
608
|
+
trail: [homeStep, ...parents, { label: name, route: site(route) }],
|
|
609
|
+
}),
|
|
610
|
+
),
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
const newsletterHtml = renderNewsletterPanel({
|
|
614
|
+
newsletter: /** @type {{newsletter?: unknown}} */ (publication).newsletter,
|
|
615
|
+
messages,
|
|
616
|
+
});
|
|
309
617
|
|
|
310
618
|
/** @type {GeneratedPage[]} */
|
|
311
619
|
const pages = [];
|
|
@@ -315,182 +623,474 @@ export function buildGeneratedPages(validatedInput) {
|
|
|
315
623
|
kind: 'profile',
|
|
316
624
|
route: publication.profile.route,
|
|
317
625
|
title: publication.title,
|
|
626
|
+
documentTitle: documentTitleOf(text0('aboutPageTitle')),
|
|
318
627
|
language: publication.defaultLanguage,
|
|
319
628
|
description: publication.description,
|
|
320
629
|
ogType: 'website',
|
|
321
|
-
|
|
322
|
-
|
|
630
|
+
...defaultSocial,
|
|
631
|
+
jsonLd: sd.serialize(
|
|
632
|
+
sd.webPage({
|
|
633
|
+
name: publication.title,
|
|
634
|
+
description: publication.description,
|
|
635
|
+
url: absolute(site(publication.profile.route)),
|
|
636
|
+
language: publication.defaultLanguage,
|
|
637
|
+
trail: [
|
|
638
|
+
homeStep,
|
|
639
|
+
{
|
|
640
|
+
label: text0('aboutPageTitle'),
|
|
641
|
+
route: site(publication.profile.route),
|
|
642
|
+
},
|
|
643
|
+
],
|
|
644
|
+
}),
|
|
645
|
+
),
|
|
646
|
+
bodyHtml:
|
|
647
|
+
`<article class="g-article"><div class="g-wrap g-article-head">` +
|
|
648
|
+
`<h1>${escapeHtml(publication.title)}</h1></div>` +
|
|
649
|
+
`<div class="g-wrap g-article-grid"><div class="g-prose">${lazyBodyImages(publication.profile.body.body)}</div></div></article>`,
|
|
323
650
|
});
|
|
324
651
|
}
|
|
325
652
|
|
|
326
653
|
for (const author of authors) {
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const biography = author.biography
|
|
331
|
-
? `<p>${escapeHtml(author.biography)}</p>`
|
|
332
|
-
: '';
|
|
654
|
+
const authored = publishedArticles.filter((record) =>
|
|
655
|
+
record.frontmatter.authorIds.includes(author.id),
|
|
656
|
+
);
|
|
333
657
|
const links =
|
|
334
658
|
author.links.length > 0
|
|
335
|
-
? `<ul>${author.links
|
|
659
|
+
? `<ul class="g-social">${author.links
|
|
336
660
|
.map((link) => {
|
|
337
661
|
const label = escapeHtml(link.label ?? link.uri);
|
|
338
|
-
return `<li><a href="${escapeHtml(link.uri)}"
|
|
662
|
+
return `<li><a class="g-icon-btn" href="${escapeHtml(link.uri)}" aria-label="${label}" title="${label}">${icon('upright')}</a></li>`;
|
|
339
663
|
})
|
|
340
664
|
.join('')}</ul>`
|
|
341
665
|
: '';
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
666
|
+
const intro =
|
|
667
|
+
(author.pronouns
|
|
668
|
+
? `<p class="g-label">${escapeHtml(author.pronouns)}</p>`
|
|
669
|
+
: '') +
|
|
670
|
+
(author.biography
|
|
671
|
+
? `<p class="g-dek">${escapeHtml(author.biography)}</p>`
|
|
672
|
+
: '') +
|
|
673
|
+
links;
|
|
345
674
|
pages.push({
|
|
346
675
|
kind: 'author',
|
|
347
|
-
route:
|
|
348
|
-
title:
|
|
676
|
+
route: authorRoute(author),
|
|
677
|
+
title: author.displayName,
|
|
678
|
+
documentTitle: documentTitleOf(author.displayName),
|
|
349
679
|
language: publication.defaultLanguage,
|
|
350
|
-
description:
|
|
680
|
+
description:
|
|
681
|
+
truncateAtWord(author.biography, DESCRIPTION_MAX) ||
|
|
682
|
+
msg('authorPageDescription', author.displayName, publicationTitle),
|
|
351
683
|
ogType: 'profile',
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
684
|
+
...(author.avatar
|
|
685
|
+
? {
|
|
686
|
+
socialImageRef: author.avatar,
|
|
687
|
+
socialImageAlt: author.displayName,
|
|
688
|
+
}
|
|
689
|
+
: defaultSocial),
|
|
690
|
+
jsonLd: sd.serialize(
|
|
691
|
+
sd.profile({
|
|
692
|
+
name: author.displayName,
|
|
693
|
+
description: author.biography || undefined,
|
|
694
|
+
url: absolute(authorHref(author)),
|
|
695
|
+
language: publication.defaultLanguage,
|
|
696
|
+
image: absoluteImage(author.avatar),
|
|
697
|
+
sameAs: author.links.map((link) => link.uri),
|
|
698
|
+
}),
|
|
699
|
+
),
|
|
364
700
|
bodyHtml:
|
|
365
|
-
|
|
701
|
+
listingHead({
|
|
702
|
+
breadcrumbHtml: crumbs(
|
|
703
|
+
[{ label: /** @type {string} */ (messages.authorsSectionLabel) }],
|
|
704
|
+
[{ label: author.displayName }],
|
|
705
|
+
),
|
|
706
|
+
kicker: /** @type {string} */ (messages.authorKickerLabel),
|
|
707
|
+
count: authored.length,
|
|
708
|
+
title: author.displayName,
|
|
709
|
+
descriptionHtml: intro,
|
|
710
|
+
markHtml: avatar(author),
|
|
711
|
+
}) + rowList(authored),
|
|
366
712
|
});
|
|
367
713
|
}
|
|
368
714
|
|
|
369
715
|
for (const record of content) {
|
|
370
716
|
const { frontmatter } = record;
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
717
|
+
const isArticle = frontmatter.kind === 'article';
|
|
718
|
+
const hero = mediaImage(frontmatter.hero?.file);
|
|
719
|
+
const headings = extractH2Headings(record.body);
|
|
720
|
+
const contents =
|
|
721
|
+
headings.length >= CONTENTS_MIN_HEADINGS
|
|
722
|
+
? `<ol class="g-toc-list">${headings
|
|
723
|
+
.map(
|
|
724
|
+
(heading) =>
|
|
725
|
+
`<li><a href="#${escapeHtml(heading.id)}" data-toc="${escapeHtml(heading.id)}">${escapeHtml(heading.text)}</a></li>`,
|
|
726
|
+
)
|
|
727
|
+
.join('')}</ol>`
|
|
728
|
+
: '';
|
|
729
|
+
const firstTag = frontmatter.tags[0];
|
|
730
|
+
const trail = [
|
|
731
|
+
...(isArticle && firstTag
|
|
732
|
+
? [{ label: firstTag, route: tagHref(firstTag) }]
|
|
733
|
+
: []),
|
|
734
|
+
{ label: frontmatter.title },
|
|
735
|
+
];
|
|
736
|
+
const partLabel = partLabelOf(record);
|
|
737
|
+
const series = frontmatter.series;
|
|
738
|
+
const seriesTag = series
|
|
739
|
+
? `<a class="g-series-tag" href="${escapeHtml(seriesHref(series))}">${icon('book')}` +
|
|
740
|
+
(partLabel
|
|
741
|
+
? `${escapeHtml(partLabel)}<span class="g-dot" aria-hidden="true"></span>`
|
|
742
|
+
: '') +
|
|
743
|
+
`${escapeHtml(series)}</a>`
|
|
744
|
+
: '';
|
|
745
|
+
const head =
|
|
746
|
+
`<div class="g-wrap g-article-head">${renderBreadcrumbs({ trail: [homeStep, ...trail], messages })}` +
|
|
747
|
+
(isArticle
|
|
748
|
+
? `<div class="g-article-labels">${firstTag ? chip(firstTag) : ''}${seriesTag}</div>`
|
|
749
|
+
: '') +
|
|
750
|
+
`<h1>${escapeHtml(frontmatter.title)}</h1>` +
|
|
751
|
+
(frontmatter.description
|
|
752
|
+
? `<p class="g-dek">${escapeHtml(frontmatter.description)}</p>`
|
|
753
|
+
: '') +
|
|
754
|
+
(isArticle
|
|
755
|
+
? `<div class="g-article-meta">${byline(record, true)}` +
|
|
756
|
+
`<div class="g-share" role="group" aria-label="${text('shareLabel')}" hidden>` +
|
|
757
|
+
`<button class="g-icon-btn" type="button" data-action="copy-link" aria-label="${text('copyLinkLabel')}" title="${text('copyLinkLabel')}">${icon('link')}</button>` +
|
|
758
|
+
`<button class="g-icon-btn" type="button" data-action="bookmark" aria-pressed="false" aria-label="${text('saveForLaterLabel')}" title="${text('saveForLaterLabel')}">${icon('bookmark')}</button>` +
|
|
759
|
+
`</div></div>`
|
|
760
|
+
: '') +
|
|
761
|
+
`</div>`;
|
|
762
|
+
const cover = hero
|
|
763
|
+
? `<figure class="g-wrap g-article-cover">${renderImage({ image: hero, alt: frontmatter.hero?.role === 'decorative' ? '' : (frontmatter.hero?.alt ?? ''), priority: true })}</figure>`
|
|
764
|
+
: '';
|
|
765
|
+
const tocNav = contents
|
|
766
|
+
? `<nav class="g-toc" aria-label="${text('onThisPageLabel')}"><p class="g-label">${icon('list')}${text('onThisPageLabel')}</p>${contents}</nav>`
|
|
767
|
+
: '';
|
|
768
|
+
const tocMobile = contents
|
|
769
|
+
? `<details class="g-toc-mobile"><summary>${icon('list')}${text('onThisPageLabel')}</summary>${contents}</details>`
|
|
770
|
+
: '';
|
|
380
771
|
const tags =
|
|
381
|
-
frontmatter.tags.length > 0
|
|
382
|
-
? `<ul>${frontmatter.tags
|
|
772
|
+
isArticle && frontmatter.tags.length > 0
|
|
773
|
+
? `<ul class="g-tags" aria-label="${text('tagListLabel')}">${frontmatter.tags
|
|
383
774
|
.map(
|
|
384
775
|
(tag) =>
|
|
385
|
-
`<li><a href="${escapeHtml(
|
|
776
|
+
`<li><a class="g-tag" href="${escapeHtml(tagHref(tag))}">${icon('hash')}${escapeHtml(tag)}</a></li>`,
|
|
386
777
|
)
|
|
387
778
|
.join('')}</ul>`
|
|
388
779
|
: '';
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
{
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
780
|
+
const prose =
|
|
781
|
+
`<div class="g-prose">${tocMobile}` +
|
|
782
|
+
renderSlot('article-preamble') +
|
|
783
|
+
lazyBodyImages(record.body) +
|
|
784
|
+
tags +
|
|
785
|
+
renderSlot('article-end') +
|
|
786
|
+
renderSlot('article-footer-ad', { collapsed: true }) +
|
|
787
|
+
`</div>`;
|
|
788
|
+
|
|
789
|
+
let foot = '';
|
|
790
|
+
let after = '';
|
|
791
|
+
if (isArticle) {
|
|
792
|
+
const authorCards = ui
|
|
793
|
+
.recordAuthors(record)
|
|
794
|
+
.map(
|
|
795
|
+
(author) =>
|
|
796
|
+
`<section class="g-author-card" aria-label="${text('aboutTheAuthorLabel')}">${avatar(author)}<div>` +
|
|
797
|
+
`<p class="g-label">${text('writtenByLabel')}</p>` +
|
|
798
|
+
`<p class="g-author-name"><a href="${escapeHtml(authorHref(author))}">${escapeHtml(author.displayName)}</a></p>` +
|
|
799
|
+
(author.biography
|
|
800
|
+
? `<p class="g-author-bio">${escapeHtml(author.biography)}</p>`
|
|
801
|
+
: '') +
|
|
802
|
+
`</div></section>`,
|
|
803
|
+
)
|
|
804
|
+
.join('');
|
|
805
|
+
const members = series ? bySeries.get(series) : undefined;
|
|
806
|
+
const seriesBox =
|
|
807
|
+
series && members && members.includes(record)
|
|
808
|
+
? `<section class="g-series-box" aria-label="${escapeHtml(series)}"><p class="g-label">${icon('book')}${escapeHtml(series)}</p><ol class="g-series-list">` +
|
|
809
|
+
members
|
|
810
|
+
.map((member, index) => {
|
|
811
|
+
const here = member === record;
|
|
812
|
+
return (
|
|
813
|
+
`<li${here ? ' aria-current="true"' : ''}><a href="${escapeHtml(recordHref(member))}">` +
|
|
814
|
+
`<span class="g-series-n">${index + 1}</span><span>${escapeHtml(member.frontmatter.title)}</span>` +
|
|
815
|
+
(here
|
|
816
|
+
? `<span class="g-series-here">${text('youAreHereLabel')}</span>`
|
|
817
|
+
: '') +
|
|
818
|
+
`</a></li>`
|
|
819
|
+
);
|
|
820
|
+
})
|
|
821
|
+
.join('') +
|
|
822
|
+
`</ol></section>`
|
|
823
|
+
: '';
|
|
824
|
+
const position = publishedArticles.indexOf(record);
|
|
825
|
+
const newer = position > 0 ? publishedArticles[position - 1] : undefined;
|
|
826
|
+
const older = position >= 0 ? publishedArticles[position + 1] : undefined;
|
|
827
|
+
const pagerLink = (
|
|
828
|
+
/** @type {import('../../../types/index.d.ts').ContentBuildRecord} */ target,
|
|
829
|
+
/** @type {boolean} */ isOlder,
|
|
830
|
+
) =>
|
|
831
|
+
`<a class="g-pager-link${isOlder ? ' g-pager-next' : ''}" rel="${isOlder ? 'next' : 'prev'}" href="${escapeHtml(recordHref(target))}">` +
|
|
832
|
+
`<span class="g-label">${isOlder ? `${text('olderArticleLabel')}${icon('arrow')}` : `${icon('back')}${text('newerArticleLabel')}`}</span>` +
|
|
833
|
+
`<span>${escapeHtml(target.frontmatter.title)}</span></a>`;
|
|
834
|
+
const pager =
|
|
835
|
+
newer || older
|
|
836
|
+
? `<nav class="g-pager" aria-label="${text('moreArticlesNavigationLabel')}">${newer ? pagerLink(newer, false) : ''}${older ? pagerLink(older, true) : ''}</nav>`
|
|
837
|
+
: '';
|
|
838
|
+
foot = `<div class="g-wrap g-article-foot">${authorCards}${seriesBox}${pager}</div>`;
|
|
839
|
+
const related = selectRelatedArticles(record, publishedArticles);
|
|
840
|
+
after =
|
|
841
|
+
related.length > 0
|
|
842
|
+
? `<section class="g-wrap g-section" aria-labelledby="related-title"><div class="g-section-head">` +
|
|
843
|
+
`<h2 id="related-title">${text('keepReadingHeading')}</h2>` +
|
|
844
|
+
(firstTag
|
|
845
|
+
? `<a class="g-more" href="${escapeHtml(tagHref(firstTag))}">${call('moreInTagLabel', firstTag)}${icon('arrow')}</a>`
|
|
846
|
+
: '') +
|
|
847
|
+
`</div><div class="g-grid">${related.map((r) => cardOf(r)).join('')}</div></section>`
|
|
848
|
+
: '';
|
|
849
|
+
}
|
|
850
|
+
const ownUrl = absolute(recordHref(record));
|
|
851
|
+
const recordAuthors = ui.recordAuthors(record);
|
|
852
|
+
const articleTrail = [
|
|
853
|
+
homeStep,
|
|
854
|
+
...trail.slice(0, -1),
|
|
855
|
+
{ label: frontmatter.title, route: recordHref(record) },
|
|
856
|
+
];
|
|
857
|
+
const articleGraph = isArticle
|
|
858
|
+
? sd.article({
|
|
859
|
+
url: ownUrl,
|
|
860
|
+
headline: frontmatter.title,
|
|
861
|
+
description: contentDescription(record),
|
|
862
|
+
image: absoluteImage(socialOfContent(frontmatter).socialImageRef),
|
|
863
|
+
datePublished: frontmatter.publishedAt,
|
|
864
|
+
dateModified: contentLastModified(frontmatter),
|
|
865
|
+
language: frontmatter.language,
|
|
866
|
+
authors: recordAuthors.map((author) => ({
|
|
867
|
+
name: author.displayName,
|
|
868
|
+
url: absolute(authorHref(author)),
|
|
869
|
+
sameAs: author.links.map((link) => link.uri),
|
|
870
|
+
})),
|
|
871
|
+
tags: frontmatter.tags,
|
|
872
|
+
series: series
|
|
873
|
+
? { name: series, url: absolute(seriesHref(series)) }
|
|
874
|
+
: undefined,
|
|
875
|
+
trail: articleTrail,
|
|
876
|
+
})
|
|
877
|
+
: sd.webPage({
|
|
878
|
+
name: frontmatter.title,
|
|
879
|
+
description: contentDescription(record),
|
|
880
|
+
url: ownUrl,
|
|
881
|
+
language: frontmatter.language,
|
|
882
|
+
trail: articleTrail,
|
|
883
|
+
dateModified: contentLastModified(frontmatter),
|
|
884
|
+
});
|
|
412
885
|
pages.push({
|
|
413
886
|
kind: frontmatter.kind,
|
|
414
887
|
route: contentRoute(frontmatter),
|
|
415
888
|
title: frontmatter.title,
|
|
889
|
+
documentTitle: documentTitleOf(frontmatter.title),
|
|
416
890
|
language: frontmatter.language,
|
|
417
|
-
description:
|
|
418
|
-
ogType:
|
|
419
|
-
|
|
891
|
+
description: contentDescription(record),
|
|
892
|
+
ogType: isArticle ? 'article' : 'website',
|
|
893
|
+
...socialOfContent(frontmatter),
|
|
894
|
+
jsonLd: sd.serialize(articleGraph),
|
|
895
|
+
articleMeta: isArticle
|
|
896
|
+
? {
|
|
897
|
+
publishedTime: frontmatter.publishedAt,
|
|
898
|
+
modifiedTime: frontmatter.updatedAt,
|
|
899
|
+
authorUrls: recordAuthors.map((author) =>
|
|
900
|
+
absolute(authorHref(author)),
|
|
901
|
+
),
|
|
902
|
+
tags: [...frontmatter.tags],
|
|
903
|
+
}
|
|
904
|
+
: undefined,
|
|
420
905
|
robotsContent:
|
|
421
906
|
frontmatter.status === 'unlisted' ? 'noindex, follow' : undefined,
|
|
422
907
|
lastModified: contentLastModified(frontmatter),
|
|
423
|
-
breadcrumbHtml: renderBreadcrumbs({ trail: breadcrumbTrail, messages }),
|
|
424
908
|
bodyHtml:
|
|
425
|
-
`<article
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
time +
|
|
430
|
-
seriesHtml +
|
|
431
|
-
tags +
|
|
432
|
-
record.body +
|
|
433
|
-
renderSlot('article-end') +
|
|
434
|
-
renderSlot('article-footer-ad', { collapsed: true }) +
|
|
435
|
-
`</article>`,
|
|
909
|
+
`<article class="g-article">${isArticle ? '<div class="g-progress" aria-hidden="true"></div>' : ''}${head}${cover}` +
|
|
910
|
+
`<div class="g-wrap g-article-grid">${tocNav}${prose}</div>${foot}</article>` +
|
|
911
|
+
after +
|
|
912
|
+
(isArticle ? newsletterHtml : ''),
|
|
436
913
|
});
|
|
437
914
|
}
|
|
438
915
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
916
|
+
// Index: page 1 is the home page (featured, tag pills, latest cards, series
|
|
917
|
+
// card, newsletter); every index page carries a slice of the older
|
|
918
|
+
// articles with pagination.
|
|
919
|
+
const [featured, ...rest] = publishedArticles;
|
|
920
|
+
const latest = rest.slice(0, LATEST_CARD_COUNT);
|
|
921
|
+
const older = rest.slice(LATEST_CARD_COUNT);
|
|
922
|
+
const indexPages = paginate(older, LISTING_PAGE_SIZE);
|
|
923
|
+
const indexRoute = (/** @type {number} */ n) =>
|
|
924
|
+
site(n === 1 ? '/' : `/page/${n}`);
|
|
925
|
+
const seriesName = publishedArticles.find((r) => r.frontmatter.series)
|
|
926
|
+
?.frontmatter.series;
|
|
927
|
+
const seriesParts = (seriesName ? bySeries.get(seriesName) : undefined) ?? [];
|
|
928
|
+
const homeFeatured = featured
|
|
929
|
+
? (() => {
|
|
930
|
+
const fm = featured.frontmatter;
|
|
931
|
+
const hero = mediaImage(fm.hero?.file);
|
|
932
|
+
const href = escapeHtml(recordHref(featured));
|
|
933
|
+
return (
|
|
934
|
+
`<section class="g-wrap g-hero" aria-labelledby="hero-title">` +
|
|
935
|
+
`<div class="g-hero-media">` +
|
|
936
|
+
(hero
|
|
937
|
+
? renderImage({
|
|
938
|
+
image: hero,
|
|
939
|
+
alt: fm.hero?.role === 'decorative' ? '' : (fm.hero?.alt ?? ''),
|
|
940
|
+
priority: true,
|
|
941
|
+
})
|
|
942
|
+
: `<span class="g-card-placeholder">${icon('sparkle')}</span>`) +
|
|
943
|
+
`</div><div class="g-hero-body"><p class="g-eyebrow"><span class="g-badge">${icon('sparkle')}${text('featuredLabel')}</span>` +
|
|
944
|
+
(fm.tags[0] ? chip(fm.tags[0]) : '') +
|
|
945
|
+
`</p><h2 id="hero-title"><a href="${href}">${escapeHtml(fm.title)}</a></h2>` +
|
|
946
|
+
(fm.description
|
|
947
|
+
? `<p class="g-dek">${escapeHtml(fm.description)}</p>`
|
|
948
|
+
: '') +
|
|
949
|
+
byline(featured) +
|
|
950
|
+
`<a class="g-btn" href="${href}">${text('readEssayLabel')}${icon('arrow')}</a></div></section>`
|
|
951
|
+
);
|
|
952
|
+
})()
|
|
953
|
+
: `<section class="g-wrap g-topic-hero"><h2>${text('indexHeading')}</h2><p>${text('emptyListingLabel')}</p></section>`;
|
|
954
|
+
const homeHeading = `<h1 class="g-sr">${escapeHtml(publicationTitle)}</h1>`;
|
|
955
|
+
const homeTags =
|
|
956
|
+
sortedTags.length > 0
|
|
957
|
+
? `<section class="g-wrap g-topic-strip" aria-label="${text('browseByTagLabel')}"><ul class="g-pills">${[
|
|
958
|
+
...sortedTags,
|
|
959
|
+
]
|
|
960
|
+
.sort(
|
|
961
|
+
(a, b) =>
|
|
962
|
+
(byTag.get(b)?.length ?? 0) - (byTag.get(a)?.length ?? 0) ||
|
|
963
|
+
(a < b ? -1 : 1),
|
|
964
|
+
)
|
|
965
|
+
.slice(0, HOME_TAG_PILL_LIMIT)
|
|
966
|
+
.map((tag) => pill(tagHref(tag), tag, byTag.get(tag)?.length, 'hash'))
|
|
967
|
+
.join('')}</ul></section>`
|
|
968
|
+
: '';
|
|
969
|
+
const homeLatest =
|
|
970
|
+
latest.length > 0
|
|
971
|
+
? `<section class="g-wrap g-section" aria-labelledby="latest-title"><div class="g-section-head"><h2 id="latest-title">${text('latestHeading')}</h2>` +
|
|
972
|
+
`<a class="g-more" href="${escapeHtml(site('/archive'))}">${text('archiveSectionLabel')}${icon('arrow')}</a></div>` +
|
|
973
|
+
`<div class="g-grid">${latest.map((r) => cardOf(r)).join('')}</div></section>`
|
|
974
|
+
: '';
|
|
975
|
+
const homeSeries =
|
|
976
|
+
seriesName && seriesParts.length > 0
|
|
977
|
+
? `<section class="g-wrap g-section"><div class="g-series-card">` +
|
|
978
|
+
`<p class="g-label">${icon('book')}${text('seriesCardLabel')}<span class="g-dot" aria-hidden="true"></span>${call('partsCountLabel', String(seriesParts.length))}</p>` +
|
|
979
|
+
`<h2><a href="${escapeHtml(seriesHref(seriesName))}">${escapeHtml(seriesName)}</a></h2>` +
|
|
980
|
+
`<ol class="g-series-list">${seriesParts
|
|
981
|
+
.map(
|
|
982
|
+
(member, index) =>
|
|
983
|
+
`<li><a href="${escapeHtml(recordHref(member))}"><span class="g-series-n">${index + 1}</span><span>${escapeHtml(member.frontmatter.title)}</span></a></li>`,
|
|
984
|
+
)
|
|
985
|
+
.join('')}</ol>` +
|
|
986
|
+
`<a class="g-btn" href="${escapeHtml(recordHref(seriesParts[0]))}">${text('startSeriesLabel')}${icon('arrow')}</a></div></section>`
|
|
987
|
+
: '';
|
|
442
988
|
indexPages.forEach((items, pageIndex) => {
|
|
443
989
|
const pageNumber = pageIndex + 1;
|
|
444
|
-
const
|
|
990
|
+
const pagination = renderPagination({
|
|
991
|
+
currentPage: pageNumber,
|
|
992
|
+
totalPages: indexPages.length,
|
|
993
|
+
routeForPage: indexRoute,
|
|
994
|
+
messages,
|
|
995
|
+
});
|
|
996
|
+
const moreList =
|
|
997
|
+
items.length > 0
|
|
998
|
+
? `<section class="g-wrap g-section"><div class="g-section-head"><h2>${text('moreArticlesHeading')}</h2></div>` +
|
|
999
|
+
`<div class="g-list">${items.map((r) => cardOf(r, 'row')).join('')}</div>${pagination}</section>`
|
|
1000
|
+
: '';
|
|
1001
|
+
const bodyHtml =
|
|
1002
|
+
pageNumber === 1
|
|
1003
|
+
? homeHeading +
|
|
1004
|
+
homeFeatured +
|
|
1005
|
+
homeTags +
|
|
1006
|
+
homeLatest +
|
|
1007
|
+
homeSeries +
|
|
1008
|
+
moreList +
|
|
1009
|
+
newsletterHtml
|
|
1010
|
+
: listingHead({
|
|
1011
|
+
breadcrumbHtml: crumbs([
|
|
1012
|
+
{ label: /** @type {string} */ (messages.indexHeading) },
|
|
1013
|
+
]),
|
|
1014
|
+
kicker: /** @type {(c: string, t: string) => string} */ (
|
|
1015
|
+
messages.pageStatusLabel
|
|
1016
|
+
)(String(pageNumber), String(indexPages.length)),
|
|
1017
|
+
count: undefined,
|
|
1018
|
+
title: /** @type {string} */ (messages.indexHeading),
|
|
1019
|
+
iconName: 'layers',
|
|
1020
|
+
}) + rowList(items, pagination);
|
|
1021
|
+
const indexTitle =
|
|
1022
|
+
pageNumber === 1
|
|
1023
|
+
? publicationTitle
|
|
1024
|
+
: `${text0('indexHeading')} \u2013 ${msg('pageStatusLabel', String(pageNumber), String(indexPages.length))}`;
|
|
1025
|
+
const indexUrl = absolute(indexRoute(pageNumber));
|
|
445
1026
|
pages.push({
|
|
446
1027
|
kind: 'index',
|
|
447
|
-
route
|
|
448
|
-
title:
|
|
1028
|
+
route: pageNumber === 1 ? '/' : `/page/${pageNumber}`,
|
|
1029
|
+
title: indexTitle,
|
|
1030
|
+
documentTitle:
|
|
1031
|
+
pageNumber === 1
|
|
1032
|
+
? homeDocumentTitle(publicationTitle, publication.description)
|
|
1033
|
+
: documentTitleOf(indexTitle),
|
|
449
1034
|
language: publication.defaultLanguage,
|
|
450
|
-
description:
|
|
1035
|
+
description:
|
|
1036
|
+
pageNumber === 1
|
|
1037
|
+
? publication.description
|
|
1038
|
+
: msg('indexPageDescription', publicationTitle),
|
|
451
1039
|
ogType: 'website',
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
1040
|
+
...defaultSocial,
|
|
1041
|
+
jsonLd: sd.serialize(
|
|
1042
|
+
pageNumber === 1
|
|
1043
|
+
? sd.home({
|
|
1044
|
+
posts: publishedArticles.slice(0, 10).map((record) => ({
|
|
1045
|
+
url: absolute(recordHref(record)),
|
|
1046
|
+
headline: record.frontmatter.title,
|
|
1047
|
+
datePublished: record.frontmatter.publishedAt,
|
|
1048
|
+
})),
|
|
1049
|
+
})
|
|
1050
|
+
: sd.collection({
|
|
1051
|
+
name: indexTitle,
|
|
1052
|
+
description: msg('indexPageDescription', publicationTitle),
|
|
1053
|
+
url: indexUrl,
|
|
1054
|
+
language: publication.defaultLanguage,
|
|
1055
|
+
trail: [
|
|
1056
|
+
homeStep,
|
|
1057
|
+
{ label: indexTitle, route: indexRoute(pageNumber) },
|
|
1058
|
+
],
|
|
1059
|
+
}),
|
|
1060
|
+
),
|
|
1061
|
+
lastModified: latestContentTimestamp(
|
|
1062
|
+
pageNumber === 1
|
|
1063
|
+
? publishedArticles.slice(0, 1 + LATEST_CARD_COUNT)
|
|
1064
|
+
: items,
|
|
1065
|
+
),
|
|
1066
|
+
bodyHtml,
|
|
463
1067
|
});
|
|
464
1068
|
});
|
|
465
1069
|
|
|
466
|
-
/** @type {Map<string, import('../../../types/index.d.ts').ContentBuildRecord[]>} */
|
|
467
|
-
const byTag = new Map();
|
|
468
|
-
for (const record of publishedArticles) {
|
|
469
|
-
for (const tag of record.frontmatter.tags) {
|
|
470
|
-
const bucket = byTag.get(tag) ?? [];
|
|
471
|
-
bucket.push(record);
|
|
472
|
-
byTag.set(tag, bucket);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
const sortedTags = [...byTag.keys()].sort();
|
|
476
1070
|
if (sortedTags.length > 0) {
|
|
477
1071
|
pages.push({
|
|
478
1072
|
kind: 'tag',
|
|
479
1073
|
route: '/tags',
|
|
480
|
-
|
|
1074
|
+
...listingMeta({
|
|
1075
|
+
name: text0('tagsSectionLabel'),
|
|
1076
|
+
description: msg(
|
|
1077
|
+
'tagsRootDescription',
|
|
1078
|
+
String(sortedTags.length),
|
|
1079
|
+
publicationTitle,
|
|
1080
|
+
),
|
|
1081
|
+
route: '/tags',
|
|
1082
|
+
}),
|
|
481
1083
|
language: publication.defaultLanguage,
|
|
482
|
-
description: publication.description,
|
|
483
|
-
ogType: 'website',
|
|
484
|
-
socialImageRef: publication.defaultImage,
|
|
485
1084
|
lastModified: latestContentTimestamp(publishedArticles),
|
|
486
|
-
bodyHtml:
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
1085
|
+
bodyHtml: listingHead({
|
|
1086
|
+
breadcrumbHtml: crumbs([
|
|
1087
|
+
{ label: /** @type {string} */ (messages.tagsSectionLabel) },
|
|
1088
|
+
]),
|
|
1089
|
+
kicker: /** @type {string} */ (messages.tagKickerLabel),
|
|
1090
|
+
count: undefined,
|
|
1091
|
+
title: /** @type {string} */ (messages.tagsSectionLabel),
|
|
1092
|
+
extraHtml: tagPills(),
|
|
1093
|
+
}),
|
|
494
1094
|
});
|
|
495
1095
|
}
|
|
496
1096
|
for (const tag of sortedTags) {
|
|
@@ -498,131 +1098,148 @@ export function buildGeneratedPages(validatedInput) {
|
|
|
498
1098
|
/** @type {import('../../../types/index.d.ts').ContentBuildRecord[]} */ (
|
|
499
1099
|
byTag.get(tag)
|
|
500
1100
|
);
|
|
501
|
-
const heading = `${/** @type {string} */ (messages.tagIndexLabelPrefix)} ${tag}`;
|
|
502
1101
|
pages.push({
|
|
503
1102
|
kind: 'tag',
|
|
504
1103
|
route: `/tags/${routeSegmentForLabel(tag)}`,
|
|
505
|
-
|
|
1104
|
+
...listingMeta({
|
|
1105
|
+
name: msg('tagPageTitle', tag),
|
|
1106
|
+
description: msg(
|
|
1107
|
+
'tagPageDescription',
|
|
1108
|
+
String(items.length),
|
|
1109
|
+
tag,
|
|
1110
|
+
publicationTitle,
|
|
1111
|
+
),
|
|
1112
|
+
route: `/tags/${routeSegmentForLabel(tag)}`,
|
|
1113
|
+
parents: [{ label: text0('tagsSectionLabel'), route: site('/tags') }],
|
|
1114
|
+
}),
|
|
506
1115
|
language: publication.defaultLanguage,
|
|
507
|
-
ogType: 'website',
|
|
508
|
-
socialImageRef: publication.defaultImage,
|
|
509
1116
|
lastModified: latestContentTimestamp(items),
|
|
510
|
-
breadcrumbHtml: renderBreadcrumbs({
|
|
511
|
-
trail: [
|
|
512
|
-
{
|
|
513
|
-
label: /** @type {string} */ (messages.homeLinkLabel),
|
|
514
|
-
route: site('/'),
|
|
515
|
-
},
|
|
516
|
-
{
|
|
517
|
-
label: /** @type {string} */ (messages.tagsSectionLabel),
|
|
518
|
-
route: site('/tags'),
|
|
519
|
-
},
|
|
520
|
-
{ label: tag },
|
|
521
|
-
],
|
|
522
|
-
messages,
|
|
523
|
-
}),
|
|
524
1117
|
bodyHtml:
|
|
525
|
-
|
|
526
|
-
|
|
1118
|
+
listingHead({
|
|
1119
|
+
breadcrumbHtml: crumbs(
|
|
1120
|
+
[
|
|
1121
|
+
{
|
|
1122
|
+
label: /** @type {string} */ (messages.tagsSectionLabel),
|
|
1123
|
+
route: site('/tags'),
|
|
1124
|
+
},
|
|
1125
|
+
],
|
|
1126
|
+
[{ label: tag }],
|
|
1127
|
+
),
|
|
1128
|
+
kicker: /** @type {string} */ (messages.tagKickerLabel),
|
|
1129
|
+
count: items.length,
|
|
1130
|
+
title: tag,
|
|
1131
|
+
extraHtml: tagPills(tag),
|
|
1132
|
+
}) + rowList(items),
|
|
527
1133
|
});
|
|
528
1134
|
}
|
|
529
1135
|
|
|
530
|
-
/** @type {Map<string, import('../../../types/index.d.ts').ContentBuildRecord[]>} */
|
|
531
|
-
const bySeries = new Map();
|
|
532
|
-
for (const record of content) {
|
|
533
|
-
const series = record.frontmatter.series;
|
|
534
|
-
if (!series || record.frontmatter.status !== 'published') continue;
|
|
535
|
-
const bucket = bySeries.get(series) ?? [];
|
|
536
|
-
bucket.push(record);
|
|
537
|
-
bySeries.set(series, bucket);
|
|
538
|
-
}
|
|
539
|
-
const sortedSeries = [...bySeries.keys()].sort();
|
|
540
1136
|
if (sortedSeries.length > 0) {
|
|
541
1137
|
pages.push({
|
|
542
1138
|
kind: 'series',
|
|
543
1139
|
route: '/series',
|
|
544
|
-
|
|
1140
|
+
...listingMeta({
|
|
1141
|
+
name: text0('seriesSectionLabel'),
|
|
1142
|
+
description: msg(
|
|
1143
|
+
'seriesRootDescription',
|
|
1144
|
+
String(sortedSeries.length),
|
|
1145
|
+
publicationTitle,
|
|
1146
|
+
),
|
|
1147
|
+
route: '/series',
|
|
1148
|
+
}),
|
|
545
1149
|
language: publication.defaultLanguage,
|
|
546
|
-
description: publication.description,
|
|
547
|
-
ogType: 'website',
|
|
548
|
-
socialImageRef: publication.defaultImage,
|
|
549
1150
|
lastModified: latestContentTimestamp([...bySeries.values()].flat()),
|
|
550
|
-
bodyHtml:
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
1151
|
+
bodyHtml: listingHead({
|
|
1152
|
+
breadcrumbHtml: crumbs([
|
|
1153
|
+
{ label: /** @type {string} */ (messages.seriesSectionLabel) },
|
|
1154
|
+
]),
|
|
1155
|
+
kicker: /** @type {string} */ (messages.seriesKickerLabel),
|
|
1156
|
+
count: undefined,
|
|
1157
|
+
title: /** @type {string} */ (messages.seriesSectionLabel),
|
|
1158
|
+
iconName: 'book',
|
|
1159
|
+
extraHtml: `<ul class="g-pills">${sortedSeries
|
|
1160
|
+
.map((name) =>
|
|
1161
|
+
pill(seriesHref(name), name, bySeries.get(name)?.length, 'book'),
|
|
556
1162
|
)
|
|
557
1163
|
.join('')}</ul>`,
|
|
1164
|
+
}),
|
|
558
1165
|
});
|
|
559
1166
|
}
|
|
560
|
-
for (const
|
|
1167
|
+
for (const name of sortedSeries) {
|
|
561
1168
|
const items =
|
|
562
1169
|
/** @type {import('../../../types/index.d.ts').ContentBuildRecord[]} */ (
|
|
563
|
-
bySeries.get(
|
|
564
|
-
)
|
|
565
|
-
.slice()
|
|
566
|
-
.sort(
|
|
567
|
-
(a, b) =>
|
|
568
|
-
(a.frontmatter.seriesOrder ?? 0) - (b.frontmatter.seriesOrder ?? 0),
|
|
569
|
-
);
|
|
570
|
-
const heading = `${/** @type {string} */ (messages.seriesIndexLabelPrefix)} ${series}`;
|
|
1170
|
+
bySeries.get(name)
|
|
1171
|
+
);
|
|
571
1172
|
pages.push({
|
|
572
1173
|
kind: 'series',
|
|
573
|
-
route: `/series/${routeSegmentForLabel(
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
},
|
|
585
|
-
{
|
|
586
|
-
label: /** @type {string} */ (messages.seriesSectionLabel),
|
|
587
|
-
route: site('/series'),
|
|
588
|
-
},
|
|
589
|
-
{ label: series },
|
|
1174
|
+
route: `/series/${routeSegmentForLabel(name)}`,
|
|
1175
|
+
...listingMeta({
|
|
1176
|
+
name,
|
|
1177
|
+
description: msg(
|
|
1178
|
+
'seriesPageDescription',
|
|
1179
|
+
String(items.length),
|
|
1180
|
+
name,
|
|
1181
|
+
publicationTitle,
|
|
1182
|
+
),
|
|
1183
|
+
route: `/series/${routeSegmentForLabel(name)}`,
|
|
1184
|
+
parents: [
|
|
1185
|
+
{ label: text0('seriesSectionLabel'), route: site('/series') },
|
|
590
1186
|
],
|
|
591
|
-
messages,
|
|
592
1187
|
}),
|
|
1188
|
+
language: publication.defaultLanguage,
|
|
1189
|
+
lastModified: latestContentTimestamp(items),
|
|
593
1190
|
bodyHtml:
|
|
594
|
-
|
|
595
|
-
|
|
1191
|
+
listingHead({
|
|
1192
|
+
breadcrumbHtml: crumbs(
|
|
1193
|
+
[
|
|
1194
|
+
{
|
|
1195
|
+
label: /** @type {string} */ (messages.seriesSectionLabel),
|
|
1196
|
+
route: site('/series'),
|
|
1197
|
+
},
|
|
1198
|
+
],
|
|
1199
|
+
[{ label: name }],
|
|
1200
|
+
),
|
|
1201
|
+
kicker: /** @type {string} */ (messages.seriesKickerLabel),
|
|
1202
|
+
count: items.length,
|
|
1203
|
+
title: name,
|
|
1204
|
+
iconName: 'book',
|
|
1205
|
+
}) + rowList(items),
|
|
596
1206
|
});
|
|
597
1207
|
}
|
|
598
1208
|
|
|
599
|
-
/** @type {Map<string, import('../../../types/index.d.ts').ContentBuildRecord[]>} */
|
|
600
|
-
const byYear = new Map();
|
|
601
|
-
for (const record of publishedArticles) {
|
|
602
|
-
const year = record.frontmatter.publishedAt.slice(0, 4);
|
|
603
|
-
const bucket = byYear.get(year) ?? [];
|
|
604
|
-
bucket.push(record);
|
|
605
|
-
byYear.set(year, bucket);
|
|
606
|
-
}
|
|
607
|
-
const sortedYears = [...byYear.keys()].sort().reverse();
|
|
608
1209
|
if (sortedYears.length > 0) {
|
|
609
1210
|
pages.push({
|
|
610
1211
|
kind: 'archive',
|
|
611
1212
|
route: '/archive',
|
|
612
|
-
|
|
1213
|
+
...listingMeta({
|
|
1214
|
+
name: text0('archiveSectionLabel'),
|
|
1215
|
+
description: msg(
|
|
1216
|
+
'archiveRootDescription',
|
|
1217
|
+
String(publishedArticles.length),
|
|
1218
|
+
publicationTitle,
|
|
1219
|
+
),
|
|
1220
|
+
route: '/archive',
|
|
1221
|
+
}),
|
|
613
1222
|
language: publication.defaultLanguage,
|
|
614
|
-
description: publication.description,
|
|
615
|
-
ogType: 'website',
|
|
616
|
-
socialImageRef: publication.defaultImage,
|
|
617
1223
|
lastModified: latestContentTimestamp(publishedArticles),
|
|
618
|
-
bodyHtml:
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
1224
|
+
bodyHtml: listingHead({
|
|
1225
|
+
breadcrumbHtml: crumbs([
|
|
1226
|
+
{ label: /** @type {string} */ (messages.archiveSectionLabel) },
|
|
1227
|
+
]),
|
|
1228
|
+
kicker: /** @type {string} */ (messages.archiveKickerLabel),
|
|
1229
|
+
count: publishedArticles.length,
|
|
1230
|
+
title: /** @type {string} */ (messages.archiveSectionLabel),
|
|
1231
|
+
iconName: 'layers',
|
|
1232
|
+
extraHtml: `<ul class="g-pills">${sortedYears
|
|
1233
|
+
.map((year) =>
|
|
1234
|
+
pill(
|
|
1235
|
+
site(`/archive/${year}`),
|
|
1236
|
+
year,
|
|
1237
|
+
byYear.get(year)?.length,
|
|
1238
|
+
'layers',
|
|
1239
|
+
),
|
|
624
1240
|
)
|
|
625
1241
|
.join('')}</ul>`,
|
|
1242
|
+
}),
|
|
626
1243
|
});
|
|
627
1244
|
}
|
|
628
1245
|
for (const year of sortedYears) {
|
|
@@ -631,45 +1248,56 @@ export function buildGeneratedPages(validatedInput) {
|
|
|
631
1248
|
byYear.get(year)
|
|
632
1249
|
);
|
|
633
1250
|
const yearPages = paginate(items, LISTING_PAGE_SIZE);
|
|
1251
|
+
const yearRoute = (/** @type {number} */ n) =>
|
|
1252
|
+
n === 1 ? `/archive/${year}` : `/archive/${year}/page/${n}`;
|
|
634
1253
|
yearPages.forEach((pageItems, pageIndex) => {
|
|
635
1254
|
const pageNumber = pageIndex + 1;
|
|
636
|
-
const route =
|
|
637
|
-
pageNumber === 1
|
|
638
|
-
? `/archive/${year}`
|
|
639
|
-
: `/archive/${year}/page/${pageNumber}`;
|
|
640
|
-
const heading = `${/** @type {string} */ (messages.archiveYearLabelPrefix)} ${year}`;
|
|
641
1255
|
pages.push({
|
|
642
1256
|
kind: 'archive',
|
|
643
|
-
route,
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
},
|
|
659
|
-
{ label: year },
|
|
1257
|
+
route: yearRoute(pageNumber),
|
|
1258
|
+
...listingMeta({
|
|
1259
|
+
name:
|
|
1260
|
+
pageNumber === 1
|
|
1261
|
+
? msg('archivePageTitle', year)
|
|
1262
|
+
: `${msg('archivePageTitle', year)} \u2013 ${msg('pageStatusLabel', String(pageNumber), String(yearPages.length))}`,
|
|
1263
|
+
description: msg(
|
|
1264
|
+
'archivePageDescription',
|
|
1265
|
+
String(items.length),
|
|
1266
|
+
year,
|
|
1267
|
+
publicationTitle,
|
|
1268
|
+
),
|
|
1269
|
+
route: yearRoute(pageNumber),
|
|
1270
|
+
parents: [
|
|
1271
|
+
{ label: text0('archiveSectionLabel'), route: site('/archive') },
|
|
660
1272
|
],
|
|
661
|
-
messages,
|
|
662
1273
|
}),
|
|
1274
|
+
language: publication.defaultLanguage,
|
|
1275
|
+
lastModified: latestContentTimestamp(pageItems),
|
|
663
1276
|
bodyHtml:
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
1277
|
+
listingHead({
|
|
1278
|
+
breadcrumbHtml: crumbs(
|
|
1279
|
+
[
|
|
1280
|
+
{
|
|
1281
|
+
label: /** @type {string} */ (messages.archiveSectionLabel),
|
|
1282
|
+
route: site('/archive'),
|
|
1283
|
+
},
|
|
1284
|
+
],
|
|
1285
|
+
[{ label: year }],
|
|
1286
|
+
),
|
|
1287
|
+
kicker: /** @type {string} */ (messages.archiveKickerLabel),
|
|
1288
|
+
count: items.length,
|
|
1289
|
+
title: year,
|
|
1290
|
+
iconName: 'layers',
|
|
1291
|
+
}) +
|
|
1292
|
+
rowList(
|
|
1293
|
+
pageItems,
|
|
1294
|
+
renderPagination({
|
|
1295
|
+
currentPage: pageNumber,
|
|
1296
|
+
totalPages: yearPages.length,
|
|
1297
|
+
routeForPage: (n) => site(yearRoute(n)),
|
|
1298
|
+
messages,
|
|
1299
|
+
}),
|
|
1300
|
+
),
|
|
673
1301
|
});
|
|
674
1302
|
});
|
|
675
1303
|
}
|
|
@@ -701,8 +1329,8 @@ export function renderErrorPageBody({ messages, homeRoute }) {
|
|
|
701
1329
|
/** @type {string} */ (messages.errorPageReturnHomeLabel),
|
|
702
1330
|
);
|
|
703
1331
|
return (
|
|
704
|
-
`<h1>${heading}</h1>` +
|
|
705
|
-
`<p>${body}</p>` +
|
|
706
|
-
`<p><a href="${escapeHtml(homeRoute)}">${returnLabel}</a></p>`
|
|
1332
|
+
`<section class="g-wrap g-topic-hero"><h1>${heading}</h1>` +
|
|
1333
|
+
`<p class="g-dek">${body}</p>` +
|
|
1334
|
+
`<p><a class="g-btn" href="${escapeHtml(homeRoute)}">${returnLabel}${icon('arrow')}</a></p></section>`
|
|
707
1335
|
);
|
|
708
1336
|
}
|