mdorigin 0.1.0 → 0.1.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 +9 -3
- package/dist/adapters/cloudflare.js +8 -0
- package/dist/adapters/node.js +1 -0
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +1 -0
- package/dist/core/markdown.d.ts +8 -0
- package/dist/core/markdown.js +35 -0
- package/dist/core/request-handler.d.ts +1 -0
- package/dist/core/request-handler.js +323 -11
- package/dist/core/router.d.ts +1 -0
- package/dist/core/router.js +1 -1
- package/dist/core/site-config.d.ts +27 -0
- package/dist/core/site-config.js +84 -2
- package/dist/html/template-kind.d.ts +1 -1
- package/dist/html/template.d.ts +12 -1
- package/dist/html/template.js +90 -21
- package/dist/html/theme.js +288 -91
- package/dist/index-builder.js +8 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,15 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
`mdorigin` is a markdown-first publishing engine.
|
|
4
4
|
|
|
5
|
-
It treats markdown as the only source of truth, serves raw `.md` directly for agents,
|
|
5
|
+
It treats markdown as the only source of truth, serves raw `.md` directly for agents, renders `.html` views for humans from the same directory tree, can return markdown from extensionless routes when clients send `Accept: text/markdown`, and supports frontmatter aliases for old URL redirects.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install mdorigin
|
|
10
|
+
npm install --save-dev mdorigin
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Then run it with `npx`:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx mdorigin dev --root docs/site
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Repo Development
|
|
14
20
|
|
|
15
21
|
```bash
|
|
16
22
|
npm install
|
|
@@ -24,15 +24,23 @@ export function createCloudflareWorker(manifest) {
|
|
|
24
24
|
draftMode: 'exclude',
|
|
25
25
|
siteConfig: manifest.siteConfig ?? {
|
|
26
26
|
siteTitle: 'mdorigin',
|
|
27
|
+
siteUrl: undefined,
|
|
28
|
+
favicon: undefined,
|
|
29
|
+
logo: undefined,
|
|
27
30
|
showDate: true,
|
|
28
31
|
showSummary: true,
|
|
29
32
|
theme: 'paper',
|
|
30
33
|
template: 'document',
|
|
31
34
|
topNav: [],
|
|
35
|
+
footerNav: [],
|
|
36
|
+
footerText: undefined,
|
|
37
|
+
socialLinks: [],
|
|
38
|
+
editLink: undefined,
|
|
32
39
|
showHomeIndex: true,
|
|
33
40
|
siteTitleConfigured: false,
|
|
34
41
|
siteDescriptionConfigured: false,
|
|
35
42
|
},
|
|
43
|
+
acceptHeader: request.headers.get('accept') ?? undefined,
|
|
36
44
|
});
|
|
37
45
|
const headers = new Headers(siteResponse.headers);
|
|
38
46
|
const body = siteResponse.body instanceof Uint8Array
|
package/dist/adapters/node.js
CHANGED
|
@@ -86,6 +86,7 @@ export function createNodeRequestListener(options) {
|
|
|
86
86
|
const siteResponse = await handleSiteRequest(store, url.pathname, {
|
|
87
87
|
draftMode: options.draftMode,
|
|
88
88
|
siteConfig: options.siteConfig,
|
|
89
|
+
acceptHeader: request.headers.accept,
|
|
89
90
|
});
|
|
90
91
|
response.statusCode = siteResponse.status;
|
|
91
92
|
for (const [headerName, headerValue] of Object.entries(siteResponse.headers)) {
|
package/dist/cli/main.d.ts
CHANGED
package/dist/cli/main.js
CHANGED
package/dist/core/markdown.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface ParsedDocumentMeta {
|
|
|
5
5
|
draft?: boolean;
|
|
6
6
|
type?: string;
|
|
7
7
|
order?: number;
|
|
8
|
+
aliases?: string[];
|
|
8
9
|
[key: string]: unknown;
|
|
9
10
|
}
|
|
10
11
|
export interface ParsedDocument {
|
|
@@ -13,8 +14,15 @@ export interface ParsedDocument {
|
|
|
13
14
|
html: string;
|
|
14
15
|
meta: ParsedDocumentMeta;
|
|
15
16
|
}
|
|
17
|
+
export interface ManagedIndexEntry {
|
|
18
|
+
kind: 'directory' | 'article';
|
|
19
|
+
title: string;
|
|
20
|
+
href: string;
|
|
21
|
+
detail?: string;
|
|
22
|
+
}
|
|
16
23
|
export declare function parseMarkdownDocument(sourcePath: string, markdown: string): Promise<ParsedDocument>;
|
|
17
24
|
export declare function renderMarkdown(markdown: string): Promise<string>;
|
|
18
25
|
export declare function rewriteMarkdownLinksInHtml(html: string): string;
|
|
19
26
|
export declare function stripManagedIndexBlock(markdown: string): string;
|
|
20
27
|
export declare function stripManagedIndexLinks(markdown: string, hrefs: ReadonlySet<string>): string;
|
|
28
|
+
export declare function extractManagedIndexEntries(markdown: string): ManagedIndexEntry[];
|
package/dist/core/markdown.js
CHANGED
|
@@ -49,6 +49,35 @@ export function stripManagedIndexLinks(markdown, hrefs) {
|
|
|
49
49
|
return `${start}${keptBlocks.join('\n\n')}\n${end}`;
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
|
+
export function extractManagedIndexEntries(markdown) {
|
|
53
|
+
const match = markdown.match(/<!-- INDEX:START -->\n?([\s\S]*?)\n?<!-- INDEX:END -->/);
|
|
54
|
+
if (!match) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
const blocks = match[1]
|
|
58
|
+
.trim()
|
|
59
|
+
.split(/\n\s*\n/g)
|
|
60
|
+
.map((block) => block.trim())
|
|
61
|
+
.filter((block) => block !== '');
|
|
62
|
+
const entries = [];
|
|
63
|
+
for (const block of blocks) {
|
|
64
|
+
const lines = block.split('\n');
|
|
65
|
+
const firstLine = lines[0]?.trim() ?? '';
|
|
66
|
+
const entryMatch = firstLine.match(/^- \[([^\]]+)\]\(([^)]+)\)$/);
|
|
67
|
+
if (!entryMatch) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const rawHref = entryMatch[2];
|
|
71
|
+
const href = rewriteMarkdownHref(rawHref);
|
|
72
|
+
entries.push({
|
|
73
|
+
kind: href.endsWith('/') ? 'directory' : 'article',
|
|
74
|
+
title: entryMatch[1],
|
|
75
|
+
href,
|
|
76
|
+
detail: lines[1]?.trim() || undefined,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return entries;
|
|
80
|
+
}
|
|
52
81
|
function normalizeMeta(data) {
|
|
53
82
|
const meta = { ...data };
|
|
54
83
|
if (typeof data.title === 'string') {
|
|
@@ -78,6 +107,12 @@ function normalizeMeta(data) {
|
|
|
78
107
|
meta.order = order;
|
|
79
108
|
}
|
|
80
109
|
}
|
|
110
|
+
if (typeof data.aliases === 'string' && data.aliases !== '') {
|
|
111
|
+
meta.aliases = [data.aliases];
|
|
112
|
+
}
|
|
113
|
+
if (Array.isArray(data.aliases)) {
|
|
114
|
+
meta.aliases = data.aliases.filter((value) => typeof value === 'string' && value !== '');
|
|
115
|
+
}
|
|
81
116
|
return meta;
|
|
82
117
|
}
|
|
83
118
|
function rewriteMarkdownHref(href) {
|
|
@@ -1,16 +1,36 @@
|
|
|
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 { resolveRequest } from './router.js';
|
|
4
|
+
import { extractManagedIndexEntries, parseMarkdownDocument, stripManagedIndexBlock, stripManagedIndexLinks, } from './markdown.js';
|
|
5
|
+
import { normalizeRequestPath, resolveRequest } from './router.js';
|
|
6
6
|
import { escapeHtml, renderDocument } from '../html/template.js';
|
|
7
7
|
export async function handleSiteRequest(store, pathname, options) {
|
|
8
|
+
if (pathname === '/sitemap.xml') {
|
|
9
|
+
return renderSitemap(store, options);
|
|
10
|
+
}
|
|
8
11
|
const resolved = resolveRequest(pathname);
|
|
12
|
+
const negotiatedMarkdown = shouldServeMarkdownForRequest(resolved, options.acceptHeader);
|
|
9
13
|
if (resolved.kind === 'not-found' || !resolved.sourcePath) {
|
|
14
|
+
const aliasRedirect = await tryRedirectAlias(store, pathname, options);
|
|
15
|
+
if (aliasRedirect !== null) {
|
|
16
|
+
return aliasRedirect;
|
|
17
|
+
}
|
|
10
18
|
return notFound();
|
|
11
19
|
}
|
|
12
20
|
const entry = await store.get(resolved.sourcePath);
|
|
13
21
|
if (entry === null) {
|
|
22
|
+
const aliasRedirect = await tryRedirectAlias(store, pathname, options);
|
|
23
|
+
if (aliasRedirect !== null) {
|
|
24
|
+
return aliasRedirect;
|
|
25
|
+
}
|
|
26
|
+
const alternateDirectoryMarkdown = await tryServeAlternateDirectoryMarkdown(store, resolved, options, negotiatedMarkdown);
|
|
27
|
+
if (alternateDirectoryMarkdown !== null) {
|
|
28
|
+
return alternateDirectoryMarkdown;
|
|
29
|
+
}
|
|
30
|
+
const alternateMarkdownRedirect = await tryRedirectAlternateDirectoryMarkdown(store, resolved, options);
|
|
31
|
+
if (alternateMarkdownRedirect !== null) {
|
|
32
|
+
return alternateMarkdownRedirect;
|
|
33
|
+
}
|
|
14
34
|
if (resolved.kind === 'html' && resolved.requestPath.endsWith('/')) {
|
|
15
35
|
const directoryIndexResponse = await tryRenderAlternateDirectoryIndex(store, resolved.requestPath, options);
|
|
16
36
|
if (directoryIndexResponse !== null) {
|
|
@@ -26,16 +46,16 @@ export async function handleSiteRequest(store, pathname, options) {
|
|
|
26
46
|
if (entry.kind !== 'text' || entry.text === undefined) {
|
|
27
47
|
return notFound();
|
|
28
48
|
}
|
|
29
|
-
if (resolved.kind === 'markdown') {
|
|
49
|
+
if (resolved.kind === 'markdown' || negotiatedMarkdown) {
|
|
30
50
|
const parsed = await parseMarkdownDocument(resolved.sourcePath, entry.text);
|
|
31
51
|
if (parsed.meta.draft === true && options.draftMode === 'exclude') {
|
|
32
52
|
return notFound();
|
|
33
53
|
}
|
|
34
54
|
return {
|
|
35
55
|
status: 200,
|
|
36
|
-
headers: {
|
|
56
|
+
headers: withVaryAcceptIfNeeded({
|
|
37
57
|
'content-type': entry.mediaType,
|
|
38
|
-
},
|
|
58
|
+
}, negotiatedMarkdown),
|
|
39
59
|
body: entry.text,
|
|
40
60
|
};
|
|
41
61
|
}
|
|
@@ -49,17 +69,26 @@ export async function handleSiteRequest(store, pathname, options) {
|
|
|
49
69
|
: isRootHomeRequest(resolved.requestPath) && navigation.items.length > 0
|
|
50
70
|
? stripManagedIndexLinks(entry.text, new Set(navigation.items.map((item) => item.href)))
|
|
51
71
|
: entry.text;
|
|
52
|
-
const
|
|
72
|
+
const catalogEntries = options.siteConfig.template === 'catalog'
|
|
73
|
+
? extractManagedIndexEntries(renderedBody)
|
|
74
|
+
: [];
|
|
75
|
+
const documentBody = options.siteConfig.template === 'catalog'
|
|
76
|
+
? stripManagedIndexBlock(renderedBody)
|
|
77
|
+
: renderedBody;
|
|
78
|
+
const renderedParsed = documentBody === entry.text
|
|
53
79
|
? parsed
|
|
54
|
-
: await parseMarkdownDocument(resolved.sourcePath,
|
|
80
|
+
: await parseMarkdownDocument(resolved.sourcePath, documentBody);
|
|
55
81
|
return {
|
|
56
82
|
status: 200,
|
|
57
|
-
headers: {
|
|
83
|
+
headers: withVaryAcceptIfNeeded({
|
|
58
84
|
'content-type': 'text/html; charset=utf-8',
|
|
59
|
-
},
|
|
85
|
+
}, shouldVaryOnAccept(resolved)),
|
|
60
86
|
body: renderDocument({
|
|
61
87
|
siteTitle: options.siteConfig.siteTitle,
|
|
62
88
|
siteDescription: options.siteConfig.siteDescription,
|
|
89
|
+
siteUrl: options.siteConfig.siteUrl,
|
|
90
|
+
favicon: options.siteConfig.favicon,
|
|
91
|
+
logo: options.siteConfig.logo,
|
|
63
92
|
title: getDocumentTitle(parsed),
|
|
64
93
|
body: renderedParsed.html,
|
|
65
94
|
summary: options.siteConfig.showSummary === false ? undefined : parsed.meta.summary,
|
|
@@ -69,7 +98,14 @@ export async function handleSiteRequest(store, pathname, options) {
|
|
|
69
98
|
theme: options.siteConfig.theme,
|
|
70
99
|
template: options.siteConfig.template,
|
|
71
100
|
topNav: navigation.items,
|
|
101
|
+
footerNav: options.siteConfig.footerNav,
|
|
102
|
+
footerText: options.siteConfig.footerText,
|
|
103
|
+
socialLinks: options.siteConfig.socialLinks,
|
|
104
|
+
editLinkHref: getEditLinkHref(options.siteConfig, resolved.sourcePath),
|
|
72
105
|
stylesheetContent: options.siteConfig.stylesheetContent,
|
|
106
|
+
canonicalPath: getCanonicalHtmlPathForContentPath(resolved.sourcePath),
|
|
107
|
+
alternateMarkdownPath: getMarkdownRequestPathForContentPath(resolved.sourcePath),
|
|
108
|
+
catalogEntries,
|
|
73
109
|
}),
|
|
74
110
|
};
|
|
75
111
|
}
|
|
@@ -103,6 +139,61 @@ function notFound() {
|
|
|
103
139
|
body: 'Not Found',
|
|
104
140
|
};
|
|
105
141
|
}
|
|
142
|
+
function redirect(location) {
|
|
143
|
+
return {
|
|
144
|
+
status: 308,
|
|
145
|
+
headers: {
|
|
146
|
+
location,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
async function renderSitemap(store, options) {
|
|
151
|
+
if (!options.siteConfig.siteUrl) {
|
|
152
|
+
return {
|
|
153
|
+
status: 500,
|
|
154
|
+
headers: {
|
|
155
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
156
|
+
},
|
|
157
|
+
body: 'sitemap.xml requires siteUrl in mdorigin.config.json',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const entries = await collectSitemapEntries(store, '', options);
|
|
161
|
+
const body = [
|
|
162
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
163
|
+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
164
|
+
...entries.map((entry) => {
|
|
165
|
+
const lastmod = entry.lastmod ? `<lastmod>${escapeHtml(entry.lastmod)}</lastmod>` : '';
|
|
166
|
+
return ` <url><loc>${escapeHtml(`${options.siteConfig.siteUrl}${entry.path}`)}</loc>${lastmod}</url>`;
|
|
167
|
+
}),
|
|
168
|
+
'</urlset>',
|
|
169
|
+
].join('\n');
|
|
170
|
+
return {
|
|
171
|
+
status: 200,
|
|
172
|
+
headers: {
|
|
173
|
+
'content-type': 'application/xml; charset=utf-8',
|
|
174
|
+
},
|
|
175
|
+
body,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function withVaryAcceptIfNeeded(headers, enabled) {
|
|
179
|
+
if (!enabled) {
|
|
180
|
+
return headers;
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
...headers,
|
|
184
|
+
vary: appendVary(headers.vary, 'Accept'),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function appendVary(existing, value) {
|
|
188
|
+
if (!existing || existing.trim() === '') {
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
191
|
+
const parts = existing.split(',').map((part) => part.trim().toLowerCase());
|
|
192
|
+
if (parts.includes(value.toLowerCase())) {
|
|
193
|
+
return existing;
|
|
194
|
+
}
|
|
195
|
+
return `${existing}, ${value}`;
|
|
196
|
+
}
|
|
106
197
|
function getDocumentTitle(parsed) {
|
|
107
198
|
if (parsed.meta.title) {
|
|
108
199
|
return parsed.meta.title;
|
|
@@ -112,6 +203,50 @@ function getDocumentTitle(parsed) {
|
|
|
112
203
|
? path.posix.basename(path.posix.dirname(parsed.sourcePath)) || 'mdorigin'
|
|
113
204
|
: basename;
|
|
114
205
|
}
|
|
206
|
+
async function collectSitemapEntries(store, directoryPath, options) {
|
|
207
|
+
const entries = await store.listDirectory(directoryPath);
|
|
208
|
+
if (entries === null) {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
const sitemapEntries = [];
|
|
212
|
+
for (const entry of entries) {
|
|
213
|
+
if (entry.kind === 'directory') {
|
|
214
|
+
sitemapEntries.push(...(await collectSitemapEntries(store, entry.path, options)));
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (!isMarkdownEntry(entry)) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const document = await store.get(entry.path);
|
|
221
|
+
if (document === null || document.kind !== 'text' || document.text === undefined) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const parsed = await parseMarkdownDocument(entry.path, document.text);
|
|
225
|
+
if (parsed.meta.draft === true && options.draftMode === 'exclude') {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
sitemapEntries.push({
|
|
229
|
+
path: getCanonicalHtmlPathForContentPath(entry.path),
|
|
230
|
+
lastmod: parsed.meta.date,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
sitemapEntries.sort((left, right) => left.path.localeCompare(right.path));
|
|
234
|
+
return dedupeSitemapEntries(sitemapEntries);
|
|
235
|
+
}
|
|
236
|
+
function dedupeSitemapEntries(entries) {
|
|
237
|
+
const deduped = new Map();
|
|
238
|
+
for (const entry of entries) {
|
|
239
|
+
const existing = deduped.get(entry.path);
|
|
240
|
+
if (!existing) {
|
|
241
|
+
deduped.set(entry.path, entry);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (!existing.lastmod && entry.lastmod) {
|
|
245
|
+
deduped.set(entry.path, entry);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return Array.from(deduped.values());
|
|
249
|
+
}
|
|
115
250
|
async function renderDirectoryListing(store, requestPath, siteConfig) {
|
|
116
251
|
const directoryPath = requestPath === '/' ? '' : requestPath.slice(1).replace(/\/$/, '');
|
|
117
252
|
const entries = await store.listDirectory(directoryPath);
|
|
@@ -135,6 +270,9 @@ async function renderDirectoryListing(store, requestPath, siteConfig) {
|
|
|
135
270
|
body: renderDocument({
|
|
136
271
|
siteTitle: siteConfig.siteTitle,
|
|
137
272
|
siteDescription: siteConfig.siteDescription,
|
|
273
|
+
siteUrl: siteConfig.siteUrl,
|
|
274
|
+
favicon: siteConfig.favicon,
|
|
275
|
+
logo: siteConfig.logo,
|
|
138
276
|
title: getDirectoryTitle(requestPath),
|
|
139
277
|
body,
|
|
140
278
|
showSummary: false,
|
|
@@ -142,7 +280,12 @@ async function renderDirectoryListing(store, requestPath, siteConfig) {
|
|
|
142
280
|
theme: siteConfig.theme,
|
|
143
281
|
template: siteConfig.template,
|
|
144
282
|
topNav: navigation.items,
|
|
283
|
+
footerNav: siteConfig.footerNav,
|
|
284
|
+
footerText: siteConfig.footerText,
|
|
285
|
+
socialLinks: siteConfig.socialLinks,
|
|
145
286
|
stylesheetContent: siteConfig.stylesheetContent,
|
|
287
|
+
canonicalPath: requestPath,
|
|
288
|
+
alternateMarkdownPath: getMarkdownRequestPathForContentPath(getDirectoryIndexContentPathForRequestPath(requestPath)),
|
|
146
289
|
}),
|
|
147
290
|
};
|
|
148
291
|
}
|
|
@@ -167,6 +310,11 @@ function getDirectoryEntryLabel(entry) {
|
|
|
167
310
|
function getDirectoryTitle(requestPath) {
|
|
168
311
|
return requestPath === '/' ? 'Index' : requestPath;
|
|
169
312
|
}
|
|
313
|
+
function getDirectoryIndexContentPathForRequestPath(requestPath) {
|
|
314
|
+
return requestPath === '/'
|
|
315
|
+
? 'index.md'
|
|
316
|
+
: `${requestPath.slice(1).replace(/\/$/, '')}/index.md`;
|
|
317
|
+
}
|
|
170
318
|
async function tryRenderAlternateDirectoryIndex(store, requestPath, options) {
|
|
171
319
|
const directoryPath = requestPath === '/' ? '' : requestPath.slice(1).replace(/\/$/, '');
|
|
172
320
|
for (const candidatePath of getDirectoryIndexCandidates(directoryPath)) {
|
|
@@ -187,9 +335,15 @@ async function tryRenderAlternateDirectoryIndex(store, requestPath, options) {
|
|
|
187
335
|
: isRootHomeRequest(requestPath) && navigation.items.length > 0
|
|
188
336
|
? stripManagedIndexLinks(entry.text, new Set(navigation.items.map((item) => item.href)))
|
|
189
337
|
: entry.text;
|
|
190
|
-
const
|
|
338
|
+
const catalogEntries = options.siteConfig.template === 'catalog'
|
|
339
|
+
? extractManagedIndexEntries(renderedBody)
|
|
340
|
+
: [];
|
|
341
|
+
const documentBody = options.siteConfig.template === 'catalog'
|
|
342
|
+
? stripManagedIndexBlock(renderedBody)
|
|
343
|
+
: renderedBody;
|
|
344
|
+
const renderedParsed = documentBody === entry.text
|
|
191
345
|
? parsed
|
|
192
|
-
: await parseMarkdownDocument(candidatePath,
|
|
346
|
+
: await parseMarkdownDocument(candidatePath, documentBody);
|
|
193
347
|
return {
|
|
194
348
|
status: 200,
|
|
195
349
|
headers: {
|
|
@@ -198,6 +352,9 @@ async function tryRenderAlternateDirectoryIndex(store, requestPath, options) {
|
|
|
198
352
|
body: renderDocument({
|
|
199
353
|
siteTitle: options.siteConfig.siteTitle,
|
|
200
354
|
siteDescription: options.siteConfig.siteDescription,
|
|
355
|
+
siteUrl: options.siteConfig.siteUrl,
|
|
356
|
+
favicon: options.siteConfig.favicon,
|
|
357
|
+
logo: options.siteConfig.logo,
|
|
201
358
|
title: getDocumentTitle(parsed),
|
|
202
359
|
body: renderedParsed.html,
|
|
203
360
|
summary: options.siteConfig.showSummary === false ? undefined : parsed.meta.summary,
|
|
@@ -207,15 +364,170 @@ async function tryRenderAlternateDirectoryIndex(store, requestPath, options) {
|
|
|
207
364
|
theme: options.siteConfig.theme,
|
|
208
365
|
template: options.siteConfig.template,
|
|
209
366
|
topNav: navigation.items,
|
|
367
|
+
footerNav: options.siteConfig.footerNav,
|
|
368
|
+
footerText: options.siteConfig.footerText,
|
|
369
|
+
socialLinks: options.siteConfig.socialLinks,
|
|
370
|
+
editLinkHref: getEditLinkHref(options.siteConfig, candidatePath),
|
|
210
371
|
stylesheetContent: options.siteConfig.stylesheetContent,
|
|
372
|
+
canonicalPath: requestPath,
|
|
373
|
+
alternateMarkdownPath: getMarkdownRequestPathForContentPath(candidatePath),
|
|
374
|
+
catalogEntries,
|
|
211
375
|
}),
|
|
212
376
|
};
|
|
213
377
|
}
|
|
214
378
|
return null;
|
|
215
379
|
}
|
|
380
|
+
async function tryServeAlternateDirectoryMarkdown(store, resolved, options, negotiatedMarkdown) {
|
|
381
|
+
if (!negotiatedMarkdown || resolved.kind !== 'html' || !resolved.sourcePath) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
if (!resolved.requestPath.endsWith('/')) {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
const directoryPath = path.posix.dirname(resolved.sourcePath);
|
|
388
|
+
for (const candidatePath of getDirectoryIndexCandidates(directoryPath === '.' ? '' : directoryPath)) {
|
|
389
|
+
if (candidatePath === resolved.sourcePath) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
const entry = await store.get(candidatePath);
|
|
393
|
+
if (entry === null || entry.kind !== 'text' || entry.text === undefined) {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const parsed = await parseMarkdownDocument(candidatePath, entry.text);
|
|
397
|
+
if (parsed.meta.draft === true && options.draftMode === 'exclude') {
|
|
398
|
+
return notFound();
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
status: 200,
|
|
402
|
+
headers: withVaryAcceptIfNeeded({
|
|
403
|
+
'content-type': entry.mediaType,
|
|
404
|
+
}, true),
|
|
405
|
+
body: entry.text,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
async function tryRedirectAlternateDirectoryMarkdown(store, resolved, options) {
|
|
411
|
+
if (resolved.kind !== 'markdown' || !resolved.sourcePath) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
const basename = path.posix.basename(resolved.sourcePath);
|
|
415
|
+
if (basename !== 'index.md' && basename !== 'README.md') {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
const directoryPath = path.posix.dirname(resolved.sourcePath);
|
|
419
|
+
for (const candidatePath of getDirectoryIndexCandidates(directoryPath === '.' ? '' : directoryPath)) {
|
|
420
|
+
if (candidatePath === resolved.sourcePath) {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
const entry = await store.get(candidatePath);
|
|
424
|
+
if (entry === null || entry.kind !== 'text' || entry.text === undefined) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const parsed = await parseMarkdownDocument(candidatePath, entry.text);
|
|
428
|
+
if (parsed.meta.draft === true && options.draftMode === 'exclude') {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
return redirect(getMarkdownRequestPathForContentPath(candidatePath));
|
|
432
|
+
}
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
function getMarkdownRequestPathForContentPath(contentPath) {
|
|
436
|
+
return `/${contentPath}`;
|
|
437
|
+
}
|
|
216
438
|
function isRootHomeRequest(requestPath) {
|
|
217
439
|
return requestPath === '/';
|
|
218
440
|
}
|
|
441
|
+
function shouldServeMarkdownForRequest(resolved, acceptHeader) {
|
|
442
|
+
return shouldVaryOnAccept(resolved) && acceptsMarkdown(acceptHeader);
|
|
443
|
+
}
|
|
444
|
+
function shouldVaryOnAccept(resolved) {
|
|
445
|
+
if (resolved.kind !== 'html') {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
return !resolved.requestPath.endsWith('.html');
|
|
449
|
+
}
|
|
450
|
+
function acceptsMarkdown(acceptHeader) {
|
|
451
|
+
if (!acceptHeader) {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
return acceptHeader
|
|
455
|
+
.split(',')
|
|
456
|
+
.map((part) => part.split(';', 1)[0]?.trim().toLowerCase())
|
|
457
|
+
.includes('text/markdown');
|
|
458
|
+
}
|
|
459
|
+
async function tryRedirectAlias(store, pathname, options) {
|
|
460
|
+
const normalizedRequestPath = normalizeRequestPath(pathname);
|
|
461
|
+
if (normalizedRequestPath === null) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
const redirectLocation = await findAliasRedirectLocation(store, '', normalizedRequestPath, options);
|
|
465
|
+
if (!redirectLocation || redirectLocation === normalizedRequestPath) {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
return redirect(redirectLocation);
|
|
469
|
+
}
|
|
470
|
+
async function findAliasRedirectLocation(store, directoryPath, requestPath, options) {
|
|
471
|
+
const entries = await store.listDirectory(directoryPath);
|
|
472
|
+
if (entries === null) {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
for (const entry of entries) {
|
|
476
|
+
if (entry.kind === 'directory') {
|
|
477
|
+
const nestedMatch = await findAliasRedirectLocation(store, entry.path, requestPath, options);
|
|
478
|
+
if (nestedMatch !== null) {
|
|
479
|
+
return nestedMatch;
|
|
480
|
+
}
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (!isMarkdownEntry(entry)) {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const document = await store.get(entry.path);
|
|
487
|
+
if (document === null || document.kind !== 'text' || document.text === undefined) {
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
const parsed = await parseMarkdownDocument(entry.path, document.text);
|
|
491
|
+
if (parsed.meta.draft === true && options.draftMode === 'exclude') {
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
const aliases = normalizeAliases(parsed.meta.aliases);
|
|
495
|
+
if (!aliases.includes(requestPath)) {
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
return getCanonicalHtmlPathForContentPath(entry.path);
|
|
499
|
+
}
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
function isMarkdownEntry(entry) {
|
|
503
|
+
return path.posix.extname(entry.name).toLowerCase() === '.md';
|
|
504
|
+
}
|
|
505
|
+
function normalizeAliases(aliases) {
|
|
506
|
+
if (!Array.isArray(aliases)) {
|
|
507
|
+
return [];
|
|
508
|
+
}
|
|
509
|
+
return aliases.flatMap((alias) => {
|
|
510
|
+
if (typeof alias !== 'string') {
|
|
511
|
+
return [];
|
|
512
|
+
}
|
|
513
|
+
const normalized = normalizeRequestPath(alias);
|
|
514
|
+
return normalized === null ? [] : [normalized];
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
function getCanonicalHtmlPathForContentPath(contentPath) {
|
|
518
|
+
const basename = path.posix.basename(contentPath).toLowerCase();
|
|
519
|
+
if (basename === 'index.md' || basename === 'readme.md') {
|
|
520
|
+
const directory = path.posix.dirname(contentPath);
|
|
521
|
+
return directory === '.' ? '/' : `/${directory}/`;
|
|
522
|
+
}
|
|
523
|
+
return `/${contentPath.slice(0, -'.md'.length)}`;
|
|
524
|
+
}
|
|
525
|
+
function getEditLinkHref(siteConfig, sourcePath) {
|
|
526
|
+
if (!siteConfig.editLink || !sourcePath) {
|
|
527
|
+
return undefined;
|
|
528
|
+
}
|
|
529
|
+
return `${siteConfig.editLink.baseUrl}${sourcePath}`;
|
|
530
|
+
}
|
|
219
531
|
async function resolveTopNav(store, siteConfig) {
|
|
220
532
|
if (siteConfig.topNav.length > 0) {
|
|
221
533
|
return {
|
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,25 +5,52 @@ 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;
|
|
18
38
|
}
|
|
19
39
|
export interface ResolvedSiteConfig {
|
|
20
40
|
siteTitle: string;
|
|
21
41
|
siteDescription?: string;
|
|
42
|
+
siteUrl?: string;
|
|
43
|
+
favicon?: string;
|
|
44
|
+
logo?: SiteLogo;
|
|
22
45
|
showDate: boolean;
|
|
23
46
|
showSummary: boolean;
|
|
24
47
|
theme: BuiltInThemeName;
|
|
25
48
|
template: TemplateName;
|
|
26
49
|
topNav: SiteNavItem[];
|
|
50
|
+
footerNav: SiteNavItem[];
|
|
51
|
+
footerText?: string;
|
|
52
|
+
socialLinks: SiteSocialLink[];
|
|
53
|
+
editLink?: EditLinkConfig;
|
|
27
54
|
showHomeIndex: boolean;
|
|
28
55
|
stylesheetContent?: string;
|
|
29
56
|
siteTitleConfigured: boolean;
|