mdorigin 0.1.0 → 0.1.2
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 +52 -3
- package/dist/adapters/cloudflare.d.ts +2 -0
- package/dist/adapters/cloudflare.js +17 -0
- package/dist/adapters/node.d.ts +2 -0
- package/dist/adapters/node.js +51 -11
- package/dist/cli/build-cloudflare.js +7 -1
- package/dist/cli/build-search.d.ts +1 -0
- package/dist/cli/build-search.js +44 -0
- package/dist/cli/dev.js +10 -1
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +15 -2
- package/dist/cli/search.d.ts +1 -0
- package/dist/cli/search.js +36 -0
- package/dist/cloudflare.d.ts +2 -0
- package/dist/cloudflare.js +41 -5
- package/dist/core/api.d.ts +13 -0
- package/dist/core/api.js +160 -0
- package/dist/core/content-store.js +5 -0
- package/dist/core/content-type.d.ts +1 -0
- package/dist/core/content-type.js +3 -0
- package/dist/core/directory-index.d.ts +1 -1
- package/dist/core/directory-index.js +5 -1
- package/dist/core/markdown.d.ts +12 -0
- package/dist/core/markdown.js +88 -0
- package/dist/core/request-handler.d.ts +5 -0
- package/dist/core/request-handler.js +412 -24
- package/dist/core/router.d.ts +1 -0
- package/dist/core/router.js +1 -1
- package/dist/core/site-config.d.ts +31 -0
- package/dist/core/site-config.js +98 -2
- package/dist/html/template-kind.d.ts +1 -1
- package/dist/html/template.d.ts +17 -1
- package/dist/html/template.js +282 -21
- package/dist/html/theme.js +542 -100
- package/dist/index-builder.js +62 -29
- package/dist/search.d.ts +59 -0
- package/dist/search.js +370 -0
- package/package.json +12 -3
|
@@ -1,22 +1,53 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { inferDirectoryContentType } from './content-type.js';
|
|
3
3
|
import { getDirectoryIndexCandidates } from './directory-index.js';
|
|
4
|
-
import { parseMarkdownDocument, stripManagedIndexBlock, stripManagedIndexLinks, } from './markdown.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { extractManagedIndexEntries, getDocumentSummary, getDocumentTitle as getParsedDocumentTitle, parseMarkdownDocument, stripManagedIndexBlock, stripManagedIndexLinks, } from './markdown.js';
|
|
5
|
+
import { handleApiRoute } from './api.js';
|
|
6
|
+
import { normalizeRequestPath, resolveRequest } from './router.js';
|
|
7
|
+
import { escapeHtml, renderCatalogArticleItems, renderDocument, } from '../html/template.js';
|
|
7
8
|
export async function handleSiteRequest(store, pathname, options) {
|
|
9
|
+
const searchEnabled = options.searchApi !== undefined;
|
|
10
|
+
const apiRoute = await handleApiRoute(pathname, options.searchParams, {
|
|
11
|
+
searchApi: options.searchApi,
|
|
12
|
+
siteConfig: options.siteConfig,
|
|
13
|
+
requestUrl: options.requestUrl,
|
|
14
|
+
});
|
|
15
|
+
if (apiRoute !== null) {
|
|
16
|
+
return apiRoute;
|
|
17
|
+
}
|
|
18
|
+
if (pathname === '/sitemap.xml') {
|
|
19
|
+
return renderSitemap(store, options);
|
|
20
|
+
}
|
|
8
21
|
const resolved = resolveRequest(pathname);
|
|
22
|
+
const catalogFragmentRequest = getCatalogFragmentRequest(options.searchParams);
|
|
23
|
+
const negotiatedMarkdown = shouldServeMarkdownForRequest(resolved, options.acceptHeader);
|
|
9
24
|
if (resolved.kind === 'not-found' || !resolved.sourcePath) {
|
|
25
|
+
const aliasRedirect = await tryRedirectAlias(store, pathname, options);
|
|
26
|
+
if (aliasRedirect !== null) {
|
|
27
|
+
return aliasRedirect;
|
|
28
|
+
}
|
|
10
29
|
return notFound();
|
|
11
30
|
}
|
|
12
31
|
const entry = await store.get(resolved.sourcePath);
|
|
13
32
|
if (entry === null) {
|
|
33
|
+
const aliasRedirect = await tryRedirectAlias(store, pathname, options);
|
|
34
|
+
if (aliasRedirect !== null) {
|
|
35
|
+
return aliasRedirect;
|
|
36
|
+
}
|
|
37
|
+
const alternateDirectoryMarkdown = await tryServeAlternateDirectoryMarkdown(store, resolved, options, negotiatedMarkdown);
|
|
38
|
+
if (alternateDirectoryMarkdown !== null) {
|
|
39
|
+
return alternateDirectoryMarkdown;
|
|
40
|
+
}
|
|
41
|
+
const alternateMarkdownRedirect = await tryRedirectAlternateDirectoryMarkdown(store, resolved, options);
|
|
42
|
+
if (alternateMarkdownRedirect !== null) {
|
|
43
|
+
return alternateMarkdownRedirect;
|
|
44
|
+
}
|
|
14
45
|
if (resolved.kind === 'html' && resolved.requestPath.endsWith('/')) {
|
|
15
46
|
const directoryIndexResponse = await tryRenderAlternateDirectoryIndex(store, resolved.requestPath, options);
|
|
16
47
|
if (directoryIndexResponse !== null) {
|
|
17
48
|
return directoryIndexResponse;
|
|
18
49
|
}
|
|
19
|
-
return renderDirectoryListing(store, resolved.requestPath, options.siteConfig);
|
|
50
|
+
return renderDirectoryListing(store, resolved.requestPath, options.siteConfig, searchEnabled);
|
|
20
51
|
}
|
|
21
52
|
return notFound();
|
|
22
53
|
}
|
|
@@ -26,16 +57,16 @@ export async function handleSiteRequest(store, pathname, options) {
|
|
|
26
57
|
if (entry.kind !== 'text' || entry.text === undefined) {
|
|
27
58
|
return notFound();
|
|
28
59
|
}
|
|
29
|
-
if (resolved.kind === 'markdown') {
|
|
60
|
+
if (resolved.kind === 'markdown' || negotiatedMarkdown) {
|
|
30
61
|
const parsed = await parseMarkdownDocument(resolved.sourcePath, entry.text);
|
|
31
62
|
if (parsed.meta.draft === true && options.draftMode === 'exclude') {
|
|
32
63
|
return notFound();
|
|
33
64
|
}
|
|
34
65
|
return {
|
|
35
66
|
status: 200,
|
|
36
|
-
headers: {
|
|
67
|
+
headers: withVaryAcceptIfNeeded({
|
|
37
68
|
'content-type': entry.mediaType,
|
|
38
|
-
},
|
|
69
|
+
}, negotiatedMarkdown),
|
|
39
70
|
body: entry.text,
|
|
40
71
|
};
|
|
41
72
|
}
|
|
@@ -49,27 +80,94 @@ export async function handleSiteRequest(store, pathname, options) {
|
|
|
49
80
|
: isRootHomeRequest(resolved.requestPath) && navigation.items.length > 0
|
|
50
81
|
? stripManagedIndexLinks(entry.text, new Set(navigation.items.map((item) => item.href)))
|
|
51
82
|
: entry.text;
|
|
52
|
-
const
|
|
83
|
+
const catalogEntries = options.siteConfig.template === 'catalog'
|
|
84
|
+
? extractManagedIndexEntries(renderedBody)
|
|
85
|
+
: [];
|
|
86
|
+
if (catalogFragmentRequest !== null &&
|
|
87
|
+
options.siteConfig.template === 'catalog') {
|
|
88
|
+
return renderCatalogPostsFragment(catalogEntries, catalogFragmentRequest);
|
|
89
|
+
}
|
|
90
|
+
const documentBody = options.siteConfig.template === 'catalog'
|
|
91
|
+
? stripManagedIndexBlock(renderedBody)
|
|
92
|
+
: renderedBody;
|
|
93
|
+
const renderedParsed = documentBody === entry.text
|
|
53
94
|
? parsed
|
|
54
|
-
: await parseMarkdownDocument(resolved.sourcePath,
|
|
95
|
+
: await parseMarkdownDocument(resolved.sourcePath, documentBody);
|
|
55
96
|
return {
|
|
56
97
|
status: 200,
|
|
57
|
-
headers: {
|
|
98
|
+
headers: withVaryAcceptIfNeeded({
|
|
58
99
|
'content-type': 'text/html; charset=utf-8',
|
|
59
|
-
},
|
|
100
|
+
}, shouldVaryOnAccept(resolved)),
|
|
60
101
|
body: renderDocument({
|
|
61
102
|
siteTitle: options.siteConfig.siteTitle,
|
|
62
103
|
siteDescription: options.siteConfig.siteDescription,
|
|
104
|
+
siteUrl: options.siteConfig.siteUrl,
|
|
105
|
+
favicon: options.siteConfig.favicon,
|
|
106
|
+
logo: options.siteConfig.logo,
|
|
63
107
|
title: getDocumentTitle(parsed),
|
|
64
108
|
body: renderedParsed.html,
|
|
65
|
-
summary: options.siteConfig.showSummary === false
|
|
109
|
+
summary: options.siteConfig.showSummary === false
|
|
110
|
+
? undefined
|
|
111
|
+
: getDocumentSummary(parsed.meta, parsed.body),
|
|
66
112
|
date: options.siteConfig.showDate === false ? undefined : parsed.meta.date,
|
|
67
113
|
showSummary: options.siteConfig.showSummary,
|
|
68
114
|
showDate: options.siteConfig.showDate,
|
|
69
115
|
theme: options.siteConfig.theme,
|
|
70
116
|
template: options.siteConfig.template,
|
|
71
117
|
topNav: navigation.items,
|
|
118
|
+
footerNav: options.siteConfig.footerNav,
|
|
119
|
+
footerText: options.siteConfig.footerText,
|
|
120
|
+
socialLinks: options.siteConfig.socialLinks,
|
|
121
|
+
editLinkHref: getEditLinkHref(options.siteConfig, resolved.sourcePath),
|
|
72
122
|
stylesheetContent: options.siteConfig.stylesheetContent,
|
|
123
|
+
canonicalPath: getCanonicalHtmlPathForContentPath(resolved.sourcePath),
|
|
124
|
+
alternateMarkdownPath: getMarkdownRequestPathForContentPath(resolved.sourcePath),
|
|
125
|
+
catalogEntries,
|
|
126
|
+
catalogRequestPath: resolved.requestPath,
|
|
127
|
+
catalogInitialPostCount: options.siteConfig.catalogInitialPostCount,
|
|
128
|
+
catalogLoadMoreStep: options.siteConfig.catalogLoadMoreStep,
|
|
129
|
+
searchEnabled,
|
|
130
|
+
}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function getCatalogFragmentRequest(searchParams) {
|
|
134
|
+
if (searchParams?.get('catalog-format') !== 'posts') {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const offset = normalizeNonNegativeInteger(searchParams.get('catalog-offset'));
|
|
138
|
+
const limit = normalizePositiveInteger(searchParams.get('catalog-limit'));
|
|
139
|
+
if (offset === null || limit === null) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return { offset, limit };
|
|
143
|
+
}
|
|
144
|
+
function normalizeNonNegativeInteger(value) {
|
|
145
|
+
if (value === null) {
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
const parsed = Number.parseInt(value, 10);
|
|
149
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : null;
|
|
150
|
+
}
|
|
151
|
+
function normalizePositiveInteger(value) {
|
|
152
|
+
if (value === null) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
const parsed = Number.parseInt(value, 10);
|
|
156
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
157
|
+
}
|
|
158
|
+
function renderCatalogPostsFragment(entries, request) {
|
|
159
|
+
const articles = entries.filter((entry) => entry.kind === 'article');
|
|
160
|
+
const visibleArticles = articles.slice(request.offset, request.offset + request.limit);
|
|
161
|
+
const nextOffset = request.offset + visibleArticles.length;
|
|
162
|
+
return {
|
|
163
|
+
status: 200,
|
|
164
|
+
headers: {
|
|
165
|
+
'content-type': 'application/json; charset=utf-8',
|
|
166
|
+
},
|
|
167
|
+
body: JSON.stringify({
|
|
168
|
+
itemsHtml: renderCatalogArticleItems(visibleArticles),
|
|
169
|
+
hasMore: nextOffset < articles.length,
|
|
170
|
+
nextOffset,
|
|
73
171
|
}),
|
|
74
172
|
};
|
|
75
173
|
}
|
|
@@ -103,16 +201,113 @@ function notFound() {
|
|
|
103
201
|
body: 'Not Found',
|
|
104
202
|
};
|
|
105
203
|
}
|
|
106
|
-
function
|
|
107
|
-
|
|
108
|
-
|
|
204
|
+
function redirect(location) {
|
|
205
|
+
return {
|
|
206
|
+
status: 308,
|
|
207
|
+
headers: {
|
|
208
|
+
location,
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
async function renderSitemap(store, options) {
|
|
213
|
+
if (!options.siteConfig.siteUrl) {
|
|
214
|
+
return {
|
|
215
|
+
status: 500,
|
|
216
|
+
headers: {
|
|
217
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
218
|
+
},
|
|
219
|
+
body: 'sitemap.xml requires siteUrl in mdorigin.config.json',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const entries = await collectSitemapEntries(store, '', options);
|
|
223
|
+
const body = [
|
|
224
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
225
|
+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
226
|
+
...entries.map((entry) => {
|
|
227
|
+
const lastmod = entry.lastmod ? `<lastmod>${escapeHtml(entry.lastmod)}</lastmod>` : '';
|
|
228
|
+
return ` <url><loc>${escapeHtml(`${options.siteConfig.siteUrl}${entry.path}`)}</loc>${lastmod}</url>`;
|
|
229
|
+
}),
|
|
230
|
+
'</urlset>',
|
|
231
|
+
].join('\n');
|
|
232
|
+
return {
|
|
233
|
+
status: 200,
|
|
234
|
+
headers: {
|
|
235
|
+
'content-type': 'application/xml; charset=utf-8',
|
|
236
|
+
},
|
|
237
|
+
body,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function withVaryAcceptIfNeeded(headers, enabled) {
|
|
241
|
+
if (!enabled) {
|
|
242
|
+
return headers;
|
|
109
243
|
}
|
|
244
|
+
return {
|
|
245
|
+
...headers,
|
|
246
|
+
vary: appendVary(headers.vary, 'Accept'),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function appendVary(existing, value) {
|
|
250
|
+
if (!existing || existing.trim() === '') {
|
|
251
|
+
return value;
|
|
252
|
+
}
|
|
253
|
+
const parts = existing.split(',').map((part) => part.trim().toLowerCase());
|
|
254
|
+
if (parts.includes(value.toLowerCase())) {
|
|
255
|
+
return existing;
|
|
256
|
+
}
|
|
257
|
+
return `${existing}, ${value}`;
|
|
258
|
+
}
|
|
259
|
+
function getDocumentTitle(parsed) {
|
|
110
260
|
const basename = path.posix.basename(parsed.sourcePath, '.md');
|
|
111
|
-
|
|
261
|
+
const fallback = basename === 'index' || basename === 'README' || basename === 'SKILL'
|
|
112
262
|
? path.posix.basename(path.posix.dirname(parsed.sourcePath)) || 'mdorigin'
|
|
113
263
|
: basename;
|
|
264
|
+
return getParsedDocumentTitle(parsed.meta, parsed.body, fallback);
|
|
265
|
+
}
|
|
266
|
+
async function collectSitemapEntries(store, directoryPath, options) {
|
|
267
|
+
const entries = await store.listDirectory(directoryPath);
|
|
268
|
+
if (entries === null) {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
const sitemapEntries = [];
|
|
272
|
+
for (const entry of entries) {
|
|
273
|
+
if (entry.kind === 'directory') {
|
|
274
|
+
sitemapEntries.push(...(await collectSitemapEntries(store, entry.path, options)));
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (!isMarkdownEntry(entry)) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const document = await store.get(entry.path);
|
|
281
|
+
if (document === null || document.kind !== 'text' || document.text === undefined) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const parsed = await parseMarkdownDocument(entry.path, document.text);
|
|
285
|
+
if (parsed.meta.draft === true && options.draftMode === 'exclude') {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
sitemapEntries.push({
|
|
289
|
+
path: getCanonicalHtmlPathForContentPath(entry.path),
|
|
290
|
+
lastmod: parsed.meta.date,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
sitemapEntries.sort((left, right) => left.path.localeCompare(right.path));
|
|
294
|
+
return dedupeSitemapEntries(sitemapEntries);
|
|
114
295
|
}
|
|
115
|
-
|
|
296
|
+
function dedupeSitemapEntries(entries) {
|
|
297
|
+
const deduped = new Map();
|
|
298
|
+
for (const entry of entries) {
|
|
299
|
+
const existing = deduped.get(entry.path);
|
|
300
|
+
if (!existing) {
|
|
301
|
+
deduped.set(entry.path, entry);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (!existing.lastmod && entry.lastmod) {
|
|
305
|
+
deduped.set(entry.path, entry);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return Array.from(deduped.values());
|
|
309
|
+
}
|
|
310
|
+
async function renderDirectoryListing(store, requestPath, siteConfig, searchEnabled) {
|
|
116
311
|
const directoryPath = requestPath === '/' ? '' : requestPath.slice(1).replace(/\/$/, '');
|
|
117
312
|
const entries = await store.listDirectory(directoryPath);
|
|
118
313
|
if (entries === null) {
|
|
@@ -135,6 +330,9 @@ async function renderDirectoryListing(store, requestPath, siteConfig) {
|
|
|
135
330
|
body: renderDocument({
|
|
136
331
|
siteTitle: siteConfig.siteTitle,
|
|
137
332
|
siteDescription: siteConfig.siteDescription,
|
|
333
|
+
siteUrl: siteConfig.siteUrl,
|
|
334
|
+
favicon: siteConfig.favicon,
|
|
335
|
+
logo: siteConfig.logo,
|
|
138
336
|
title: getDirectoryTitle(requestPath),
|
|
139
337
|
body,
|
|
140
338
|
showSummary: false,
|
|
@@ -142,7 +340,13 @@ async function renderDirectoryListing(store, requestPath, siteConfig) {
|
|
|
142
340
|
theme: siteConfig.theme,
|
|
143
341
|
template: siteConfig.template,
|
|
144
342
|
topNav: navigation.items,
|
|
343
|
+
footerNav: siteConfig.footerNav,
|
|
344
|
+
footerText: siteConfig.footerText,
|
|
345
|
+
socialLinks: siteConfig.socialLinks,
|
|
145
346
|
stylesheetContent: siteConfig.stylesheetContent,
|
|
347
|
+
canonicalPath: requestPath,
|
|
348
|
+
alternateMarkdownPath: getMarkdownRequestPathForContentPath(getDirectoryIndexContentPathForRequestPath(requestPath)),
|
|
349
|
+
searchEnabled,
|
|
146
350
|
}),
|
|
147
351
|
};
|
|
148
352
|
}
|
|
@@ -167,6 +371,11 @@ function getDirectoryEntryLabel(entry) {
|
|
|
167
371
|
function getDirectoryTitle(requestPath) {
|
|
168
372
|
return requestPath === '/' ? 'Index' : requestPath;
|
|
169
373
|
}
|
|
374
|
+
function getDirectoryIndexContentPathForRequestPath(requestPath) {
|
|
375
|
+
return requestPath === '/'
|
|
376
|
+
? 'index.md'
|
|
377
|
+
: `${requestPath.slice(1).replace(/\/$/, '')}/index.md`;
|
|
378
|
+
}
|
|
170
379
|
async function tryRenderAlternateDirectoryIndex(store, requestPath, options) {
|
|
171
380
|
const directoryPath = requestPath === '/' ? '' : requestPath.slice(1).replace(/\/$/, '');
|
|
172
381
|
for (const candidatePath of getDirectoryIndexCandidates(directoryPath)) {
|
|
@@ -187,9 +396,20 @@ async function tryRenderAlternateDirectoryIndex(store, requestPath, options) {
|
|
|
187
396
|
: isRootHomeRequest(requestPath) && navigation.items.length > 0
|
|
188
397
|
? stripManagedIndexLinks(entry.text, new Set(navigation.items.map((item) => item.href)))
|
|
189
398
|
: entry.text;
|
|
190
|
-
const
|
|
399
|
+
const catalogEntries = options.siteConfig.template === 'catalog'
|
|
400
|
+
? extractManagedIndexEntries(renderedBody)
|
|
401
|
+
: [];
|
|
402
|
+
const catalogFragmentRequest = getCatalogFragmentRequest(options.searchParams);
|
|
403
|
+
if (catalogFragmentRequest !== null &&
|
|
404
|
+
options.siteConfig.template === 'catalog') {
|
|
405
|
+
return renderCatalogPostsFragment(catalogEntries, catalogFragmentRequest);
|
|
406
|
+
}
|
|
407
|
+
const documentBody = options.siteConfig.template === 'catalog'
|
|
408
|
+
? stripManagedIndexBlock(renderedBody)
|
|
409
|
+
: renderedBody;
|
|
410
|
+
const renderedParsed = documentBody === entry.text
|
|
191
411
|
? parsed
|
|
192
|
-
: await parseMarkdownDocument(candidatePath,
|
|
412
|
+
: await parseMarkdownDocument(candidatePath, documentBody);
|
|
193
413
|
return {
|
|
194
414
|
status: 200,
|
|
195
415
|
headers: {
|
|
@@ -198,24 +418,188 @@ async function tryRenderAlternateDirectoryIndex(store, requestPath, options) {
|
|
|
198
418
|
body: renderDocument({
|
|
199
419
|
siteTitle: options.siteConfig.siteTitle,
|
|
200
420
|
siteDescription: options.siteConfig.siteDescription,
|
|
421
|
+
siteUrl: options.siteConfig.siteUrl,
|
|
422
|
+
favicon: options.siteConfig.favicon,
|
|
423
|
+
logo: options.siteConfig.logo,
|
|
201
424
|
title: getDocumentTitle(parsed),
|
|
202
425
|
body: renderedParsed.html,
|
|
203
|
-
summary: options.siteConfig.showSummary === false
|
|
426
|
+
summary: options.siteConfig.showSummary === false
|
|
427
|
+
? undefined
|
|
428
|
+
: getDocumentSummary(parsed.meta, parsed.body),
|
|
204
429
|
date: options.siteConfig.showDate === false ? undefined : parsed.meta.date,
|
|
205
430
|
showSummary: options.siteConfig.showSummary,
|
|
206
431
|
showDate: options.siteConfig.showDate,
|
|
207
432
|
theme: options.siteConfig.theme,
|
|
208
433
|
template: options.siteConfig.template,
|
|
209
434
|
topNav: navigation.items,
|
|
435
|
+
footerNav: options.siteConfig.footerNav,
|
|
436
|
+
footerText: options.siteConfig.footerText,
|
|
437
|
+
socialLinks: options.siteConfig.socialLinks,
|
|
438
|
+
editLinkHref: getEditLinkHref(options.siteConfig, candidatePath),
|
|
210
439
|
stylesheetContent: options.siteConfig.stylesheetContent,
|
|
440
|
+
canonicalPath: requestPath,
|
|
441
|
+
alternateMarkdownPath: getMarkdownRequestPathForContentPath(candidatePath),
|
|
442
|
+
catalogEntries,
|
|
443
|
+
catalogRequestPath: requestPath,
|
|
444
|
+
catalogInitialPostCount: options.siteConfig.catalogInitialPostCount,
|
|
445
|
+
catalogLoadMoreStep: options.siteConfig.catalogLoadMoreStep,
|
|
446
|
+
searchEnabled: options.searchApi !== undefined,
|
|
211
447
|
}),
|
|
212
448
|
};
|
|
213
449
|
}
|
|
214
450
|
return null;
|
|
215
451
|
}
|
|
452
|
+
async function tryServeAlternateDirectoryMarkdown(store, resolved, options, negotiatedMarkdown) {
|
|
453
|
+
if (!negotiatedMarkdown || resolved.kind !== 'html' || !resolved.sourcePath) {
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
if (!resolved.requestPath.endsWith('/')) {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
const directoryPath = path.posix.dirname(resolved.sourcePath);
|
|
460
|
+
for (const candidatePath of getDirectoryIndexCandidates(directoryPath === '.' ? '' : directoryPath)) {
|
|
461
|
+
if (candidatePath === resolved.sourcePath) {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
const entry = await store.get(candidatePath);
|
|
465
|
+
if (entry === null || entry.kind !== 'text' || entry.text === undefined) {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
const parsed = await parseMarkdownDocument(candidatePath, entry.text);
|
|
469
|
+
if (parsed.meta.draft === true && options.draftMode === 'exclude') {
|
|
470
|
+
return notFound();
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
status: 200,
|
|
474
|
+
headers: withVaryAcceptIfNeeded({
|
|
475
|
+
'content-type': entry.mediaType,
|
|
476
|
+
}, true),
|
|
477
|
+
body: entry.text,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
async function tryRedirectAlternateDirectoryMarkdown(store, resolved, options) {
|
|
483
|
+
if (resolved.kind !== 'markdown' || !resolved.sourcePath) {
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
const basename = path.posix.basename(resolved.sourcePath);
|
|
487
|
+
if (basename !== 'index.md' && basename !== 'README.md') {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
const directoryPath = path.posix.dirname(resolved.sourcePath);
|
|
491
|
+
for (const candidatePath of getDirectoryIndexCandidates(directoryPath === '.' ? '' : directoryPath)) {
|
|
492
|
+
if (candidatePath === resolved.sourcePath) {
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
const entry = await store.get(candidatePath);
|
|
496
|
+
if (entry === null || entry.kind !== 'text' || entry.text === undefined) {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
const parsed = await parseMarkdownDocument(candidatePath, entry.text);
|
|
500
|
+
if (parsed.meta.draft === true && options.draftMode === 'exclude') {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
return redirect(getMarkdownRequestPathForContentPath(candidatePath));
|
|
504
|
+
}
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
function getMarkdownRequestPathForContentPath(contentPath) {
|
|
508
|
+
return `/${contentPath}`;
|
|
509
|
+
}
|
|
216
510
|
function isRootHomeRequest(requestPath) {
|
|
217
511
|
return requestPath === '/';
|
|
218
512
|
}
|
|
513
|
+
function shouldServeMarkdownForRequest(resolved, acceptHeader) {
|
|
514
|
+
return shouldVaryOnAccept(resolved) && acceptsMarkdown(acceptHeader);
|
|
515
|
+
}
|
|
516
|
+
function shouldVaryOnAccept(resolved) {
|
|
517
|
+
if (resolved.kind !== 'html') {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
return !resolved.requestPath.endsWith('.html');
|
|
521
|
+
}
|
|
522
|
+
function acceptsMarkdown(acceptHeader) {
|
|
523
|
+
if (!acceptHeader) {
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
526
|
+
return acceptHeader
|
|
527
|
+
.split(',')
|
|
528
|
+
.map((part) => part.split(';', 1)[0]?.trim().toLowerCase())
|
|
529
|
+
.includes('text/markdown');
|
|
530
|
+
}
|
|
531
|
+
async function tryRedirectAlias(store, pathname, options) {
|
|
532
|
+
const normalizedRequestPath = normalizeRequestPath(pathname);
|
|
533
|
+
if (normalizedRequestPath === null) {
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
const redirectLocation = await findAliasRedirectLocation(store, '', normalizedRequestPath, options);
|
|
537
|
+
if (!redirectLocation || redirectLocation === normalizedRequestPath) {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
return redirect(redirectLocation);
|
|
541
|
+
}
|
|
542
|
+
async function findAliasRedirectLocation(store, directoryPath, requestPath, options) {
|
|
543
|
+
const entries = await store.listDirectory(directoryPath);
|
|
544
|
+
if (entries === null) {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
for (const entry of entries) {
|
|
548
|
+
if (entry.kind === 'directory') {
|
|
549
|
+
const nestedMatch = await findAliasRedirectLocation(store, entry.path, requestPath, options);
|
|
550
|
+
if (nestedMatch !== null) {
|
|
551
|
+
return nestedMatch;
|
|
552
|
+
}
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
if (!isMarkdownEntry(entry)) {
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
const document = await store.get(entry.path);
|
|
559
|
+
if (document === null || document.kind !== 'text' || document.text === undefined) {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
const parsed = await parseMarkdownDocument(entry.path, document.text);
|
|
563
|
+
if (parsed.meta.draft === true && options.draftMode === 'exclude') {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
const aliases = normalizeAliases(parsed.meta.aliases);
|
|
567
|
+
if (!aliases.includes(requestPath)) {
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
return getCanonicalHtmlPathForContentPath(entry.path);
|
|
571
|
+
}
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
function isMarkdownEntry(entry) {
|
|
575
|
+
return path.posix.extname(entry.name).toLowerCase() === '.md';
|
|
576
|
+
}
|
|
577
|
+
function normalizeAliases(aliases) {
|
|
578
|
+
if (!Array.isArray(aliases)) {
|
|
579
|
+
return [];
|
|
580
|
+
}
|
|
581
|
+
return aliases.flatMap((alias) => {
|
|
582
|
+
if (typeof alias !== 'string') {
|
|
583
|
+
return [];
|
|
584
|
+
}
|
|
585
|
+
const normalized = normalizeRequestPath(alias);
|
|
586
|
+
return normalized === null ? [] : [normalized];
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
function getCanonicalHtmlPathForContentPath(contentPath) {
|
|
590
|
+
const basename = path.posix.basename(contentPath).toLowerCase();
|
|
591
|
+
if (basename === 'index.md' || basename === 'readme.md') {
|
|
592
|
+
const directory = path.posix.dirname(contentPath);
|
|
593
|
+
return directory === '.' ? '/' : `/${directory}/`;
|
|
594
|
+
}
|
|
595
|
+
return `/${contentPath.slice(0, -'.md'.length)}`;
|
|
596
|
+
}
|
|
597
|
+
function getEditLinkHref(siteConfig, sourcePath) {
|
|
598
|
+
if (!siteConfig.editLink || !sourcePath) {
|
|
599
|
+
return undefined;
|
|
600
|
+
}
|
|
601
|
+
return `${siteConfig.editLink.baseUrl}${sourcePath}`;
|
|
602
|
+
}
|
|
219
603
|
async function resolveTopNav(store, siteConfig) {
|
|
220
604
|
if (siteConfig.topNav.length > 0) {
|
|
221
605
|
return {
|
|
@@ -273,9 +657,7 @@ async function resolveDirectoryNav(store, entry) {
|
|
|
273
657
|
const parsed = await parseMarkdownDocument(candidatePath, contentEntry.text);
|
|
274
658
|
const shape = await inspectDirectoryShape(store, entry.path);
|
|
275
659
|
return {
|
|
276
|
-
title:
|
|
277
|
-
? parsed.meta.title
|
|
278
|
-
: entry.name,
|
|
660
|
+
title: getParsedDocumentTitle(parsed.meta, parsed.body, entry.name),
|
|
279
661
|
type: inferDirectoryContentType(parsed.meta, shape),
|
|
280
662
|
order: parsed.meta.order,
|
|
281
663
|
};
|
|
@@ -289,11 +671,13 @@ async function inspectDirectoryShape(store, directoryPath) {
|
|
|
289
671
|
const entries = await store.listDirectory(directoryPath);
|
|
290
672
|
if (entries === null) {
|
|
291
673
|
return {
|
|
674
|
+
hasSkillIndex: false,
|
|
292
675
|
hasChildDirectories: false,
|
|
293
676
|
hasExtraMarkdownFiles: false,
|
|
294
677
|
hasAssetFiles: false,
|
|
295
678
|
};
|
|
296
679
|
}
|
|
680
|
+
let hasSkillIndex = false;
|
|
297
681
|
let hasChildDirectories = false;
|
|
298
682
|
let hasExtraMarkdownFiles = false;
|
|
299
683
|
let hasAssetFiles = false;
|
|
@@ -307,7 +691,10 @@ async function inspectDirectoryShape(store, directoryPath) {
|
|
|
307
691
|
}
|
|
308
692
|
const extension = path.posix.extname(entry.name).toLowerCase();
|
|
309
693
|
if (extension === '.md') {
|
|
310
|
-
if (entry.name
|
|
694
|
+
if (entry.name === 'SKILL.md') {
|
|
695
|
+
hasSkillIndex = true;
|
|
696
|
+
}
|
|
697
|
+
else if (entry.name !== 'index.md' && entry.name !== 'README.md') {
|
|
311
698
|
hasExtraMarkdownFiles = true;
|
|
312
699
|
}
|
|
313
700
|
continue;
|
|
@@ -315,6 +702,7 @@ async function inspectDirectoryShape(store, directoryPath) {
|
|
|
315
702
|
hasAssetFiles = true;
|
|
316
703
|
}
|
|
317
704
|
return {
|
|
705
|
+
hasSkillIndex,
|
|
318
706
|
hasChildDirectories,
|
|
319
707
|
hasExtraMarkdownFiles,
|
|
320
708
|
hasAssetFiles,
|
package/dist/core/router.d.ts
CHANGED
package/dist/core/router.js
CHANGED
|
@@ -63,7 +63,7 @@ export function resolveRequest(pathname) {
|
|
|
63
63
|
sourcePath,
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
|
-
function normalizeRequestPath(pathname) {
|
|
66
|
+
export function normalizeRequestPath(pathname) {
|
|
67
67
|
try {
|
|
68
68
|
const decoded = decodeURIComponent(pathname || '/');
|
|
69
69
|
const collapsed = decoded.replace(/\/{2,}/g, '/');
|
|
@@ -5,26 +5,57 @@ export interface SiteNavItem {
|
|
|
5
5
|
label: string;
|
|
6
6
|
href: string;
|
|
7
7
|
}
|
|
8
|
+
export interface SiteLogo {
|
|
9
|
+
src: string;
|
|
10
|
+
alt?: string;
|
|
11
|
+
href?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface SiteSocialLink {
|
|
14
|
+
icon: string;
|
|
15
|
+
label: string;
|
|
16
|
+
href: string;
|
|
17
|
+
}
|
|
18
|
+
export interface EditLinkConfig {
|
|
19
|
+
baseUrl: string;
|
|
20
|
+
}
|
|
8
21
|
export interface SiteConfig {
|
|
9
22
|
siteTitle?: string;
|
|
10
23
|
siteDescription?: string;
|
|
24
|
+
siteUrl?: string;
|
|
25
|
+
favicon?: string;
|
|
26
|
+
logo?: SiteLogo;
|
|
11
27
|
showDate?: boolean;
|
|
12
28
|
showSummary?: boolean;
|
|
13
29
|
stylesheet?: string;
|
|
14
30
|
theme?: BuiltInThemeName;
|
|
15
31
|
template?: TemplateName;
|
|
16
32
|
topNav?: SiteNavItem[];
|
|
33
|
+
footerNav?: SiteNavItem[];
|
|
34
|
+
footerText?: string;
|
|
35
|
+
socialLinks?: SiteSocialLink[];
|
|
36
|
+
editLink?: EditLinkConfig;
|
|
17
37
|
showHomeIndex?: boolean;
|
|
38
|
+
catalogInitialPostCount?: number;
|
|
39
|
+
catalogLoadMoreStep?: number;
|
|
18
40
|
}
|
|
19
41
|
export interface ResolvedSiteConfig {
|
|
20
42
|
siteTitle: string;
|
|
21
43
|
siteDescription?: string;
|
|
44
|
+
siteUrl?: string;
|
|
45
|
+
favicon?: string;
|
|
46
|
+
logo?: SiteLogo;
|
|
22
47
|
showDate: boolean;
|
|
23
48
|
showSummary: boolean;
|
|
24
49
|
theme: BuiltInThemeName;
|
|
25
50
|
template: TemplateName;
|
|
26
51
|
topNav: SiteNavItem[];
|
|
52
|
+
footerNav: SiteNavItem[];
|
|
53
|
+
footerText?: string;
|
|
54
|
+
socialLinks: SiteSocialLink[];
|
|
55
|
+
editLink?: EditLinkConfig;
|
|
27
56
|
showHomeIndex: boolean;
|
|
57
|
+
catalogInitialPostCount: number;
|
|
58
|
+
catalogLoadMoreStep: number;
|
|
28
59
|
stylesheetContent?: string;
|
|
29
60
|
siteTitleConfigured: boolean;
|
|
30
61
|
siteDescriptionConfigured: boolean;
|