mdorigin 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/adapters/cloudflare.js +2 -0
- package/dist/cli/build-index.js +3 -0
- package/dist/core/extensions.d.ts +13 -0
- package/dist/core/request-handler.js +420 -12
- package/dist/core/router.d.ts +9 -0
- package/dist/core/router.js +26 -0
- package/dist/core/site-config.d.ts +53 -0
- package/dist/core/site-config.js +127 -0
- package/dist/core/site-url.d.ts +2 -0
- package/dist/core/site-url.js +6 -0
- package/dist/html/template.d.ts +17 -0
- package/dist/html/template.js +76 -33
- package/dist/html/theme.js +29 -0
- package/dist/i18n/messages.d.ts +45 -0
- package/dist/i18n/messages.js +104 -0
- package/dist/index-builder.d.ts +3 -1
- package/dist/index-builder.js +8 -2
- package/dist/search.js +1 -6
- package/package.json +4 -4
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { isIgnoredContentName } from './content-store.js';
|
|
3
|
-
import { inferDirectoryContentType } from './content-type.js';
|
|
3
|
+
import { inferDirectoryContentType, resolveContentType, } from './content-type.js';
|
|
4
4
|
import { getDirectoryIndexCandidates } from './directory-index.js';
|
|
5
|
-
import { extractManagedIndexEntries, getDocumentSummary, getDocumentTitle as getParsedDocumentTitle, parseMarkdownDocument, stripManagedIndexBlock, stripManagedIndexLinks, } from './markdown.js';
|
|
5
|
+
import { extractManagedIndexEntries, getDocumentSummary, getDocumentTitle as getParsedDocumentTitle, parseMarkdownDocument, stripManagedIndexBlock, stripManagedIndexLinks, stripMachineOnlyMarkdownComments, } from './markdown.js';
|
|
6
|
+
import { ensureTrailingSlash, trimLeadingSlash } from './site-url.js';
|
|
6
7
|
import { applyIndexTransforms, renderFooterOverride, renderHeaderOverride, renderPageWithPlugins, transformHtmlWithPlugins, } from './extensions.js';
|
|
7
8
|
import { handleApiRoute } from './api.js';
|
|
8
|
-
import { normalizeRequestPath, resolveRequest } from './router.js';
|
|
9
|
+
import { matchRequestLocale, normalizeRequestPath, resolveRequest } from './router.js';
|
|
9
10
|
import { escapeHtml, renderListingArticleItems, renderDocument, } from '../html/template.js';
|
|
11
|
+
import { formatSiteMessage, resolveSiteMessages, } from '../i18n/messages.js';
|
|
10
12
|
export async function handleSiteRequest(store, pathname, options) {
|
|
11
13
|
const plugins = options.plugins ?? [];
|
|
12
14
|
const searchEnabled = options.searchApi !== undefined;
|
|
15
|
+
const requestLocale = matchRequestLocale(pathname, options.siteConfig.locales);
|
|
16
|
+
const defaultLocale = options.siteConfig.locales?.find((locale) => locale.isDefault);
|
|
17
|
+
if (defaultLocale !== undefined && defaultLocale.pathPrefix !== '' && pathname === '/') {
|
|
18
|
+
return redirect(`${defaultLocale.pathPrefix}/`);
|
|
19
|
+
}
|
|
13
20
|
const apiRoute = await handleApiRoute(pathname, options.searchParams, {
|
|
14
21
|
searchApi: options.searchApi,
|
|
15
22
|
siteConfig: options.siteConfig,
|
|
@@ -21,6 +28,16 @@ export async function handleSiteRequest(store, pathname, options) {
|
|
|
21
28
|
if (pathname === '/sitemap.xml') {
|
|
22
29
|
return renderSitemap(store, options);
|
|
23
30
|
}
|
|
31
|
+
if (pathname === '/feed.xml') {
|
|
32
|
+
return renderRssFeed(store, options, defaultLocale ?? null);
|
|
33
|
+
}
|
|
34
|
+
if (options.siteConfig.locales !== undefined && requestLocale !== null) {
|
|
35
|
+
for (const locale of options.siteConfig.locales) {
|
|
36
|
+
if (locale.pathPrefix !== '' && pathname === `${locale.pathPrefix}/feed.xml`) {
|
|
37
|
+
return renderRssFeed(store, options, locale);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
24
41
|
const resolved = resolveRequest(pathname);
|
|
25
42
|
const listingFragmentRequest = getListingFragmentRequest(options.searchParams);
|
|
26
43
|
const negotiatedMarkdown = shouldServeMarkdownForRequest(resolved, options.acceptHeader);
|
|
@@ -29,7 +46,7 @@ export async function handleSiteRequest(store, pathname, options) {
|
|
|
29
46
|
if (aliasRedirect !== null) {
|
|
30
47
|
return aliasRedirect;
|
|
31
48
|
}
|
|
32
|
-
return
|
|
49
|
+
return renderNotFoundForRequest(store, pathname, options);
|
|
33
50
|
}
|
|
34
51
|
const entry = await store.get(resolved.sourcePath);
|
|
35
52
|
if (entry === null) {
|
|
@@ -45,25 +62,29 @@ export async function handleSiteRequest(store, pathname, options) {
|
|
|
45
62
|
if (alternateMarkdownRedirect !== null) {
|
|
46
63
|
return alternateMarkdownRedirect;
|
|
47
64
|
}
|
|
65
|
+
const canonicalDirectoryRedirect = await tryRedirectCanonicalDirectoryPath(store, resolved);
|
|
66
|
+
if (canonicalDirectoryRedirect !== null) {
|
|
67
|
+
return canonicalDirectoryRedirect;
|
|
68
|
+
}
|
|
48
69
|
if (resolved.kind === 'html' && resolved.requestPath.endsWith('/')) {
|
|
49
70
|
const directoryIndexResponse = await tryRenderAlternateDirectoryIndex(store, resolved.requestPath, options);
|
|
50
71
|
if (directoryIndexResponse !== null) {
|
|
51
72
|
return directoryIndexResponse;
|
|
52
73
|
}
|
|
53
|
-
return renderDirectoryListing(store, resolved.requestPath, options.siteConfig, searchEnabled);
|
|
74
|
+
return renderDirectoryListing(store, resolved.requestPath, options.siteConfig, searchEnabled, requestLocale);
|
|
54
75
|
}
|
|
55
|
-
return
|
|
76
|
+
return renderNotFoundForResolvedRequest(store, resolved, options, negotiatedMarkdown, requestLocale);
|
|
56
77
|
}
|
|
57
78
|
if (resolved.kind === 'asset') {
|
|
58
79
|
return serveAsset(entry);
|
|
59
80
|
}
|
|
60
81
|
if (entry.kind !== 'text' || entry.text === undefined) {
|
|
61
|
-
return
|
|
82
|
+
return renderNotFoundForResolvedRequest(store, resolved, options, negotiatedMarkdown, requestLocale);
|
|
62
83
|
}
|
|
63
84
|
if (resolved.kind === 'markdown' || negotiatedMarkdown) {
|
|
64
85
|
const parsed = await parseMarkdownDocument(resolved.sourcePath, entry.text);
|
|
65
86
|
if (parsed.meta.draft === true && options.draftMode === 'exclude') {
|
|
66
|
-
return
|
|
87
|
+
return renderNotFoundForResolvedRequest(store, resolved, options, negotiatedMarkdown, requestLocale);
|
|
67
88
|
}
|
|
68
89
|
return {
|
|
69
90
|
status: 200,
|
|
@@ -75,7 +96,7 @@ export async function handleSiteRequest(store, pathname, options) {
|
|
|
75
96
|
}
|
|
76
97
|
const parsed = await parseMarkdownDocument(resolved.sourcePath, entry.text);
|
|
77
98
|
if (parsed.meta.draft === true && options.draftMode === 'exclude') {
|
|
78
|
-
return
|
|
99
|
+
return renderNotFoundForResolvedRequest(store, resolved, options, negotiatedMarkdown, requestLocale);
|
|
79
100
|
}
|
|
80
101
|
const navigation = await resolveTopNav(store, options.siteConfig);
|
|
81
102
|
const renderedBody = isRootHomeRequest(resolved.requestPath) && !options.siteConfig.showHomeIndex
|
|
@@ -106,6 +127,9 @@ export async function handleSiteRequest(store, pathname, options) {
|
|
|
106
127
|
listingEntries,
|
|
107
128
|
searchEnabled,
|
|
108
129
|
plugins,
|
|
130
|
+
store,
|
|
131
|
+
requestLocale,
|
|
132
|
+
draftMode: options.draftMode,
|
|
109
133
|
varyOnAccept: shouldVaryOnAccept(resolved),
|
|
110
134
|
});
|
|
111
135
|
}
|
|
@@ -140,6 +164,8 @@ function buildPageRenderModel(options) {
|
|
|
140
164
|
kind: options.listingEntries.length > 0 ? 'listing' : 'page',
|
|
141
165
|
requestPath: options.resolvedRequestPath,
|
|
142
166
|
sourcePath: options.sourcePath,
|
|
167
|
+
locale: options.locale,
|
|
168
|
+
languages: options.languages,
|
|
143
169
|
siteTitle: options.siteConfig.siteTitle,
|
|
144
170
|
siteDescription: options.siteConfig.siteDescription,
|
|
145
171
|
siteUrl: options.siteConfig.siteUrl,
|
|
@@ -172,9 +198,18 @@ function buildPageRenderModel(options) {
|
|
|
172
198
|
};
|
|
173
199
|
}
|
|
174
200
|
async function renderStructuredPage(options) {
|
|
201
|
+
const localeConfig = options.requestLocale;
|
|
202
|
+
const localeCode = localeConfig?.code ?? options.siteConfig.locale;
|
|
203
|
+
const messages = getEffectiveLocaleMessages(options.siteConfig, localeConfig);
|
|
204
|
+
const htmlLang = getFrontmatterLocale(options.parsed.meta) ?? localeCode;
|
|
205
|
+
const languages = await buildLanguageOptions(options.store, { sourcePath: options.sourcePath }, localeConfig, options.siteConfig, options.draftMode);
|
|
206
|
+
const hreflangAlternates = buildHreflangAlternates(languages, options.siteConfig);
|
|
207
|
+
const searchLocaleFilter = buildSearchLocaleFilter(options.siteConfig, localeConfig);
|
|
175
208
|
const page = buildPageRenderModel({
|
|
176
209
|
resolvedRequestPath: options.requestPath,
|
|
177
210
|
sourcePath: options.sourcePath,
|
|
211
|
+
locale: localeCode,
|
|
212
|
+
languages,
|
|
178
213
|
renderedBodyHtml: options.renderedParsed.html,
|
|
179
214
|
parsed: options.parsed,
|
|
180
215
|
siteConfig: options.siteConfig,
|
|
@@ -196,6 +231,8 @@ async function renderStructuredPage(options) {
|
|
|
196
231
|
return renderDocument({
|
|
197
232
|
siteTitle: currentPage.siteTitle,
|
|
198
233
|
siteDescription: currentPage.siteDescription,
|
|
234
|
+
locale: htmlLang,
|
|
235
|
+
messages,
|
|
199
236
|
siteUrl: currentPage.siteUrl,
|
|
200
237
|
favicon: currentPage.favicon,
|
|
201
238
|
socialImage: currentPage.socialImage,
|
|
@@ -214,11 +251,15 @@ async function renderStructuredPage(options) {
|
|
|
214
251
|
stylesheetContent: currentPage.stylesheetContent,
|
|
215
252
|
canonicalPath: currentPage.canonicalPath,
|
|
216
253
|
alternateMarkdownPath: currentPage.alternateMarkdownPath,
|
|
254
|
+
rssFeedUrl: getRssFeedUrl(currentPage.siteUrl, options.siteConfig, localeConfig),
|
|
217
255
|
listingEntries: currentPage.listingEntries,
|
|
218
256
|
listingRequestPath: currentPage.listingRequestPath,
|
|
219
257
|
listingInitialPostCount: currentPage.listingInitialPostCount,
|
|
220
258
|
listingLoadMoreStep: currentPage.listingLoadMoreStep,
|
|
221
259
|
searchEnabled: currentPage.searchEnabled,
|
|
260
|
+
languages: currentPage.languages,
|
|
261
|
+
hreflangAlternates,
|
|
262
|
+
searchLocaleFilter,
|
|
222
263
|
headerHtml,
|
|
223
264
|
footerHtml,
|
|
224
265
|
});
|
|
@@ -281,6 +322,69 @@ function notFound() {
|
|
|
281
322
|
body: 'Not Found',
|
|
282
323
|
};
|
|
283
324
|
}
|
|
325
|
+
async function renderNotFoundForRequest(store, pathname, options) {
|
|
326
|
+
const resolved = resolveRequest(pathname);
|
|
327
|
+
return renderNotFoundForResolvedRequest(store, resolved, options, false, matchRequestLocale(pathname, options.siteConfig.locales));
|
|
328
|
+
}
|
|
329
|
+
async function renderNotFoundForResolvedRequest(store, resolved, options, negotiatedMarkdown, requestLocale) {
|
|
330
|
+
const varyOnAccept = shouldVaryOnAccept(resolved);
|
|
331
|
+
if (resolved.kind !== 'html' || negotiatedMarkdown) {
|
|
332
|
+
return withNotFoundVary(varyOnAccept);
|
|
333
|
+
}
|
|
334
|
+
return renderHtmlNotFound(store, resolved.requestPath, options, varyOnAccept, requestLocale);
|
|
335
|
+
}
|
|
336
|
+
async function renderHtmlNotFound(store, requestPath, options, varyOnAccept, requestLocale) {
|
|
337
|
+
const navigation = await resolveTopNav(store, options.siteConfig);
|
|
338
|
+
const localeConfig = requestLocale;
|
|
339
|
+
const locale = localeConfig?.code ?? options.siteConfig.locale;
|
|
340
|
+
const messages = getEffectiveLocaleMessages(options.siteConfig, localeConfig);
|
|
341
|
+
const languages = await buildLanguageOptions(store, {}, localeConfig, options.siteConfig, options.draftMode);
|
|
342
|
+
const body = [
|
|
343
|
+
`<h1>${escapeHtml(messages['error.notFoundTitle'])}</h1>`,
|
|
344
|
+
`<p>${formatSiteMessage(messages['error.notFoundBody'], {
|
|
345
|
+
path: escapeHtml(requestPath),
|
|
346
|
+
})}</p>`,
|
|
347
|
+
].join('');
|
|
348
|
+
return {
|
|
349
|
+
status: 404,
|
|
350
|
+
headers: withVaryAcceptIfNeeded({
|
|
351
|
+
'content-type': 'text/html; charset=utf-8',
|
|
352
|
+
}, varyOnAccept),
|
|
353
|
+
body: renderDocument({
|
|
354
|
+
siteTitle: options.siteConfig.siteTitle,
|
|
355
|
+
siteDescription: options.siteConfig.siteDescription,
|
|
356
|
+
siteUrl: options.siteConfig.siteUrl,
|
|
357
|
+
favicon: options.siteConfig.favicon,
|
|
358
|
+
socialImage: options.siteConfig.socialImage,
|
|
359
|
+
logo: options.siteConfig.logo,
|
|
360
|
+
title: messages['error.notFoundTitle'],
|
|
361
|
+
body,
|
|
362
|
+
locale,
|
|
363
|
+
messages,
|
|
364
|
+
languages,
|
|
365
|
+
showSummary: false,
|
|
366
|
+
showDate: false,
|
|
367
|
+
topNav: navigation.items,
|
|
368
|
+
footerNav: options.siteConfig.footerNav,
|
|
369
|
+
footerText: options.siteConfig.footerText,
|
|
370
|
+
socialLinks: options.siteConfig.socialLinks,
|
|
371
|
+
stylesheetContent: options.siteConfig.stylesheetContent,
|
|
372
|
+
rssFeedUrl: getRssFeedUrl(options.siteConfig.siteUrl, options.siteConfig, localeConfig),
|
|
373
|
+
searchEnabled: options.searchApi !== undefined,
|
|
374
|
+
}),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function withNotFoundVary(varyOnAccept) {
|
|
378
|
+
if (!varyOnAccept) {
|
|
379
|
+
return notFound();
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
...notFound(),
|
|
383
|
+
headers: withVaryAcceptIfNeeded({
|
|
384
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
385
|
+
}, true),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
284
388
|
function redirect(location) {
|
|
285
389
|
return {
|
|
286
390
|
status: 308,
|
|
@@ -289,6 +393,127 @@ function redirect(location) {
|
|
|
289
393
|
},
|
|
290
394
|
};
|
|
291
395
|
}
|
|
396
|
+
function getEffectiveLocaleMessages(siteConfig, localeConfig) {
|
|
397
|
+
if (localeConfig === null) {
|
|
398
|
+
return resolveSiteMessages(siteConfig.locale, siteConfig.messages);
|
|
399
|
+
}
|
|
400
|
+
return resolveSiteMessages(localeConfig.code, {
|
|
401
|
+
...siteConfig.messages,
|
|
402
|
+
...localeConfig.messages,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
function getFrontmatterLocale(meta) {
|
|
406
|
+
if (typeof meta.lang !== 'string') {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
const trimmed = meta.lang.trim();
|
|
410
|
+
return trimmed === '' ? null : trimmed;
|
|
411
|
+
}
|
|
412
|
+
async function buildLanguageOptions(store, target, currentLocale, siteConfig, draftMode) {
|
|
413
|
+
const locales = siteConfig.locales;
|
|
414
|
+
if (locales === undefined || locales.length === 0) {
|
|
415
|
+
return [];
|
|
416
|
+
}
|
|
417
|
+
const languages = [];
|
|
418
|
+
for (const locale of locales) {
|
|
419
|
+
const translated = await localeHasContent(store, target, currentLocale, locale, draftMode);
|
|
420
|
+
languages.push({
|
|
421
|
+
code: locale.code,
|
|
422
|
+
label: locale.label,
|
|
423
|
+
href: translated === null
|
|
424
|
+
? getLocaleHomePath(locale)
|
|
425
|
+
: getCanonicalHtmlPathForContentPath(translated),
|
|
426
|
+
current: locale === currentLocale,
|
|
427
|
+
translated: translated !== null,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
return languages;
|
|
431
|
+
}
|
|
432
|
+
async function localeHasContent(store, target, currentLocale, locale, draftMode) {
|
|
433
|
+
if (locale === currentLocale) {
|
|
434
|
+
return target.sourcePath
|
|
435
|
+
? target.sourcePath
|
|
436
|
+
: target.directoryPath === ''
|
|
437
|
+
? 'index.md'
|
|
438
|
+
: `${target.directoryPath}/index.md`;
|
|
439
|
+
}
|
|
440
|
+
const candidates = [];
|
|
441
|
+
if (target.sourcePath !== undefined) {
|
|
442
|
+
candidates.push(mapContentPathForLocale(target.sourcePath, currentLocale, locale));
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
const directoryPath = target.directoryPath ?? '';
|
|
446
|
+
for (const candidate of getDirectoryIndexCandidates(mapContentPathForLocale(directoryPath, currentLocale, locale))) {
|
|
447
|
+
candidates.push(candidate);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
for (const candidate of candidates) {
|
|
451
|
+
if (await hasPublishedContent(store, candidate, draftMode)) {
|
|
452
|
+
return candidate;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
function mapContentPathForLocale(contentPath, from, to) {
|
|
458
|
+
if (from === null || from.contentBase === '') {
|
|
459
|
+
return to.contentBase === '' ? contentPath : `${to.contentBase}/${contentPath}`;
|
|
460
|
+
}
|
|
461
|
+
if (!contentPath.startsWith(`${from.contentBase}/`)) {
|
|
462
|
+
return contentPath;
|
|
463
|
+
}
|
|
464
|
+
const rest = contentPath.slice(from.contentBase.length + 1);
|
|
465
|
+
return to.contentBase === '' ? rest : `${to.contentBase}/${rest}`;
|
|
466
|
+
}
|
|
467
|
+
function getLocaleHomePath(locale) {
|
|
468
|
+
return locale.pathPrefix === '' ? '/' : `${locale.pathPrefix}/`;
|
|
469
|
+
}
|
|
470
|
+
async function hasPublishedContent(store, contentPath, draftMode) {
|
|
471
|
+
const entry = await store.get(contentPath);
|
|
472
|
+
if (entry === null || entry.kind !== 'text' || entry.text === undefined) {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
if (draftMode === 'exclude') {
|
|
476
|
+
const parsed = await parseMarkdownDocument(contentPath, entry.text);
|
|
477
|
+
if (parsed.meta.draft === true) {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
function buildHreflangAlternates(languages, siteConfig) {
|
|
484
|
+
if (languages.length === 0 || siteConfig.siteUrl === undefined) {
|
|
485
|
+
return [];
|
|
486
|
+
}
|
|
487
|
+
const base = ensureTrailingSlash(siteConfig.siteUrl);
|
|
488
|
+
const alternates = languages
|
|
489
|
+
.filter((language) => language.translated)
|
|
490
|
+
.map((language) => ({
|
|
491
|
+
hreflang: language.code,
|
|
492
|
+
href: new URL(trimLeadingSlash(language.href), base).toString(),
|
|
493
|
+
}));
|
|
494
|
+
const defaultLocale = siteConfig.locales?.find((locale) => locale.isDefault);
|
|
495
|
+
const defaultLanguage = defaultLocale
|
|
496
|
+
? languages.find((language) => language.code === defaultLocale.code)
|
|
497
|
+
: undefined;
|
|
498
|
+
if (defaultLanguage?.translated) {
|
|
499
|
+
alternates.push({
|
|
500
|
+
hreflang: 'x-default',
|
|
501
|
+
href: new URL(trimLeadingSlash(defaultLanguage.href), base).toString(),
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
return alternates;
|
|
505
|
+
}
|
|
506
|
+
function buildSearchLocaleFilter(siteConfig, localeConfig) {
|
|
507
|
+
if (siteConfig.locales === undefined || localeConfig === null) {
|
|
508
|
+
return undefined;
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
include: localeConfig.contentBase === '' ? '' : `${localeConfig.contentBase}/`,
|
|
512
|
+
exclude: siteConfig.locales
|
|
513
|
+
.filter((locale) => locale !== localeConfig && locale.contentBase !== '')
|
|
514
|
+
.map((locale) => `${locale.contentBase}/`),
|
|
515
|
+
};
|
|
516
|
+
}
|
|
292
517
|
async function renderSitemap(store, options) {
|
|
293
518
|
if (!options.siteConfig.siteUrl) {
|
|
294
519
|
return {
|
|
@@ -317,6 +542,62 @@ async function renderSitemap(store, options) {
|
|
|
317
542
|
body,
|
|
318
543
|
};
|
|
319
544
|
}
|
|
545
|
+
async function renderRssFeed(store, options, locale) {
|
|
546
|
+
if (!isRssEnabled(options.siteConfig) || !options.siteConfig.siteUrl) {
|
|
547
|
+
return notFound();
|
|
548
|
+
}
|
|
549
|
+
const excludedLocaleDirectories = new Set((options.siteConfig.locales ?? [])
|
|
550
|
+
.filter((entry) => entry.contentBase !== '' && entry !== locale)
|
|
551
|
+
.map((entry) => entry.contentBase));
|
|
552
|
+
const items = await collectRssFeedItems(store, locale?.contentBase ?? '', options, excludedLocaleDirectories);
|
|
553
|
+
const limitedItems = items.slice(0, options.siteConfig.rss?.maxItems ?? 20);
|
|
554
|
+
const rssFeedUrl = getRssFeedUrl(options.siteConfig.siteUrl, options.siteConfig, locale);
|
|
555
|
+
const title = options.siteConfig.rss?.title ?? options.siteConfig.siteTitle;
|
|
556
|
+
const description = options.siteConfig.rss?.description ?? options.siteConfig.siteDescription;
|
|
557
|
+
const lastBuildDate = limitedItems[0]?.pubDate.toUTCString();
|
|
558
|
+
const body = [
|
|
559
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
560
|
+
'<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">',
|
|
561
|
+
'<channel>',
|
|
562
|
+
` <title>${escapeHtml(title)}</title>`,
|
|
563
|
+
` <link>${escapeHtml(options.siteConfig.siteUrl)}</link>`,
|
|
564
|
+
description
|
|
565
|
+
? ` <description>${escapeHtml(description)}</description>`
|
|
566
|
+
: ' <description></description>',
|
|
567
|
+
' <generator>mdorigin</generator>',
|
|
568
|
+
rssFeedUrl
|
|
569
|
+
? ` <atom:link href="${escapeHtml(rssFeedUrl)}" rel="self" type="application/rss+xml" />`
|
|
570
|
+
: '',
|
|
571
|
+
options.siteConfig.rss?.author
|
|
572
|
+
? ` <managingEditor>${escapeHtml(options.siteConfig.rss.author)}</managingEditor>`
|
|
573
|
+
: '',
|
|
574
|
+
lastBuildDate ? ` <lastBuildDate>${escapeHtml(lastBuildDate)}</lastBuildDate>` : '',
|
|
575
|
+
...limitedItems.map((item) => [
|
|
576
|
+
' <item>',
|
|
577
|
+
` <title>${escapeHtml(item.title)}</title>`,
|
|
578
|
+
` <link>${escapeHtml(item.absoluteUrl)}</link>`,
|
|
579
|
+
` <guid isPermaLink="true">${escapeHtml(item.absoluteUrl)}</guid>`,
|
|
580
|
+
` <pubDate>${escapeHtml(item.pubDate.toUTCString())}</pubDate>`,
|
|
581
|
+
item.summary
|
|
582
|
+
? ` <description>${escapeHtml(item.summary)}</description>`
|
|
583
|
+
: '',
|
|
584
|
+
' </item>',
|
|
585
|
+
]
|
|
586
|
+
.filter((line) => line !== '')
|
|
587
|
+
.join('\n')),
|
|
588
|
+
'</channel>',
|
|
589
|
+
'</rss>',
|
|
590
|
+
]
|
|
591
|
+
.filter((line) => line !== '')
|
|
592
|
+
.join('\n');
|
|
593
|
+
return {
|
|
594
|
+
status: 200,
|
|
595
|
+
headers: {
|
|
596
|
+
'content-type': 'application/rss+xml; charset=utf-8',
|
|
597
|
+
},
|
|
598
|
+
body,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
320
601
|
function withVaryAcceptIfNeeded(headers, enabled) {
|
|
321
602
|
if (!enabled) {
|
|
322
603
|
return headers;
|
|
@@ -387,7 +668,78 @@ function dedupeSitemapEntries(entries) {
|
|
|
387
668
|
}
|
|
388
669
|
return Array.from(deduped.values());
|
|
389
670
|
}
|
|
390
|
-
async function
|
|
671
|
+
async function collectRssFeedItems(store, directoryPath, options, excludedDirectories) {
|
|
672
|
+
const entries = await store.listDirectory(directoryPath);
|
|
673
|
+
if (entries === null) {
|
|
674
|
+
return [];
|
|
675
|
+
}
|
|
676
|
+
const feedItems = [];
|
|
677
|
+
const directoryShape = inspectDirectoryShapeEntries(entries);
|
|
678
|
+
for (const entry of entries) {
|
|
679
|
+
if (entry.kind === 'directory') {
|
|
680
|
+
if (excludedDirectories.has(entry.path)) {
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
feedItems.push(...(await collectRssFeedItems(store, entry.path, options, excludedDirectories)));
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
if (!isMarkdownEntry(entry)) {
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
const document = await store.get(entry.path);
|
|
690
|
+
if (document === null || document.kind !== 'text' || document.text === undefined) {
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
const parsed = await parseMarkdownDocument(entry.path, document.text);
|
|
694
|
+
if (parsed.meta.draft === true && options.draftMode === 'exclude') {
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
const pubDate = parseFeedDate(parsed.meta.date);
|
|
698
|
+
if (pubDate === null) {
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
const contentType = inferFeedContentType(entry.path, parsed.meta, directoryShape);
|
|
702
|
+
if (contentType !== 'post') {
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
const canonicalPath = getCanonicalHtmlPathForContentPath(entry.path);
|
|
706
|
+
feedItems.push({
|
|
707
|
+
title: getDocumentTitle(parsed),
|
|
708
|
+
canonicalPath,
|
|
709
|
+
absoluteUrl: new URL(trimLeadingSlash(canonicalPath), ensureTrailingSlash(options.siteConfig.siteUrl ?? '')).toString(),
|
|
710
|
+
summary: getFeedSummary(parsed),
|
|
711
|
+
pubDate,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
feedItems.sort((left, right) => {
|
|
715
|
+
const timeDelta = right.pubDate.getTime() - left.pubDate.getTime();
|
|
716
|
+
return timeDelta !== 0
|
|
717
|
+
? timeDelta
|
|
718
|
+
: left.canonicalPath.localeCompare(right.canonicalPath);
|
|
719
|
+
});
|
|
720
|
+
return feedItems;
|
|
721
|
+
}
|
|
722
|
+
function inferFeedContentType(contentPath, meta, directoryShape) {
|
|
723
|
+
const explicitType = resolveContentType(meta);
|
|
724
|
+
if (explicitType) {
|
|
725
|
+
return explicitType;
|
|
726
|
+
}
|
|
727
|
+
if (isDirectoryIndexContentPath(contentPath)) {
|
|
728
|
+
return inferDirectoryContentType(meta, directoryShape);
|
|
729
|
+
}
|
|
730
|
+
return typeof meta.date === 'string' && meta.date !== '' ? 'post' : 'page';
|
|
731
|
+
}
|
|
732
|
+
function getFeedSummary(parsed) {
|
|
733
|
+
return getDocumentSummary(parsed.meta, stripMachineOnlyMarkdownComments(stripManagedIndexBlock(parsed.body)));
|
|
734
|
+
}
|
|
735
|
+
function parseFeedDate(value) {
|
|
736
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
const parsed = new Date(value);
|
|
740
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
741
|
+
}
|
|
742
|
+
async function renderDirectoryListing(store, requestPath, siteConfig, searchEnabled, requestLocale) {
|
|
391
743
|
const directoryPath = requestPath === '/' ? '' : requestPath.slice(1).replace(/\/$/, '');
|
|
392
744
|
const entries = await store.listDirectory(directoryPath);
|
|
393
745
|
if (entries === null) {
|
|
@@ -395,12 +747,17 @@ async function renderDirectoryListing(store, requestPath, siteConfig, searchEnab
|
|
|
395
747
|
}
|
|
396
748
|
const visibleEntries = entries.filter(isVisibleDirectoryEntry);
|
|
397
749
|
const navigation = await resolveTopNav(store, siteConfig);
|
|
750
|
+
const locale = requestLocale?.code ?? siteConfig.locale;
|
|
751
|
+
const messages = getEffectiveLocaleMessages(siteConfig, requestLocale);
|
|
752
|
+
const languages = await buildLanguageOptions(store, { directoryPath }, requestLocale, siteConfig, 'exclude');
|
|
398
753
|
const listItems = visibleEntries
|
|
399
754
|
.map((entry) => `<li><a href="${getDirectoryEntryHref(requestPath, entry)}">${escapeHtml(getDirectoryEntryLabel(entry))}</a></li>`)
|
|
400
755
|
.join('');
|
|
401
756
|
const body = [
|
|
402
757
|
`<h1>${escapeHtml(getDirectoryTitle(requestPath))}</h1>`,
|
|
403
|
-
visibleEntries.length > 0
|
|
758
|
+
visibleEntries.length > 0
|
|
759
|
+
? `<ul>${listItems}</ul>`
|
|
760
|
+
: `<p>${escapeHtml(messages['listing.emptyDirectory'])}</p>`,
|
|
404
761
|
].join('');
|
|
405
762
|
return {
|
|
406
763
|
status: 200,
|
|
@@ -410,6 +767,9 @@ async function renderDirectoryListing(store, requestPath, siteConfig, searchEnab
|
|
|
410
767
|
body: renderDocument({
|
|
411
768
|
siteTitle: siteConfig.siteTitle,
|
|
412
769
|
siteDescription: siteConfig.siteDescription,
|
|
770
|
+
locale,
|
|
771
|
+
messages,
|
|
772
|
+
languages,
|
|
413
773
|
siteUrl: siteConfig.siteUrl,
|
|
414
774
|
favicon: siteConfig.favicon,
|
|
415
775
|
logo: siteConfig.logo,
|
|
@@ -424,6 +784,7 @@ async function renderDirectoryListing(store, requestPath, siteConfig, searchEnab
|
|
|
424
784
|
stylesheetContent: siteConfig.stylesheetContent,
|
|
425
785
|
canonicalPath: requestPath,
|
|
426
786
|
alternateMarkdownPath: getMarkdownRequestPathForContentPath(getDirectoryIndexContentPathForRequestPath(requestPath)),
|
|
787
|
+
rssFeedUrl: getRssFeedUrl(siteConfig.siteUrl, siteConfig, requestLocale),
|
|
427
788
|
searchEnabled,
|
|
428
789
|
}),
|
|
429
790
|
};
|
|
@@ -499,6 +860,9 @@ async function tryRenderAlternateDirectoryIndex(store, requestPath, options) {
|
|
|
499
860
|
listingEntries,
|
|
500
861
|
searchEnabled: options.searchApi !== undefined,
|
|
501
862
|
plugins,
|
|
863
|
+
store,
|
|
864
|
+
requestLocale: matchRequestLocale(requestPath, options.siteConfig.locales),
|
|
865
|
+
draftMode: options.draftMode,
|
|
502
866
|
});
|
|
503
867
|
}
|
|
504
868
|
return null;
|
|
@@ -558,6 +922,23 @@ async function tryRedirectAlternateDirectoryMarkdown(store, resolved, options) {
|
|
|
558
922
|
}
|
|
559
923
|
return null;
|
|
560
924
|
}
|
|
925
|
+
async function tryRedirectCanonicalDirectoryPath(store, resolved) {
|
|
926
|
+
if (resolved.kind !== 'html' || resolved.requestPath.endsWith('/')) {
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
if (path.posix.extname(resolved.requestPath) !== '') {
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
const directoryPath = resolved.requestPath.slice(1);
|
|
933
|
+
if (directoryPath === '') {
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
const directoryEntries = await store.listDirectory(directoryPath);
|
|
937
|
+
if (directoryEntries === null) {
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
return redirect(`${resolved.requestPath}/`);
|
|
941
|
+
}
|
|
561
942
|
function getMarkdownRequestPathForContentPath(contentPath) {
|
|
562
943
|
return `/${contentPath}`;
|
|
563
944
|
}
|
|
@@ -628,6 +1009,10 @@ async function findAliasRedirectLocation(store, directoryPath, requestPath, opti
|
|
|
628
1009
|
function isMarkdownEntry(entry) {
|
|
629
1010
|
return path.posix.extname(entry.name).toLowerCase() === '.md';
|
|
630
1011
|
}
|
|
1012
|
+
function isDirectoryIndexContentPath(contentPath) {
|
|
1013
|
+
const basename = path.posix.basename(contentPath).toLowerCase();
|
|
1014
|
+
return basename === 'index.md' || basename === 'readme.md' || basename === 'skill.md';
|
|
1015
|
+
}
|
|
631
1016
|
function normalizeAliases(aliases) {
|
|
632
1017
|
if (!Array.isArray(aliases)) {
|
|
633
1018
|
return [];
|
|
@@ -642,12 +1027,26 @@ function normalizeAliases(aliases) {
|
|
|
642
1027
|
}
|
|
643
1028
|
function getCanonicalHtmlPathForContentPath(contentPath) {
|
|
644
1029
|
const basename = path.posix.basename(contentPath).toLowerCase();
|
|
645
|
-
if (basename === 'index.md' ||
|
|
1030
|
+
if (basename === 'index.md' ||
|
|
1031
|
+
basename === 'readme.md' ||
|
|
1032
|
+
basename === 'skill.md') {
|
|
646
1033
|
const directory = path.posix.dirname(contentPath);
|
|
647
1034
|
return directory === '.' ? '/' : `/${directory}/`;
|
|
648
1035
|
}
|
|
649
1036
|
return `/${contentPath.slice(0, -'.md'.length)}`;
|
|
650
1037
|
}
|
|
1038
|
+
function isRssEnabled(siteConfig) {
|
|
1039
|
+
return Boolean(siteConfig.siteUrl) && (siteConfig.rss?.enabled ?? true);
|
|
1040
|
+
}
|
|
1041
|
+
function getRssFeedUrl(siteUrl, siteConfig, locale) {
|
|
1042
|
+
if (!siteUrl || !isRssEnabled(siteConfig)) {
|
|
1043
|
+
return undefined;
|
|
1044
|
+
}
|
|
1045
|
+
const feedPath = locale !== null && locale.pathPrefix !== ''
|
|
1046
|
+
? `${locale.pathPrefix.slice(1)}/feed.xml`
|
|
1047
|
+
: 'feed.xml';
|
|
1048
|
+
return new URL(feedPath, ensureTrailingSlash(siteUrl)).toString();
|
|
1049
|
+
}
|
|
651
1050
|
function getEditLinkHref(siteConfig, sourcePath) {
|
|
652
1051
|
if (!siteConfig.editLink || !sourcePath) {
|
|
653
1052
|
return undefined;
|
|
@@ -669,9 +1068,15 @@ async function resolveTopNav(store, siteConfig) {
|
|
|
669
1068
|
};
|
|
670
1069
|
}
|
|
671
1070
|
const directories = rootEntries.filter((entry) => entry.kind === 'directory');
|
|
1071
|
+
const localeDirectoryNames = new Set((siteConfig.locales ?? [])
|
|
1072
|
+
.filter((locale) => locale.contentBase !== '')
|
|
1073
|
+
.map((locale) => locale.contentBase));
|
|
672
1074
|
const navItems = [];
|
|
673
1075
|
const orderedNavItems = [];
|
|
674
1076
|
for (const entry of directories) {
|
|
1077
|
+
if (localeDirectoryNames.has(entry.name)) {
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
675
1080
|
const resolved = await resolveDirectoryNav(store, entry);
|
|
676
1081
|
if (resolved.type !== 'page') {
|
|
677
1082
|
continue;
|
|
@@ -731,6 +1136,9 @@ async function inspectDirectoryShape(store, directoryPath) {
|
|
|
731
1136
|
hasAssetFiles: false,
|
|
732
1137
|
};
|
|
733
1138
|
}
|
|
1139
|
+
return inspectDirectoryShapeEntries(entries);
|
|
1140
|
+
}
|
|
1141
|
+
function inspectDirectoryShapeEntries(entries) {
|
|
734
1142
|
let hasSkillIndex = false;
|
|
735
1143
|
let hasChildDirectories = false;
|
|
736
1144
|
let hasExtraMarkdownFiles = false;
|
package/dist/core/router.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ResolvedLocaleConfig } from './site-config.js';
|
|
1
2
|
export type ResolvedRequestKind = 'markdown' | 'html' | 'asset' | 'not-found';
|
|
2
3
|
export interface ResolvedRequest {
|
|
3
4
|
kind: ResolvedRequestKind;
|
|
@@ -6,3 +7,11 @@ export interface ResolvedRequest {
|
|
|
6
7
|
}
|
|
7
8
|
export declare function resolveRequest(pathname: string): ResolvedRequest;
|
|
8
9
|
export declare function normalizeRequestPath(pathname: string): string | null;
|
|
10
|
+
/**
|
|
11
|
+
* Attributes a request path to a configured content locale. Locale URL
|
|
12
|
+
* prefixes map 1:1 to top-level `{code}` content directories, so attribution
|
|
13
|
+
* never rewrites content resolution: a path that matches a locale prefix keeps
|
|
14
|
+
* resolving to that locale's directory, and any other path belongs to the
|
|
15
|
+
* default locale (whose content lives at the content root when unprefixed).
|
|
16
|
+
*/
|
|
17
|
+
export declare function matchRequestLocale(pathname: string, locales: readonly ResolvedLocaleConfig[] | undefined): ResolvedLocaleConfig | null;
|
package/dist/core/router.js
CHANGED
|
@@ -80,3 +80,29 @@ export function normalizeRequestPath(pathname) {
|
|
|
80
80
|
return null;
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Attributes a request path to a configured content locale. Locale URL
|
|
85
|
+
* prefixes map 1:1 to top-level `{code}` content directories, so attribution
|
|
86
|
+
* never rewrites content resolution: a path that matches a locale prefix keeps
|
|
87
|
+
* resolving to that locale's directory, and any other path belongs to the
|
|
88
|
+
* default locale (whose content lives at the content root when unprefixed).
|
|
89
|
+
*/
|
|
90
|
+
export function matchRequestLocale(pathname, locales) {
|
|
91
|
+
if (locales === undefined || locales.length === 0) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const normalized = normalizeRequestPath(pathname);
|
|
95
|
+
if (normalized === null) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const prefixed = locales
|
|
99
|
+
.filter((locale) => locale.pathPrefix !== '')
|
|
100
|
+
.sort((left, right) => right.pathPrefix.length - left.pathPrefix.length);
|
|
101
|
+
for (const locale of prefixed) {
|
|
102
|
+
if (normalized === locale.pathPrefix ||
|
|
103
|
+
normalized.startsWith(`${locale.pathPrefix}/`)) {
|
|
104
|
+
return locale;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return locales.find((locale) => locale.isDefault) ?? null;
|
|
108
|
+
}
|