edgepress 2026.10.12 → 2026.10.13
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 +12 -2
- package/config.yml +171 -171
- package/content/assets/edgepress/brand/sources/export-manifest.json +181 -181
- package/content/assets/edgepress-markdown-guide.svg +21 -21
- package/content/guides/markdown-media.md +38 -0
- package/content/guides/product-demos.md +30 -30
- package/content/pages/deployment/en.json +1 -1
- package/content/posts/2026-09-24-edgepress-release-2026-9-1/en.md +1 -1
- package/content/posts/2026-09-24-edgepress-release-2026-9-1/zh-cn.md +1 -1
- package/languages/base/en.json +98 -98
- package/languages/packs/zh-CN.json +98 -98
- package/package.json +90 -90
- package/src/assets.js +187 -186
- package/src/browser-audit.js +158 -158
- package/src/build.js +293 -291
- package/src/cli.js +364 -364
- package/src/content.js +233 -233
- package/src/data-saver.js +3 -3
- package/src/generators.js +414 -420
- package/src/i18n.js +86 -86
- package/src/markdown.js +195 -160
- package/src/page-check.js +1 -1
- package/src/plugin-api.js +77 -77
- package/src/report-visuals.js +642 -642
- package/src/revision-cards.js +30 -30
- package/src/rss.js +100 -0
- package/src/theme.js +4 -4
- package/src/video-metadata.js +36 -0
- package/static/edgepress/fonts/faces.css +2217 -2217
- package/static/edgepress/media-viewer.js +67 -10
- package/static/edgepress/project-demo.js +83 -83
- package/static/edgepress/search.js +84 -84
- package/tests/assets-runtime.test.mjs +26 -26
- package/tests/audit.test.mjs +142 -142
- package/tests/brand-regression.test.mjs +110 -110
- package/tests/document-package.test.mjs +2 -2
- package/tests/embed-syntax.test.mjs +68 -68
- package/tests/image-viewer.test.mjs +36 -36
- package/tests/markdown-media.test.mjs +119 -0
- package/tests/oembed-layout.test.mjs +6 -1
- package/tests/project-demo.test.mjs +73 -73
- package/tests/release-version.test.mjs +28 -0
- package/tests/rss.test.mjs +54 -0
- package/tests/video-metadata.test.mjs +28 -0
- package/tests/writing.test.mjs +83 -83
- package/themes/atelier/assets/style.css +340 -340
- package/themes/default/assets/style.css +371 -371
- package/themes/default/layouts/partials/head.html +1 -1
- package/themes/folio/assets/style.css +515 -515
- package/themes/folio/theme.json +5 -5
- package/themes/signal/assets/style.css +332 -332
- package/tools/README.md +10 -10
- package/tools/assets/generate-brand-assets.py +160 -160
- package/tools/assets/video-preview.mjs +15 -0
- package/tools/recordings/demo-workbench.mjs +53 -53
- package/tools/recordings/desktop-recording.mjs +70 -70
- package/tools/recordings/vm-desktop.mjs +59 -59
package/src/generators.js
CHANGED
|
@@ -1,420 +1,414 @@
|
|
|
1
|
-
import {renderPostServices,hasServiceBlock} from './service-block.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (value
|
|
41
|
-
return '/' + value;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
counts.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
'
|
|
79
|
-
|
|
80
|
-
'
|
|
81
|
-
'
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
escapeHtml(video.
|
|
91
|
-
'
|
|
92
|
-
'
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
'@
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
if (page ===
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (value
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
homepageLocales.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
const
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
'<
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
categories.
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
const
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
...
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
escapeHtml(
|
|
358
|
-
'
|
|
359
|
-
'><
|
|
360
|
-
'
|
|
361
|
-
'
|
|
362
|
-
'
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
const
|
|
402
|
-
const
|
|
403
|
-
const
|
|
404
|
-
'</h1><p>' + escapeHtml(
|
|
405
|
-
escapeHtml(translate(config, locale, 'returnHome')) + '</a></p></section>';
|
|
406
|
-
routes.push(await pageRoute(prefix + '
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
...site.posts.filter(post => !post.draft).map(post => ({thread:post.bundlePath,title:post.title})),
|
|
416
|
-
...site.pages.filter(page => !page.draft && hasServiceBlock(page.blocks)).map(page => ({thread:'page:' + page.bundlePath,title:page.title}))
|
|
417
|
-
] : [];
|
|
418
|
-
routes.push(fileRoute('edgepress/service-contexts.json', JSON.stringify([...new Map(discussions.map(item => [item.thread,item])).values()]), 'application/json; charset=utf-8'));
|
|
419
|
-
return routes;
|
|
420
|
-
}
|
|
1
|
+
import {renderPostServices,hasServiceBlock} from './service-block.js';
|
|
2
|
+
import {generateRssRoute} from './rss.js';
|
|
3
|
+
import {renderRevisionCards} from './revision-cards.js';
|
|
4
|
+
import { authorForPost, readingMinutes, filterPostsByCategory, categorySlug, normalizeCategory } from './post-details.js';
|
|
5
|
+
import { plainText, renderMarkdownExcerpt } from './markdown.js';
|
|
6
|
+
import { renderBlocks } from './page-blocks.js';
|
|
7
|
+
import { renderLayout } from './theme.js';
|
|
8
|
+
import { localizedContentUrl, localizedUrl, translate } from './i18n.js';
|
|
9
|
+
import { postsForLocale, slugify, sortPostsNewest } from './content.js';
|
|
10
|
+
import { mapLimit } from './concurrency.js';
|
|
11
|
+
import { renderIcon } from './icons.js';
|
|
12
|
+
|
|
13
|
+
function escapeHtml(value) {
|
|
14
|
+
return String(value ?? '').replace(/[&<>"']/g, (char) => ({
|
|
15
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
|
16
|
+
})[char]);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function escapeXml(value) {
|
|
20
|
+
return String(value ?? '').replace(/[&<>"']/g, (char) => ({
|
|
21
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
|
22
|
+
})[char]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function urlFor(path) {
|
|
26
|
+
const parts = String(path ?? '').split('/').filter(Boolean).map(encodeURIComponent);
|
|
27
|
+
return '/' + parts.join('/') + (String(path).endsWith('/') && parts.length ? '/' : '');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function dateLabel(date, language, timeZone = 'UTC', dateOnly = true) {
|
|
31
|
+
return new Intl.DateTimeFormat(language, dateOnly?{dateStyle:'long',timeZone}:{year:'numeric',month:'long',day:'numeric',hour:'2-digit',minute:'2-digit',timeZoneName:'short',timeZone}).format(date);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fileRoute(path, body, contentType = 'text/html; charset=utf-8') {
|
|
35
|
+
return { path, body, contentType };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function urlPathForRoute(path) {
|
|
39
|
+
const value = String(path).replace(/\\/g, '/');
|
|
40
|
+
if (value === 'index.html') return '/';
|
|
41
|
+
if (value.endsWith('/index.html')) return '/' + value.slice(0, -'index.html'.length);
|
|
42
|
+
return '/' + value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function decodeHeadingEntities(value) {
|
|
46
|
+
return value.replace(/&(#x[\da-f]+|#\d+|amp|lt|gt|quot|apos|nbsp);/gi, (match, entity) => {
|
|
47
|
+
const named = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ' };
|
|
48
|
+
if (entity[0] === '#') {
|
|
49
|
+
const hex = entity[1]?.toLowerCase() === 'x';
|
|
50
|
+
const point = Number.parseInt(entity.slice(hex ? 2 : 1), hex ? 16 : 10);
|
|
51
|
+
return Number.isFinite(point) && point > 0 && point <= 0x10ffff ? String.fromCodePoint(point) : match;
|
|
52
|
+
}
|
|
53
|
+
return named[entity.toLowerCase()] ?? match;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function addPostContents(html, locale, config) {
|
|
58
|
+
const headings = [];
|
|
59
|
+
const counts = new Map();
|
|
60
|
+
const content = html.replace(/<h([2-6])\b([^>]*)>([\s\S]*?)<\/h\1>/gi, (whole, levelText, attributes, inner) => {
|
|
61
|
+
const label = decodeHeadingEntities(inner.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim());
|
|
62
|
+
if (!label) return whole;
|
|
63
|
+
const base = slugify(label);
|
|
64
|
+
const count = (counts.get(base) || 0) + 1;
|
|
65
|
+
counts.set(base, count);
|
|
66
|
+
const id = count === 1 ? base : base + '-' + count;
|
|
67
|
+
headings.push({ id, label, level: Number(levelText) });
|
|
68
|
+
const safeAttributes = attributes.replace(/\s+id\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, '');
|
|
69
|
+
return '<h' + levelText + safeAttributes + ' id="' + escapeHtml(id) + '">' + inner + '</h' + levelText + '>';
|
|
70
|
+
});
|
|
71
|
+
if (!headings.length) return { html: content, toc: '' };
|
|
72
|
+
const items = headings.map((heading) => '<li class="post-toc-level-' + heading.level + '"><a href="#' +
|
|
73
|
+
escapeHtml(heading.id) + '">' + escapeHtml(heading.label) + '</a></li>').join('');
|
|
74
|
+
const contentsLabel = translate(config, locale, 'postContents');
|
|
75
|
+
const closeContents = translate(config, locale, 'closeContents');
|
|
76
|
+
const toc = '<aside class="post-toc-widget" data-post-toc hidden>' +
|
|
77
|
+
'<button class="post-toc-toggle" type="button" aria-expanded="false" aria-controls="post-toc-panel" aria-label="' +
|
|
78
|
+
escapeHtml(contentsLabel) + '">' + renderIcon('book-open', 'post-toc-icon') + '<span>' + escapeHtml(contentsLabel) + '</span></button>' +
|
|
79
|
+
'<nav class="post-toc-panel" id="post-toc-panel" aria-labelledby="post-toc-title" aria-label="' +
|
|
80
|
+
escapeHtml(contentsLabel) + '" hidden><div class="post-toc-heading"><h2 id="post-toc-title">' + escapeHtml(contentsLabel) +
|
|
81
|
+
'</h2><button class="post-toc-close" type="button" aria-label="' + escapeHtml(closeContents) + '"><span aria-hidden="true">×</span></button></div>' +
|
|
82
|
+
'<ol>' + items + '</ol></nav></aside>';
|
|
83
|
+
return { html: content, toc };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function renderPostVideo(video, locale) {
|
|
87
|
+
if (!video) return '';
|
|
88
|
+
const videoTitle = escapeHtml(video.title);
|
|
89
|
+
return '<figure class="post-video"><video data-edgepress-video controls playsinline preload="none" poster="' + escapeHtml(video.poster) + '" aria-label="' +
|
|
90
|
+
escapeHtml(video.alt) + '"><source src="' + escapeHtml(video.src) + '" type="video/mp4"><track kind="captions" src="' +
|
|
91
|
+
escapeHtml(video.captions) + '" srclang="' + escapeHtml(video.captionLanguage) + '" label="' + escapeHtml(video.captionLabel) + '" default>' +
|
|
92
|
+
'<a href="' + escapeHtml(video.src) + '">' + (locale.toLowerCase().startsWith('zh') ? '下载 MP4 视频' : 'Download the MP4 video') +
|
|
93
|
+
'</a></video><figcaption><strong>' + videoTitle + '</strong><br>' + escapeHtml(video.description) + '</figcaption></figure>';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function pageRoute(path, title, description, body, locale, config, extensions, metadata = {}) {
|
|
97
|
+
const urlPath = metadata.urlPath || urlPathForRoute(path);
|
|
98
|
+
const structuredData = metadata.structuredData ?? {
|
|
99
|
+
'@context': 'https://schema.org',
|
|
100
|
+
'@type': 'WebPage',
|
|
101
|
+
name: title,
|
|
102
|
+
description: description || config.site.description,
|
|
103
|
+
url: config.site.url ? config.site.url + urlPath : undefined,
|
|
104
|
+
inLanguage: locale,
|
|
105
|
+
isPartOf: config.site.url ? { '@type': 'WebSite', name: config.site.title, url: config.site.url } : undefined
|
|
106
|
+
};
|
|
107
|
+
body = body.replace(/<pre\b/gi, '<pre data-copyable-code');
|
|
108
|
+
let html = await renderLayout(config, extensions, {
|
|
109
|
+
title,
|
|
110
|
+
description,
|
|
111
|
+
locale,
|
|
112
|
+
urlPath,
|
|
113
|
+
keywords: metadata.keywords,
|
|
114
|
+
image: metadata.image,
|
|
115
|
+
robots: metadata.robots,
|
|
116
|
+
structuredData
|
|
117
|
+
}, body);
|
|
118
|
+
if (config.preview) {
|
|
119
|
+
html = html.replace(/<\/body>/i, '<script defer src="/__edgepress_preview.js"></script></body>');
|
|
120
|
+
}
|
|
121
|
+
return fileRoute(path, html);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function renderPostAuthor(post, locale, config) {
|
|
125
|
+
if (!post.author) return '';
|
|
126
|
+
return '<span class="post-author">' + escapeHtml(translate(config, locale, 'postAuthor').replace('{author}', post.author)) + '</span>';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function renderPostCard(post, locale, config, showLanguage = false) {
|
|
130
|
+
const summary = await renderMarkdownExcerpt(post.description || post.markdown, {...config.markdown,linkResolver:value=>localizedContentUrl(config,post.locale,value)});
|
|
131
|
+
const postLanguage = showLanguage && post.locale !== locale ? translate(config, post.locale, 'languageName') : '';
|
|
132
|
+
return '<article class="post-card" lang="' + escapeHtml(post.locale) + '"><h2><a href="' + escapeHtml(urlFor(post.path)) + '">' + escapeHtml(post.title) + '</a></h2>' +
|
|
133
|
+
'<p class="meta"><time data-local-time data-time-locale="' + escapeHtml(post.locale || locale) + '" data-date-only="' + (post.dateOnly === true) + '" datetime="' + (post.dateOnly ? post.date.toISOString().slice(0,10) : post.date.toISOString()) + '">' + escapeHtml(dateLabel(post.date, post.locale || locale, post.dateOnly ? 'UTC' : config.site.timeZone, post.dateOnly)) + '</time>' +
|
|
134
|
+
(postLanguage ? ' <span class="post-language" lang="' + escapeHtml(post.locale) + '">' + escapeHtml(postLanguage) + '</span>' : '') +
|
|
135
|
+
(post.author ? ' ' + renderPostAuthor(post, locale, config) : '') + '</p><div class="post-excerpt" lang="' + escapeHtml(post.locale) + '">' +
|
|
136
|
+
summary + '</div></article>';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function paginationPageNumbers(currentPage, totalPages) {
|
|
140
|
+
const visible = new Set([1, totalPages]);
|
|
141
|
+
for (let page = Math.max(1, currentPage - 2); page <= Math.min(totalPages, currentPage + 2); page += 1) visible.add(page);
|
|
142
|
+
const pages = [...visible].sort((left, right) => left - right);
|
|
143
|
+
const entries = [];
|
|
144
|
+
for (let index = 0; index < pages.length; index += 1) {
|
|
145
|
+
const page = pages[index];
|
|
146
|
+
const previous = pages[index - 1];
|
|
147
|
+
if (previous !== undefined && page - previous > 1) {
|
|
148
|
+
if (page - previous === 2) entries.push(previous + 1);
|
|
149
|
+
else entries.push(null);
|
|
150
|
+
}
|
|
151
|
+
entries.push(page);
|
|
152
|
+
}
|
|
153
|
+
return entries;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function renderPagination(locale, currentPage, totalPages, config, pageUrl) {
|
|
157
|
+
const label = (key) => translate(config, locale, key);
|
|
158
|
+
const pageLinks = paginationPageNumbers(currentPage, totalPages).map((page) => {
|
|
159
|
+
if (page === null) return '<li class="pagination-ellipsis" aria-hidden="true">…</li>';
|
|
160
|
+
if (page === currentPage) return '<li><span class="pagination-current" aria-current="page">' + page + '</span></li>';
|
|
161
|
+
const pageLabel = label('pageNumber').replace('{page}', String(page));
|
|
162
|
+
return '<li><a href="' + escapeHtml(pageUrl(page)) + '" aria-label="' + escapeHtml(pageLabel) + '">' + page + '</a></li>';
|
|
163
|
+
}).join('');
|
|
164
|
+
const previous = currentPage > 1
|
|
165
|
+
? '<a rel="prev" href="' + escapeHtml(pageUrl(currentPage - 1)) + '">' + escapeHtml(label('newer')) + '</a>' : '';
|
|
166
|
+
const next = currentPage < totalPages
|
|
167
|
+
? '<a rel="next" href="' + escapeHtml(pageUrl(currentPage + 1)) + '">' + escapeHtml(label('older')) + '</a>' : '';
|
|
168
|
+
return '<nav class="pagination" aria-label="' + escapeHtml(label('pagination')) + '">' + previous +
|
|
169
|
+
'<ol class="pagination-pages">' + pageLinks + '</ol>' + next + '</nav>';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function pageBodyForDocument(page, site, config, locale, isHomepage = false, renderContext = {}) {
|
|
173
|
+
const content = await renderBlocks(page.blocks, { config, locale, site, document: page, isHomepage, ...renderContext });
|
|
174
|
+
const firstElement = page.blocks[0]?.cells?.[0]?.[0];
|
|
175
|
+
const needsTitle = !isHomepage || firstElement?.type !== 'hero';
|
|
176
|
+
const title = needsTitle ? '<header class="page-title"><h1>' + escapeHtml(page.title) + '</h1></header>' : '';
|
|
177
|
+
return '<article class="page-builder">' + title + content + '</article>';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function paginatedLatestPostsSize(page) {
|
|
181
|
+
function find(value) {
|
|
182
|
+
if (Array.isArray(value)) {
|
|
183
|
+
for (const item of value) {
|
|
184
|
+
const result = find(item);
|
|
185
|
+
if (result !== null) return result;
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
if (!value || typeof value !== 'object') return null;
|
|
190
|
+
if (value.type === 'latest-posts' && value.paginate === true) return value.count ?? 3;
|
|
191
|
+
return find(value.cells) ?? find(value.blocks);
|
|
192
|
+
}
|
|
193
|
+
return find(page?.blocks);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function homepageDocument(pages, locale, defaultLocale) {
|
|
197
|
+
return pages.find((page) => page.homepage && page.locale === locale) ||
|
|
198
|
+
pages.find((page) => page.homepage && page.locale === defaultLocale) || null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function generateBuiltinRoutes(site, config, extensions) {
|
|
202
|
+
const routes = [];
|
|
203
|
+
const allPagePaths = new Set();
|
|
204
|
+
const defaultLocale = config.i18n.defaultLocale;
|
|
205
|
+
const homepages = site.pages.filter((page) => page.homepage);
|
|
206
|
+
const homepageLocales = new Set();
|
|
207
|
+
for (const page of homepages) {
|
|
208
|
+
if (homepageLocales.has(page.locale)) throw new Error('Only one homepage may be configured for locale ' + page.locale);
|
|
209
|
+
homepageLocales.add(page.locale);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const allPosts = sortPostsNewest(site.posts);
|
|
213
|
+
for (const locale of config.i18n.locales) {
|
|
214
|
+
const posts = allPosts.filter((post) => post.locale === locale);
|
|
215
|
+
const localizedPosts = postsForLocale(allPosts, locale, defaultLocale);
|
|
216
|
+
const archivePosts = filterPostsByCategory(localizedPosts, config.site.archive?.categories);
|
|
217
|
+
const pages = site.pages.filter((page) => page.locale === locale && !page.homepage);
|
|
218
|
+
const homePage = homepageDocument(site.pages, locale, defaultLocale);
|
|
219
|
+
const latestPostsPageSize = paginatedLatestPostsSize(homePage);
|
|
220
|
+
if (latestPostsPageSize !== null && (!Number.isInteger(latestPostsPageSize) || latestPostsPageSize < 1 || latestPostsPageSize > 12)) {
|
|
221
|
+
throw new Error('A paginated latest-posts block must use a count from 1 to 12');
|
|
222
|
+
}
|
|
223
|
+
const homepageArchivePages = latestPostsPageSize ? Math.ceil(archivePosts.length / latestPostsPageSize) : 0;
|
|
224
|
+
const prefix = locale === defaultLocale ? '' : locale + '/';
|
|
225
|
+
const label = (key) => translate(config, locale, key);
|
|
226
|
+
const localeHomeUrl = localizedUrl(config, locale, '');
|
|
227
|
+
allPagePaths.add(localeHomeUrl);
|
|
228
|
+
|
|
229
|
+
if (homePage) {
|
|
230
|
+
const latestPostsPagination = latestPostsPageSize ? {
|
|
231
|
+
totalPages: homepageArchivePages,
|
|
232
|
+
archiveUrl: localizedUrl(config, locale, 'archives/'),
|
|
233
|
+
olderUrl: localizedUrl(config, locale, 'archives/page/2/')
|
|
234
|
+
} : null;
|
|
235
|
+
const body = await pageBodyForDocument(homePage, site, config, locale, true, { latestPostsPagination });
|
|
236
|
+
routes.push(await pageRoute(prefix + 'index.html', homePage.title, homePage.description || config.site.description,
|
|
237
|
+
body, locale, config, extensions, {
|
|
238
|
+
urlPath: localeHomeUrl,
|
|
239
|
+
keywords: homePage.keywords,
|
|
240
|
+
image: homePage.image,
|
|
241
|
+
robots: homePage.robots,
|
|
242
|
+
structuredData: {
|
|
243
|
+
'@context': 'https://schema.org', '@type': 'WebSite', name: config.site.title,
|
|
244
|
+
description: homePage.description || config.site.description,
|
|
245
|
+
url: config.site.url ? config.site.url + localeHomeUrl : undefined,
|
|
246
|
+
inLanguage: locale,
|
|
247
|
+
publisher: config.site.seo.author ? { '@type': 'Organization', name: config.site.seo.author } : undefined
|
|
248
|
+
}
|
|
249
|
+
}));
|
|
250
|
+
} else {
|
|
251
|
+
const totalPages = Math.max(1, Math.ceil(archivePosts.length / config.pagination.perPage));
|
|
252
|
+
for (let pageNumber = 1; pageNumber <= totalPages; pageNumber += 1) {
|
|
253
|
+
const subset = archivePosts.slice((pageNumber - 1) * config.pagination.perPage, pageNumber * config.pagination.perPage);
|
|
254
|
+
const cards = await Promise.all(subset.map((post) => renderPostCard(post, locale, config, true)));
|
|
255
|
+
const body = '<section class="intro"><h1>' + escapeHtml(config.site.title) + '</h1><p>' + escapeHtml(config.site.description) +
|
|
256
|
+
'</p></section><section class="post-list" aria-label="' + escapeHtml(label('latestPosts')) + '">' +
|
|
257
|
+
(cards.length ? '<div class="post-list archive-post-list">' + cards.join('') + '</div>' : '<p>' + escapeHtml(label('noPosts')) + '</p>') +
|
|
258
|
+
'</section>' + (pageNumber === 1 && pages.length ? '<section class="page-links"><h2>' + escapeHtml(label('pages')) + '</h2><ul>' +
|
|
259
|
+
pages.map((page) => '<li><a href="' + escapeHtml(urlFor(page.path)) + '">' + escapeHtml(page.title) + '</a></li>').join('') + '</ul></section>' : '') +
|
|
260
|
+
(totalPages > 1 ? renderPagination(locale, pageNumber, totalPages, config, (page) =>
|
|
261
|
+
localizedUrl(config, locale, page === 1 ? '' : 'page/' + page + '/')) : '');
|
|
262
|
+
const routePath = pageNumber === 1 ? prefix + 'index.html' : prefix + 'page/' + pageNumber + '/index.html';
|
|
263
|
+
routes.push(await pageRoute(routePath, pageNumber === 1 ? config.site.title : 'Page ' + pageNumber,
|
|
264
|
+
config.site.description, body, locale, config, extensions));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
for (const post of posts) allPagePaths.add(urlFor(post.path));
|
|
269
|
+
const postRoutes = await mapLimit(posts, config.concurrency, async (post) => {
|
|
270
|
+
const renderedMarkdown = addPostContents(post.html, locale, config);
|
|
271
|
+
const author = authorForPost(post, config);
|
|
272
|
+
const minutes = post.readingMinutes || readingMinutes(post.markdown);
|
|
273
|
+
const authorHtml = author.name ? '<span class="post-author">' +
|
|
274
|
+
(author.avatar ? '<img class="author-avatar" src="' + escapeHtml(author.avatar) + '" width="40" height="40" alt="" decoding="async">' : '') +
|
|
275
|
+
escapeHtml(label('postAuthor').replace('{author}', author.name)) + '</span>' : '';
|
|
276
|
+
const tagsHtml = post.tags.length ? '<div class="post-tags" role="group" aria-label="' + escapeHtml(label('tags')) + '"><span class="post-tags-label">' +
|
|
277
|
+
escapeHtml(label('tags')) + ':</span>' + post.tags.map(tag => '<span class="post-tag">' + escapeHtml(tag) + '</span>').join('') + '</div>' : '';
|
|
278
|
+
const body = '<article class="post"><header><h1>' + escapeHtml(post.title) + '</h1><div class="meta post-byline">' + authorHtml +
|
|
279
|
+
'<time data-local-time data-time-locale="' + escapeHtml(post.locale || locale) + '" data-date-only="' + (post.dateOnly === true) + '" datetime="' + (post.dateOnly ? post.date.toISOString().slice(0,10) : post.date.toISOString()) + '">' + escapeHtml(dateLabel(post.date, post.locale || locale, post.dateOnly ? 'UTC' : config.site.timeZone, post.dateOnly)) + '</time>' +
|
|
280
|
+
'<span class="post-reading-time" data-reading-minutes="' + minutes + '">' + escapeHtml(label('postReadingTime').replace('{minutes}', minutes)) + '</span>' +
|
|
281
|
+
(post.updated ? '<span class="post-updated">' + escapeHtml(label('postUpdated')) + ' <time data-local-time data-time-locale="' + escapeHtml(locale) + '" datetime="' + post.updated.toISOString() + '">' + escapeHtml(dateLabel(post.updated, locale, config.site.timeZone, false)) + '</time></span>' : '') +
|
|
282
|
+
'</div>' + tagsHtml + '</header>' + (post.showChanges && post.changes ? await renderRevisionCards(post.changes,label,post.revisionImages,{linkResolver:value=>localizedContentUrl(config,locale,value)}) : '') + renderPostVideo(post.video, locale) + renderedMarkdown.toc + '<div class="post-content">' + renderedMarkdown.html + '</div></article>' + renderPostServices(post, locale, config);
|
|
283
|
+
const canonicalPath = urlFor(post.path);
|
|
284
|
+
return pageRoute(post.path + 'index.html', post.title, post.description || plainText(post.markdown).slice(0, 160), body,
|
|
285
|
+
locale, config, extensions, {
|
|
286
|
+
urlPath: canonicalPath, keywords: post.tags, image: post.image, robots: post.robots,
|
|
287
|
+
structuredData: {
|
|
288
|
+
'@context': 'https://schema.org', '@type': 'BlogPosting', headline: post.title,
|
|
289
|
+
description: post.description || plainText(post.markdown).slice(0, 160),
|
|
290
|
+
datePublished: post.dateOnly ? post.date.toISOString().slice(0,10) : post.date.toISOString(), dateModified: post.updated ? post.updated.toISOString() : post.dateOnly ? post.date.toISOString().slice(0,10) : post.date.toISOString(),
|
|
291
|
+
author: { '@type': 'Person', name: author.name || config.site.title },
|
|
292
|
+
mainEntityOfPage: config.site.url ? config.site.url + canonicalPath : undefined,
|
|
293
|
+
inLanguage: locale
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
routes.push(...postRoutes);
|
|
298
|
+
|
|
299
|
+
for (const page of pages) allPagePaths.add(urlFor(page.path));
|
|
300
|
+
const pageRoutes = await mapLimit(pages, config.concurrency, async (page) => {
|
|
301
|
+
const body = await pageBodyForDocument(page, site, config, locale, false);
|
|
302
|
+
return pageRoute(page.path ? page.path + 'index.html' : prefix + 'index.html', page.title,
|
|
303
|
+
page.description || config.site.description, body, locale, config, extensions, {
|
|
304
|
+
urlPath: urlFor(page.path), keywords: page.keywords, image: page.image, robots: page.robots
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
routes.push(...pageRoutes);
|
|
308
|
+
|
|
309
|
+
if (!homePage) allPagePaths.add(localizedUrl(config, locale, ''));
|
|
310
|
+
const archivePageSize = latestPostsPageSize || config.pagination.perPage;
|
|
311
|
+
const archivePageCount = Math.max(1, Math.ceil(archivePosts.length / archivePageSize));
|
|
312
|
+
for (let pageNumber = 1; pageNumber <= archivePageCount; pageNumber += 1) {
|
|
313
|
+
const subset = archivePosts.slice((pageNumber - 1) * archivePageSize, pageNumber * archivePageSize);
|
|
314
|
+
const cards = await Promise.all(subset.map((post) => renderPostCard(post, locale, config, true)));
|
|
315
|
+
const archiveUrl = pageNumber === 1 ? 'archives/' : 'archives/page/' + pageNumber + '/';
|
|
316
|
+
const pagination = archivePageCount > 1
|
|
317
|
+
? renderPagination(locale, pageNumber, archivePageCount, config, (page) => localizedUrl(config, locale,
|
|
318
|
+
page === 1 ? 'archives/' : 'archives/page/' + page + '/')) : '';
|
|
319
|
+
const archiveBody = '<section><h1>' + escapeHtml(label('archives')) + '</h1>' +
|
|
320
|
+
(cards.length ? '<div class="post-list archive-post-list">' + cards.join('') + '</div>' : '<p>' + escapeHtml(label('noPosts')) + '</p>') + pagination + '</section>';
|
|
321
|
+
routes.push(await pageRoute(prefix + (pageNumber === 1 ? 'archives/index.html' : 'archives/page/' + pageNumber + '/index.html'),
|
|
322
|
+
pageNumber === 1 ? label('archives') : label('archives') + ' ' + pageNumber, label('allPosts'), archiveBody, locale, config, extensions,
|
|
323
|
+
{ urlPath: localizedUrl(config, locale, archiveUrl) }));
|
|
324
|
+
allPagePaths.add(localizedUrl(config, locale, archiveUrl));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const categories = new Map();
|
|
328
|
+
for (const post of localizedPosts) {
|
|
329
|
+
const category = normalizeCategory(post.category);
|
|
330
|
+
if (!categories.has(category)) categories.set(category, []);
|
|
331
|
+
categories.get(category).push(post);
|
|
332
|
+
}
|
|
333
|
+
for (const [category, categoryPosts] of categories) {
|
|
334
|
+
const slug = categorySlug(category);
|
|
335
|
+
const title = category === 'blog' ? label('categoryBlog') : category === 'uncategorized' ? label('categoryUncategorized') : category;
|
|
336
|
+
const totalPages = Math.ceil(categoryPosts.length / config.pagination.perPage);
|
|
337
|
+
const categoryPath = number => 'categories/' + slug + '/' + (number === 1 ? '' : 'page/' + number + '/');
|
|
338
|
+
for (let number = 1; number <= totalPages; number += 1) {
|
|
339
|
+
const cards = await Promise.all(categoryPosts.slice((number - 1) * config.pagination.perPage, number * config.pagination.perPage)
|
|
340
|
+
.map(post => renderPostCard(post, locale, config, true)));
|
|
341
|
+
const pagination = totalPages > 1 ? renderPagination(locale, number, totalPages, config,
|
|
342
|
+
page => localizedUrl(config, locale, categoryPath(page))) : '';
|
|
343
|
+
const body = '<section><h1>' + escapeHtml(title) + '</h1><div class="post-list archive-post-list">' + cards.join('') + '</div>' + pagination + '</section>';
|
|
344
|
+
const urlPath = localizedUrl(config, locale, categoryPath(number));
|
|
345
|
+
allPagePaths.add(urlPath);
|
|
346
|
+
routes.push(await pageRoute(prefix + categoryPath(number) + 'index.html', title, title, body, locale, config, extensions, { urlPath }));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const searchable = [
|
|
351
|
+
...posts.map((post) => ({ title: post.title, author: post.author, date: post.dateOnly ? post.date.toISOString().slice(0,10) : post.date.toISOString(), dateOnly: post.dateOnly, url: urlFor(post.path), summary: plainText(post.description || post.markdown).slice(0, 220), content: plainText(post.markdown), type: 'post' })),
|
|
352
|
+
...pages.map((page) => ({ title: page.title, url: urlFor(page.path), summary: page.description || plainText(page.markdown).slice(0, 220), type: 'page' }))
|
|
353
|
+
];
|
|
354
|
+
routes.push(fileRoute(prefix + 'search.json', JSON.stringify(searchable), 'application/json; charset=utf-8'));
|
|
355
|
+
|
|
356
|
+
const searchBody = '<section class="local-search-page"><h1>' + escapeHtml(label('searchPosts')) + '</h1>' +
|
|
357
|
+
'<p>' + escapeHtml(label('searchIntro')) + '</p><form class="local-search-form" data-edgepress-search data-index="' +
|
|
358
|
+
escapeHtml(localizedUrl(config, locale, 'search.json')) + '"><label for="edgepress-search-query">' + escapeHtml(label('searchLabel')) +
|
|
359
|
+
'</label><div class="local-search-controls"><input id="edgepress-search-query" name="q" type="search" autocomplete="off" ' +
|
|
360
|
+
'><button type="submit">' + escapeHtml(label('searchButton')) + '</button></div>' +
|
|
361
|
+
'</form><p id="edgepress-search-status" role="status" aria-live="polite">' +
|
|
362
|
+
'' + '</p><ol id="edgepress-search-results" class="local-search-results"></ol>' +
|
|
363
|
+
'<noscript><p>' + escapeHtml(label('searchNeedsJavaScript')) + '</p></noscript></section>';
|
|
364
|
+
routes.push(await pageRoute(prefix + 'search/index.html', label('searchPosts'), label('searchIntro'), searchBody,
|
|
365
|
+
locale, config, extensions));
|
|
366
|
+
|
|
367
|
+
routes.push(await generateRssRoute(posts, config, locale));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const base = config.site.url;
|
|
371
|
+
const sitemap = '<?xml version="1.0" encoding="utf-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' +
|
|
372
|
+
[...allPagePaths].map((path) => '<url><loc>' + escapeXml(base + path) + '</loc></url>').join('') + '</urlset>';
|
|
373
|
+
routes.push(fileRoute('sitemap.xml', sitemap, 'application/xml; charset=utf-8'));
|
|
374
|
+
const agentNames = config.site.agentSeo.enabled ? config.site.agentSeo.agentCrawlers : [];
|
|
375
|
+
const robots = ['User-agent: *', 'Allow: /', ...agentNames.flatMap((name) => ['', 'User-agent: ' + name, 'Allow: /']), '',
|
|
376
|
+
'Sitemap: ' + (base ? base + '/sitemap.xml' : '/sitemap.xml'), ''].join('\n');
|
|
377
|
+
routes.push(fileRoute('robots.txt', robots, 'text/plain; charset=utf-8'));
|
|
378
|
+
if (config.preview) {
|
|
379
|
+
routes.push(fileRoute('__edgepress_preview.json', JSON.stringify({ buildId: config.previewBuildId }), 'application/json; charset=utf-8'));
|
|
380
|
+
routes.push(fileRoute('__edgepress_preview.js', '(()=>{let seen="";const poll=async()=>{try{const response=await fetch("/__edgepress_preview.json?ts="+Date.now(),{cache:"no-store"});if(!response.ok)return;const state=await response.json();if(seen&&state.buildId!==seen){window.location.reload();return}seen=state.buildId}catch{}};void poll();window.setInterval(poll,1000)})();', 'text/javascript; charset=utf-8'));
|
|
381
|
+
}
|
|
382
|
+
if (config.site.agentSeo.enabled && config.site.agentSeo.llmsTxt) {
|
|
383
|
+
const sections = [...site.pages.filter((page) => !page.draft), ...site.posts.slice(0, 30)];
|
|
384
|
+
const lines = ['# ' + config.site.title, '', config.site.agentSeo.summary || config.site.description, ''];
|
|
385
|
+
if (config.site.agentSeo.topics.length) lines.push('Topics: ' + config.site.agentSeo.topics.join(', '), '');
|
|
386
|
+
lines.push('## Project pages', '');
|
|
387
|
+
for (const page of sections.filter((item) => item.kind === 'pages')) lines.push('- [' + page.title + '](' + base + urlFor(page.path) + '): ' + (page.description || plainText(page.markdown).slice(0, 240)));
|
|
388
|
+
lines.push('', '## Recent posts', '');
|
|
389
|
+
for (const post of sections.filter((item) => item.kind === 'posts').slice(0, 30)) lines.push('- [' + post.title + '](' + base + urlFor(post.path) + '): ' + (post.description || plainText(post.markdown).slice(0, 240)));
|
|
390
|
+
routes.push(fileRoute('llms.txt', lines.join('\n') + '\n', 'text/plain; charset=utf-8'));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
for (const locale of config.i18n.locales) {
|
|
394
|
+
const prefix = locale === defaultLocale ? '' : locale + '/';
|
|
395
|
+
const notFoundTitle = translate(config, locale, 'notFound');
|
|
396
|
+
const notFoundText = translate(config, locale, 'notFoundText');
|
|
397
|
+
const notFoundBody = '<section class="error-page"><p class="error-code" aria-hidden="true">404</p><h1>' + escapeHtml(notFoundTitle) +
|
|
398
|
+
'</h1><p>' + escapeHtml(notFoundText) + '</p><p><a class="button" href="' + escapeHtml(localizedUrl(config, locale, '')) + '">' +
|
|
399
|
+
escapeHtml(translate(config, locale, 'returnHome')) + '</a></p></section>';
|
|
400
|
+
routes.push(await pageRoute(prefix + '404.html', notFoundTitle, notFoundText, notFoundBody, locale, config, extensions, { robots: 'noindex,follow' }));
|
|
401
|
+
const forbiddenTitle = translate(config, locale, 'forbidden');
|
|
402
|
+
const forbiddenText = translate(config, locale, 'forbiddenText');
|
|
403
|
+
const forbiddenBody = '<section class="error-page"><p class="error-code" aria-hidden="true">403</p><h1>' + escapeHtml(forbiddenTitle) +
|
|
404
|
+
'</h1><p>' + escapeHtml(forbiddenText) + '</p><p><a class="button" href="' + escapeHtml(localizedUrl(config, locale, '')) + '">' +
|
|
405
|
+
escapeHtml(translate(config, locale, 'returnHome')) + '</a></p></section>';
|
|
406
|
+
routes.push(await pageRoute(prefix + '403.html', forbiddenTitle, forbiddenText, forbiddenBody, locale, config, extensions, { robots: 'noindex,nofollow' }));
|
|
407
|
+
}
|
|
408
|
+
const discussions = config.browserPlugins.services.some(service => service.enabled !== false && service.provider === 'external-widget') ? [
|
|
409
|
+
...site.posts.filter(post => !post.draft).map(post => ({thread:post.bundlePath,title:post.title})),
|
|
410
|
+
...site.pages.filter(page => !page.draft && hasServiceBlock(page.blocks)).map(page => ({thread:'page:' + page.bundlePath,title:page.title}))
|
|
411
|
+
] : [];
|
|
412
|
+
routes.push(fileRoute('edgepress/service-contexts.json', JSON.stringify([...new Map(discussions.map(item => [item.thread,item])).values()]), 'application/json; charset=utf-8'));
|
|
413
|
+
return routes;
|
|
414
|
+
}
|