@rathnasgala2/template 2.1.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 +95 -59
- 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 +209 -60
- 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 +79 -8
- 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 +922 -293
- 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/route.js +42 -0
- package/src/core/internal/search-index.js +5 -2
- package/src/core/internal/seo.js +57 -2
- package/src/core/internal/sitemap.js +5 -4
- package/src/core/internal/skeleton.js +73 -132
- package/src/core/internal/structured-data.js +247 -0
- package/src/core/internal/theme-assets.js +21 -2
|
@@ -58,10 +58,22 @@ 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';
|
|
64
|
-
import {
|
|
75
|
+
import { deriveAuthorSlugs, routeSegmentForLabel } from './route-labels.js';
|
|
76
|
+
import { derivePublicBasePath, joinPublicRoute } from './route.js';
|
|
65
77
|
|
|
66
78
|
/** Listing page size (index and per-year archive pages). Scoped decision:
|
|
67
79
|
* this renderer has no externally fixed page size; 10 is a deterministic,
|
|
@@ -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,16 +258,106 @@ 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) {
|
|
267
|
-
const { publication, authors, content, basePath } = validatedInput;
|
|
358
|
+
export function buildGeneratedPages(validatedInput, options = {}) {
|
|
359
|
+
const { publication, authors, content, basePath, baseUrl } = validatedInput;
|
|
360
|
+
const publicBasePath = derivePublicBasePath(baseUrl, basePath);
|
|
268
361
|
const messages = getMessages(publication.defaultLanguage);
|
|
269
362
|
const authorsById = new Map(authors.map((author) => [author.id, author]));
|
|
270
363
|
|
|
@@ -272,39 +365,255 @@ export function buildGeneratedPages(validatedInput) {
|
|
|
272
365
|
* @param {string} route an un-joined route
|
|
273
366
|
* @returns {string} the `basePath`-joined absolute route
|
|
274
367
|
*/
|
|
275
|
-
const site = (route) =>
|
|
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;
|
|
276
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;
|
|
277
443
|
/**
|
|
278
444
|
* @param {import('../../../types/index.d.ts').ContentBuildRecord} record
|
|
279
|
-
* @returns {string}
|
|
445
|
+
* @returns {string} the page description: authored, else the body's
|
|
446
|
+
* opening words
|
|
280
447
|
*/
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
`<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),
|
|
306
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;
|
|
307
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
|
+
});
|
|
308
617
|
|
|
309
618
|
/** @type {GeneratedPage[]} */
|
|
310
619
|
const pages = [];
|
|
@@ -314,182 +623,474 @@ export function buildGeneratedPages(validatedInput) {
|
|
|
314
623
|
kind: 'profile',
|
|
315
624
|
route: publication.profile.route,
|
|
316
625
|
title: publication.title,
|
|
626
|
+
documentTitle: documentTitleOf(text0('aboutPageTitle')),
|
|
317
627
|
language: publication.defaultLanguage,
|
|
318
628
|
description: publication.description,
|
|
319
629
|
ogType: 'website',
|
|
320
|
-
|
|
321
|
-
|
|
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>`,
|
|
322
650
|
});
|
|
323
651
|
}
|
|
324
652
|
|
|
325
653
|
for (const author of authors) {
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const biography = author.biography
|
|
330
|
-
? `<p>${escapeHtml(author.biography)}</p>`
|
|
331
|
-
: '';
|
|
654
|
+
const authored = publishedArticles.filter((record) =>
|
|
655
|
+
record.frontmatter.authorIds.includes(author.id),
|
|
656
|
+
);
|
|
332
657
|
const links =
|
|
333
658
|
author.links.length > 0
|
|
334
|
-
? `<ul>${author.links
|
|
659
|
+
? `<ul class="g-social">${author.links
|
|
335
660
|
.map((link) => {
|
|
336
661
|
const label = escapeHtml(link.label ?? link.uri);
|
|
337
|
-
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>`;
|
|
338
663
|
})
|
|
339
664
|
.join('')}</ul>`
|
|
340
665
|
: '';
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
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;
|
|
344
674
|
pages.push({
|
|
345
675
|
kind: 'author',
|
|
346
|
-
route:
|
|
347
|
-
title:
|
|
676
|
+
route: authorRoute(author),
|
|
677
|
+
title: author.displayName,
|
|
678
|
+
documentTitle: documentTitleOf(author.displayName),
|
|
348
679
|
language: publication.defaultLanguage,
|
|
349
|
-
description:
|
|
680
|
+
description:
|
|
681
|
+
truncateAtWord(author.biography, DESCRIPTION_MAX) ||
|
|
682
|
+
msg('authorPageDescription', author.displayName, publicationTitle),
|
|
350
683
|
ogType: 'profile',
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
+
),
|
|
363
700
|
bodyHtml:
|
|
364
|
-
|
|
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),
|
|
365
712
|
});
|
|
366
713
|
}
|
|
367
714
|
|
|
368
715
|
for (const record of content) {
|
|
369
716
|
const { frontmatter } = record;
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
+
: '';
|
|
379
771
|
const tags =
|
|
380
|
-
frontmatter.tags.length > 0
|
|
381
|
-
? `<ul>${frontmatter.tags
|
|
772
|
+
isArticle && frontmatter.tags.length > 0
|
|
773
|
+
? `<ul class="g-tags" aria-label="${text('tagListLabel')}">${frontmatter.tags
|
|
382
774
|
.map(
|
|
383
775
|
(tag) =>
|
|
384
|
-
`<li><a href="${escapeHtml(
|
|
776
|
+
`<li><a class="g-tag" href="${escapeHtml(tagHref(tag))}">${icon('hash')}${escapeHtml(tag)}</a></li>`,
|
|
385
777
|
)
|
|
386
778
|
.join('')}</ul>`
|
|
387
779
|
: '';
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
{
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
+
});
|
|
411
885
|
pages.push({
|
|
412
886
|
kind: frontmatter.kind,
|
|
413
887
|
route: contentRoute(frontmatter),
|
|
414
888
|
title: frontmatter.title,
|
|
889
|
+
documentTitle: documentTitleOf(frontmatter.title),
|
|
415
890
|
language: frontmatter.language,
|
|
416
|
-
description:
|
|
417
|
-
ogType:
|
|
418
|
-
|
|
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,
|
|
419
905
|
robotsContent:
|
|
420
906
|
frontmatter.status === 'unlisted' ? 'noindex, follow' : undefined,
|
|
421
907
|
lastModified: contentLastModified(frontmatter),
|
|
422
|
-
breadcrumbHtml: renderBreadcrumbs({ trail: breadcrumbTrail, messages }),
|
|
423
908
|
bodyHtml:
|
|
424
|
-
`<article
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
time +
|
|
429
|
-
seriesHtml +
|
|
430
|
-
tags +
|
|
431
|
-
record.body +
|
|
432
|
-
renderSlot('article-end') +
|
|
433
|
-
renderSlot('article-footer-ad', { collapsed: true }) +
|
|
434
|
-
`</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 : ''),
|
|
435
913
|
});
|
|
436
914
|
}
|
|
437
915
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
+
: '';
|
|
441
988
|
indexPages.forEach((items, pageIndex) => {
|
|
442
989
|
const pageNumber = pageIndex + 1;
|
|
443
|
-
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));
|
|
444
1026
|
pages.push({
|
|
445
1027
|
kind: 'index',
|
|
446
|
-
route
|
|
447
|
-
title:
|
|
1028
|
+
route: pageNumber === 1 ? '/' : `/page/${pageNumber}`,
|
|
1029
|
+
title: indexTitle,
|
|
1030
|
+
documentTitle:
|
|
1031
|
+
pageNumber === 1
|
|
1032
|
+
? homeDocumentTitle(publicationTitle, publication.description)
|
|
1033
|
+
: documentTitleOf(indexTitle),
|
|
448
1034
|
language: publication.defaultLanguage,
|
|
449
|
-
description:
|
|
1035
|
+
description:
|
|
1036
|
+
pageNumber === 1
|
|
1037
|
+
? publication.description
|
|
1038
|
+
: msg('indexPageDescription', publicationTitle),
|
|
450
1039
|
ogType: 'website',
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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,
|
|
462
1067
|
});
|
|
463
1068
|
});
|
|
464
1069
|
|
|
465
|
-
/** @type {Map<string, import('../../../types/index.d.ts').ContentBuildRecord[]>} */
|
|
466
|
-
const byTag = new Map();
|
|
467
|
-
for (const record of publishedArticles) {
|
|
468
|
-
for (const tag of record.frontmatter.tags) {
|
|
469
|
-
const bucket = byTag.get(tag) ?? [];
|
|
470
|
-
bucket.push(record);
|
|
471
|
-
byTag.set(tag, bucket);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
const sortedTags = [...byTag.keys()].sort();
|
|
475
1070
|
if (sortedTags.length > 0) {
|
|
476
1071
|
pages.push({
|
|
477
1072
|
kind: 'tag',
|
|
478
1073
|
route: '/tags',
|
|
479
|
-
|
|
1074
|
+
...listingMeta({
|
|
1075
|
+
name: text0('tagsSectionLabel'),
|
|
1076
|
+
description: msg(
|
|
1077
|
+
'tagsRootDescription',
|
|
1078
|
+
String(sortedTags.length),
|
|
1079
|
+
publicationTitle,
|
|
1080
|
+
),
|
|
1081
|
+
route: '/tags',
|
|
1082
|
+
}),
|
|
480
1083
|
language: publication.defaultLanguage,
|
|
481
|
-
description: publication.description,
|
|
482
|
-
ogType: 'website',
|
|
483
|
-
socialImageRef: publication.defaultImage,
|
|
484
1084
|
lastModified: latestContentTimestamp(publishedArticles),
|
|
485
|
-
bodyHtml:
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
+
}),
|
|
493
1094
|
});
|
|
494
1095
|
}
|
|
495
1096
|
for (const tag of sortedTags) {
|
|
@@ -497,131 +1098,148 @@ export function buildGeneratedPages(validatedInput) {
|
|
|
497
1098
|
/** @type {import('../../../types/index.d.ts').ContentBuildRecord[]} */ (
|
|
498
1099
|
byTag.get(tag)
|
|
499
1100
|
);
|
|
500
|
-
const heading = `${/** @type {string} */ (messages.tagIndexLabelPrefix)} ${tag}`;
|
|
501
1101
|
pages.push({
|
|
502
1102
|
kind: 'tag',
|
|
503
1103
|
route: `/tags/${routeSegmentForLabel(tag)}`,
|
|
504
|
-
|
|
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
|
+
}),
|
|
505
1115
|
language: publication.defaultLanguage,
|
|
506
|
-
ogType: 'website',
|
|
507
|
-
socialImageRef: publication.defaultImage,
|
|
508
1116
|
lastModified: latestContentTimestamp(items),
|
|
509
|
-
breadcrumbHtml: renderBreadcrumbs({
|
|
510
|
-
trail: [
|
|
511
|
-
{
|
|
512
|
-
label: /** @type {string} */ (messages.homeLinkLabel),
|
|
513
|
-
route: site('/'),
|
|
514
|
-
},
|
|
515
|
-
{
|
|
516
|
-
label: /** @type {string} */ (messages.tagsSectionLabel),
|
|
517
|
-
route: site('/tags'),
|
|
518
|
-
},
|
|
519
|
-
{ label: tag },
|
|
520
|
-
],
|
|
521
|
-
messages,
|
|
522
|
-
}),
|
|
523
1117
|
bodyHtml:
|
|
524
|
-
|
|
525
|
-
|
|
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),
|
|
526
1133
|
});
|
|
527
1134
|
}
|
|
528
1135
|
|
|
529
|
-
/** @type {Map<string, import('../../../types/index.d.ts').ContentBuildRecord[]>} */
|
|
530
|
-
const bySeries = new Map();
|
|
531
|
-
for (const record of content) {
|
|
532
|
-
const series = record.frontmatter.series;
|
|
533
|
-
if (!series || record.frontmatter.status !== 'published') continue;
|
|
534
|
-
const bucket = bySeries.get(series) ?? [];
|
|
535
|
-
bucket.push(record);
|
|
536
|
-
bySeries.set(series, bucket);
|
|
537
|
-
}
|
|
538
|
-
const sortedSeries = [...bySeries.keys()].sort();
|
|
539
1136
|
if (sortedSeries.length > 0) {
|
|
540
1137
|
pages.push({
|
|
541
1138
|
kind: 'series',
|
|
542
1139
|
route: '/series',
|
|
543
|
-
|
|
1140
|
+
...listingMeta({
|
|
1141
|
+
name: text0('seriesSectionLabel'),
|
|
1142
|
+
description: msg(
|
|
1143
|
+
'seriesRootDescription',
|
|
1144
|
+
String(sortedSeries.length),
|
|
1145
|
+
publicationTitle,
|
|
1146
|
+
),
|
|
1147
|
+
route: '/series',
|
|
1148
|
+
}),
|
|
544
1149
|
language: publication.defaultLanguage,
|
|
545
|
-
description: publication.description,
|
|
546
|
-
ogType: 'website',
|
|
547
|
-
socialImageRef: publication.defaultImage,
|
|
548
1150
|
lastModified: latestContentTimestamp([...bySeries.values()].flat()),
|
|
549
|
-
bodyHtml:
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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'),
|
|
555
1162
|
)
|
|
556
1163
|
.join('')}</ul>`,
|
|
1164
|
+
}),
|
|
557
1165
|
});
|
|
558
1166
|
}
|
|
559
|
-
for (const
|
|
1167
|
+
for (const name of sortedSeries) {
|
|
560
1168
|
const items =
|
|
561
1169
|
/** @type {import('../../../types/index.d.ts').ContentBuildRecord[]} */ (
|
|
562
|
-
bySeries.get(
|
|
563
|
-
)
|
|
564
|
-
.slice()
|
|
565
|
-
.sort(
|
|
566
|
-
(a, b) =>
|
|
567
|
-
(a.frontmatter.seriesOrder ?? 0) - (b.frontmatter.seriesOrder ?? 0),
|
|
568
|
-
);
|
|
569
|
-
const heading = `${/** @type {string} */ (messages.seriesIndexLabelPrefix)} ${series}`;
|
|
1170
|
+
bySeries.get(name)
|
|
1171
|
+
);
|
|
570
1172
|
pages.push({
|
|
571
1173
|
kind: 'series',
|
|
572
|
-
route: `/series/${routeSegmentForLabel(
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
},
|
|
584
|
-
{
|
|
585
|
-
label: /** @type {string} */ (messages.seriesSectionLabel),
|
|
586
|
-
route: site('/series'),
|
|
587
|
-
},
|
|
588
|
-
{ 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') },
|
|
589
1186
|
],
|
|
590
|
-
messages,
|
|
591
1187
|
}),
|
|
1188
|
+
language: publication.defaultLanguage,
|
|
1189
|
+
lastModified: latestContentTimestamp(items),
|
|
592
1190
|
bodyHtml:
|
|
593
|
-
|
|
594
|
-
|
|
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),
|
|
595
1206
|
});
|
|
596
1207
|
}
|
|
597
1208
|
|
|
598
|
-
/** @type {Map<string, import('../../../types/index.d.ts').ContentBuildRecord[]>} */
|
|
599
|
-
const byYear = new Map();
|
|
600
|
-
for (const record of publishedArticles) {
|
|
601
|
-
const year = record.frontmatter.publishedAt.slice(0, 4);
|
|
602
|
-
const bucket = byYear.get(year) ?? [];
|
|
603
|
-
bucket.push(record);
|
|
604
|
-
byYear.set(year, bucket);
|
|
605
|
-
}
|
|
606
|
-
const sortedYears = [...byYear.keys()].sort().reverse();
|
|
607
1209
|
if (sortedYears.length > 0) {
|
|
608
1210
|
pages.push({
|
|
609
1211
|
kind: 'archive',
|
|
610
1212
|
route: '/archive',
|
|
611
|
-
|
|
1213
|
+
...listingMeta({
|
|
1214
|
+
name: text0('archiveSectionLabel'),
|
|
1215
|
+
description: msg(
|
|
1216
|
+
'archiveRootDescription',
|
|
1217
|
+
String(publishedArticles.length),
|
|
1218
|
+
publicationTitle,
|
|
1219
|
+
),
|
|
1220
|
+
route: '/archive',
|
|
1221
|
+
}),
|
|
612
1222
|
language: publication.defaultLanguage,
|
|
613
|
-
description: publication.description,
|
|
614
|
-
ogType: 'website',
|
|
615
|
-
socialImageRef: publication.defaultImage,
|
|
616
1223
|
lastModified: latestContentTimestamp(publishedArticles),
|
|
617
|
-
bodyHtml:
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
+
),
|
|
623
1240
|
)
|
|
624
1241
|
.join('')}</ul>`,
|
|
1242
|
+
}),
|
|
625
1243
|
});
|
|
626
1244
|
}
|
|
627
1245
|
for (const year of sortedYears) {
|
|
@@ -630,45 +1248,56 @@ export function buildGeneratedPages(validatedInput) {
|
|
|
630
1248
|
byYear.get(year)
|
|
631
1249
|
);
|
|
632
1250
|
const yearPages = paginate(items, LISTING_PAGE_SIZE);
|
|
1251
|
+
const yearRoute = (/** @type {number} */ n) =>
|
|
1252
|
+
n === 1 ? `/archive/${year}` : `/archive/${year}/page/${n}`;
|
|
633
1253
|
yearPages.forEach((pageItems, pageIndex) => {
|
|
634
1254
|
const pageNumber = pageIndex + 1;
|
|
635
|
-
const route =
|
|
636
|
-
pageNumber === 1
|
|
637
|
-
? `/archive/${year}`
|
|
638
|
-
: `/archive/${year}/page/${pageNumber}`;
|
|
639
|
-
const heading = `${/** @type {string} */ (messages.archiveYearLabelPrefix)} ${year}`;
|
|
640
1255
|
pages.push({
|
|
641
1256
|
kind: 'archive',
|
|
642
|
-
route,
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
},
|
|
658
|
-
{ 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') },
|
|
659
1272
|
],
|
|
660
|
-
messages,
|
|
661
1273
|
}),
|
|
1274
|
+
language: publication.defaultLanguage,
|
|
1275
|
+
lastModified: latestContentTimestamp(pageItems),
|
|
662
1276
|
bodyHtml:
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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
|
+
),
|
|
672
1301
|
});
|
|
673
1302
|
});
|
|
674
1303
|
}
|
|
@@ -700,8 +1329,8 @@ export function renderErrorPageBody({ messages, homeRoute }) {
|
|
|
700
1329
|
/** @type {string} */ (messages.errorPageReturnHomeLabel),
|
|
701
1330
|
);
|
|
702
1331
|
return (
|
|
703
|
-
`<h1>${heading}</h1>` +
|
|
704
|
-
`<p>${body}</p>` +
|
|
705
|
-
`<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>`
|
|
706
1335
|
);
|
|
707
1336
|
}
|