@daz4126/swifty 3.2.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +60 -0
- package/README.md +5 -0
- package/package.json +11 -7
- package/src/assets.js +124 -29
- package/src/build.js +8 -1
- package/src/cli.js +7 -4
- package/src/config.js +87 -5
- package/src/data.js +26 -21
- package/src/frontmatter.js +27 -0
- package/src/index.js +7 -0
- package/src/init.js +4 -0
- package/src/layout.js +8 -1
- package/src/minify.js +33 -127
- package/src/pages.js +69 -28
- package/src/partials.js +10 -6
- package/src/rss.js +4 -3
- package/src/server.js +90 -0
- package/src/sitemap.js +4 -1
- package/src/urls.js +116 -0
- package/src/watcher.js +1 -0
package/src/index.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
export { default, default as build, prepareOutputDirectory } from "./build.js";
|
|
2
2
|
export { defaultConfig, loadConfig, reloadConfig } from "./config.js";
|
|
3
3
|
export { addLinks, createPages, generatePages } from "./pages.js";
|
|
4
|
+
export { startServer } from "./server.js";
|
|
5
|
+
export {
|
|
6
|
+
applyBasePathToHtml,
|
|
7
|
+
normalizeBasePath,
|
|
8
|
+
normalizePermalink,
|
|
9
|
+
withBasePath,
|
|
10
|
+
} from "./urls.js";
|
package/src/init.js
CHANGED
|
@@ -12,7 +12,11 @@ function getStructure(sitename) {
|
|
|
12
12
|
"js/": null,
|
|
13
13
|
"images/": null,
|
|
14
14
|
"data/": null,
|
|
15
|
+
"public/": null,
|
|
16
|
+
".gitignore": `node_modules/
|
|
17
|
+
.swifty-cache/`,
|
|
15
18
|
"config.yaml": `sitename: ${sitename}
|
|
19
|
+
base_path: ""
|
|
16
20
|
breadcrumb_separator: "»"
|
|
17
21
|
breadcrumb_class: swifty_breadcrumb
|
|
18
22
|
link_class: swifty_link
|
package/src/layout.js
CHANGED
|
@@ -37,7 +37,14 @@ const getLayout = async (layoutName) => {
|
|
|
37
37
|
const createTemplate = async () => {
|
|
38
38
|
// Read the template from pages folder
|
|
39
39
|
const templatePath = path.join(baseDir, 'template.html');
|
|
40
|
-
|
|
40
|
+
let templateContent;
|
|
41
|
+
try {
|
|
42
|
+
templateContent = await fs.readFile(templatePath, 'utf-8');
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new Error(`Unable to read template ${templatePath}: ${error.message}`, {
|
|
45
|
+
cause: error,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
41
48
|
|
|
42
49
|
// Preconnect hints for external CDNs (improves connection setup time)
|
|
43
50
|
const preconnectHints = [
|
package/src/minify.js
CHANGED
|
@@ -1,138 +1,44 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const blocks = [];
|
|
5
|
-
const tokenPrefix = `__SWIFTY_MINIFY_BLOCK_${protectionSequence++}_`;
|
|
6
|
-
const protectedContent = content.replace(pattern, (match) => {
|
|
7
|
-
const token = `${tokenPrefix}${blocks.length}__`;
|
|
8
|
-
blocks.push(match);
|
|
9
|
-
return token;
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
return {
|
|
13
|
-
content: protectedContent,
|
|
14
|
-
restore: (value) =>
|
|
15
|
-
value.replace(
|
|
16
|
-
new RegExp(`${tokenPrefix}(\\d+)__`, "g"),
|
|
17
|
-
(_, index) => blocks[index],
|
|
18
|
-
),
|
|
19
|
-
};
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const protectCssValues = (css) => {
|
|
23
|
-
const blocks = [];
|
|
24
|
-
let content = "";
|
|
25
|
-
|
|
26
|
-
const protect = (start, end) => {
|
|
27
|
-
const token = `__SWIFTY_MINIFY_BLOCK_${blocks.length}__`;
|
|
28
|
-
blocks.push(css.slice(start, end));
|
|
29
|
-
content += token;
|
|
30
|
-
return end;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const readStringEnd = (start) => {
|
|
34
|
-
const quote = css[start];
|
|
35
|
-
let index = start + 1;
|
|
36
|
-
|
|
37
|
-
while (index < css.length) {
|
|
38
|
-
if (css[index] === "\\") {
|
|
39
|
-
index += 2;
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
if (css[index] === quote) return index + 1;
|
|
43
|
-
index += 1;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return css.length;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const readUrlEnd = (start) => {
|
|
50
|
-
let index = start + 4;
|
|
51
|
-
|
|
52
|
-
while (index < css.length) {
|
|
53
|
-
const char = css[index];
|
|
54
|
-
if (char === "\"" || char === "'") {
|
|
55
|
-
index = readStringEnd(index);
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
if (char === "\\") {
|
|
59
|
-
index += 2;
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
if (char === ")") return index + 1;
|
|
63
|
-
index += 1;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return css.length;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
for (let index = 0; index < css.length; ) {
|
|
70
|
-
const char = css[index];
|
|
71
|
-
const isUrl = css.slice(index, index + 4).toLowerCase() === "url(";
|
|
72
|
-
|
|
73
|
-
if (char === "/" && css[index + 1] === "*") {
|
|
74
|
-
const commentEnd = css.indexOf("*/", index + 2);
|
|
75
|
-
const end = commentEnd === -1 ? css.length : commentEnd + 2;
|
|
76
|
-
content += css.slice(index, end);
|
|
77
|
-
index = end;
|
|
78
|
-
} else if (char === "\"" || char === "'") {
|
|
79
|
-
index = protect(index, readStringEnd(index));
|
|
80
|
-
} else if (isUrl) {
|
|
81
|
-
index = protect(index, readUrlEnd(index));
|
|
82
|
-
} else {
|
|
83
|
-
content += char;
|
|
84
|
-
index += 1;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
content,
|
|
90
|
-
restore: (value) =>
|
|
91
|
-
value.replace(/__SWIFTY_MINIFY_BLOCK_(\d+)__/g, (_, index) => blocks[index]),
|
|
92
|
-
};
|
|
93
|
-
};
|
|
1
|
+
import CleanCSS from "clean-css";
|
|
2
|
+
import { minify as minifyHtmlContent } from "html-minifier-terser";
|
|
3
|
+
import { minify as minifyJsContent } from "terser";
|
|
94
4
|
|
|
95
5
|
const minifyCss = (css) => {
|
|
96
|
-
const
|
|
6
|
+
const result = new CleanCSS({
|
|
7
|
+
level: 1,
|
|
8
|
+
rebase: false,
|
|
9
|
+
}).minify(css);
|
|
97
10
|
|
|
98
|
-
|
|
99
|
-
.
|
|
100
|
-
|
|
101
|
-
.replace(/\s*([{}:;,])\s*/g, "$1")
|
|
102
|
-
.replace(/;}/g, "}")
|
|
103
|
-
.trim();
|
|
11
|
+
if (result.errors.length) {
|
|
12
|
+
throw new Error(result.errors.join("; "));
|
|
13
|
+
}
|
|
104
14
|
|
|
105
|
-
return
|
|
15
|
+
return result.styles;
|
|
106
16
|
};
|
|
107
17
|
|
|
108
|
-
const minifyJs = (js) => {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
18
|
+
const minifyJs = async (js) => {
|
|
19
|
+
const result = await minifyJsContent(js, {
|
|
20
|
+
compress: true,
|
|
21
|
+
mangle: false,
|
|
22
|
+
format: {
|
|
23
|
+
comments: /^!/,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
116
26
|
|
|
117
|
-
return
|
|
27
|
+
return result.code || "";
|
|
118
28
|
};
|
|
119
29
|
|
|
120
|
-
const minifyHtml = (html) =>
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
.trim();
|
|
134
|
-
|
|
135
|
-
return blocks.restore(strings.restore(minified));
|
|
136
|
-
};
|
|
30
|
+
const minifyHtml = (html) =>
|
|
31
|
+
minifyHtmlContent(html, {
|
|
32
|
+
caseSensitive: true,
|
|
33
|
+
collapseWhitespace: true,
|
|
34
|
+
conservativeCollapse: true,
|
|
35
|
+
continueOnParseError: false,
|
|
36
|
+
keepClosingSlash: true,
|
|
37
|
+
minifyCSS: false,
|
|
38
|
+
minifyJS: false,
|
|
39
|
+
removeComments: true,
|
|
40
|
+
removeScriptTypeAttributes: true,
|
|
41
|
+
removeStyleLinkTypeAttributes: true,
|
|
42
|
+
});
|
|
137
43
|
|
|
138
44
|
export { minifyCss, minifyHtml, minifyJs };
|
package/src/pages.js
CHANGED
|
@@ -3,7 +3,6 @@ import fs from "fs/promises";
|
|
|
3
3
|
import path from "path";
|
|
4
4
|
|
|
5
5
|
import fsExtra from "fs-extra";
|
|
6
|
-
import matter from "gray-matter";
|
|
7
6
|
|
|
8
7
|
import { replacePlaceholders } from "./partials.js";
|
|
9
8
|
import { dirs, defaultConfig, loadConfig } from "./config.js";
|
|
@@ -12,6 +11,13 @@ import { chunkPages, generatePaginationNav } from "./pagination.js";
|
|
|
12
11
|
import { marked } from "./markdown.js";
|
|
13
12
|
import { mapLimit } from "./concurrency.js";
|
|
14
13
|
import { minifyHtml } from "./minify.js";
|
|
14
|
+
import { parseFrontMatter } from "./frontmatter.js";
|
|
15
|
+
import {
|
|
16
|
+
applyBasePathToHtml,
|
|
17
|
+
normalizePermalink,
|
|
18
|
+
routeToOutputPath,
|
|
19
|
+
withBasePath,
|
|
20
|
+
} from "./urls.js";
|
|
15
21
|
|
|
16
22
|
// Returns stats if valid (directory or .md file), null otherwise
|
|
17
23
|
const getValidStats = async (filePath) => {
|
|
@@ -49,10 +55,23 @@ const applyMarkdownFileToPage = async (
|
|
|
49
55
|
filePath,
|
|
50
56
|
{ notFound = false, folderIndex = false } = {},
|
|
51
57
|
) => {
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
let parsed;
|
|
59
|
+
try {
|
|
60
|
+
const markdownContent = await fs.readFile(filePath, "utf-8");
|
|
61
|
+
parsed = parseFrontMatter(markdownContent);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw new Error(`Unable to parse Markdown file ${filePath}: ${error.message}`, {
|
|
64
|
+
cause: error,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const { data, content } = parsed;
|
|
54
68
|
Object.assign(page, { meta: { ...page.meta, ...data }, content });
|
|
55
69
|
|
|
70
|
+
if (data.permalink !== undefined) {
|
|
71
|
+
page.route = normalizePermalink(data.permalink);
|
|
72
|
+
page.url = withBasePath(page.route);
|
|
73
|
+
}
|
|
74
|
+
|
|
56
75
|
if (folderIndex) {
|
|
57
76
|
const stats = await fs.stat(filePath);
|
|
58
77
|
const { dateFormat } = page.meta;
|
|
@@ -148,13 +167,15 @@ const generatePages = async (sourceDir, baseDir = sourceDir, parent) => {
|
|
|
148
167
|
? parent.layout
|
|
149
168
|
: config.default_layout_name;
|
|
150
169
|
|
|
170
|
+
const route = root ? "/" : finalPath;
|
|
151
171
|
const page = {
|
|
152
172
|
name,
|
|
153
173
|
root,
|
|
154
174
|
layout,
|
|
155
175
|
filePath,
|
|
156
176
|
filename: file.name.replace(/\.md$/, ""),
|
|
157
|
-
|
|
177
|
+
route,
|
|
178
|
+
url: withBasePath(route),
|
|
158
179
|
nav: !parent && !root && !notFound,
|
|
159
180
|
parent: parent
|
|
160
181
|
? { title: parent.meta.title, url: parent.url }
|
|
@@ -211,11 +232,11 @@ const generatePages = async (sourceDir, baseDir = sourceDir, parent) => {
|
|
|
211
232
|
|
|
212
233
|
// Load folder's own config for pagination settings
|
|
213
234
|
const dirConfig = await loadConfig(page.filePath);
|
|
214
|
-
const mergedConfig = {
|
|
215
|
-
...page.meta,
|
|
235
|
+
const mergedConfig = {
|
|
216
236
|
...dirConfig,
|
|
237
|
+
...page.meta,
|
|
217
238
|
// Only set default page_count if explicitly specified in either page meta or dir config
|
|
218
|
-
page_count:
|
|
239
|
+
page_count: page.meta.page_count ?? dirConfig.page_count,
|
|
219
240
|
};
|
|
220
241
|
page.meta = mergedConfig;
|
|
221
242
|
|
|
@@ -258,6 +279,7 @@ const generatePages = async (sourceDir, baseDir = sourceDir, parent) => {
|
|
|
258
279
|
const paginatedPage = {
|
|
259
280
|
name: `${page.name} - Page ${pageNum}`,
|
|
260
281
|
title: `${page.meta.title || page.name} - Page ${pageNum}`,
|
|
282
|
+
route: `${page.route}/page/${pageNum}`,
|
|
261
283
|
url: `${page.url}/page/${pageNum}`,
|
|
262
284
|
folder: false,
|
|
263
285
|
layout: page.layout,
|
|
@@ -295,15 +317,18 @@ const generatePages = async (sourceDir, baseDir = sourceDir, parent) => {
|
|
|
295
317
|
pageIndex.push({ url: page.url, title: page.title, nav: page.nav });
|
|
296
318
|
}
|
|
297
319
|
}, getBuildConcurrency());
|
|
298
|
-
} catch (
|
|
299
|
-
|
|
320
|
+
} catch (error) {
|
|
321
|
+
throw new Error(`Unable to generate pages from ${sourceDir}: ${error.message}`, {
|
|
322
|
+
cause: error,
|
|
323
|
+
});
|
|
300
324
|
}
|
|
301
325
|
|
|
302
326
|
// Make Tags page
|
|
303
327
|
if (!parent && tagsMap.size) {
|
|
304
328
|
const tagLayout = await fsExtra.pathExists(`${dirs.layouts}/tags.html`);
|
|
305
329
|
const tagPage = {
|
|
306
|
-
|
|
330
|
+
route: "/tags",
|
|
331
|
+
url: withBasePath("/tags"),
|
|
307
332
|
nav: false,
|
|
308
333
|
folder: true,
|
|
309
334
|
name: "Tags",
|
|
@@ -318,6 +343,7 @@ const generatePages = async (sourceDir, baseDir = sourceDir, parent) => {
|
|
|
318
343
|
};
|
|
319
344
|
|
|
320
345
|
for (const [tag, pagesForTag] of tagsMap) {
|
|
346
|
+
const route = `/tags/${tag}`;
|
|
321
347
|
const page = {
|
|
322
348
|
name: tag,
|
|
323
349
|
title: tag,
|
|
@@ -325,7 +351,8 @@ const generatePages = async (sourceDir, baseDir = sourceDir, parent) => {
|
|
|
325
351
|
undefined,
|
|
326
352
|
defaultConfig.dateFormat,
|
|
327
353
|
),
|
|
328
|
-
|
|
354
|
+
route,
|
|
355
|
+
url: withBasePath(route),
|
|
329
356
|
layout: tagLayout ? "tags" : defaultConfig.default_layout_name,
|
|
330
357
|
meta: { ...config, title: `Pages tagged with ${capitalize(tag)}` },
|
|
331
358
|
};
|
|
@@ -372,17 +399,23 @@ const generateLinkList = async (name, pages) => {
|
|
|
372
399
|
};
|
|
373
400
|
|
|
374
401
|
const render = async (page) => {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
402
|
+
try {
|
|
403
|
+
const replacedContent = await replacePlaceholders(page.content, page);
|
|
404
|
+
const htmlContent = marked.parse(replacedContent); // Markdown processed once
|
|
405
|
+
const wrappedContent = await applyLayoutAndWrapContent(page, htmlContent);
|
|
406
|
+
// Use function to avoid $` special replacement patterns in content
|
|
407
|
+
const template = await getTemplate();
|
|
408
|
+
const htmlWithTemplate = template.replace(
|
|
409
|
+
/<%=\s*content\s*%>/g,
|
|
410
|
+
() => wrappedContent,
|
|
411
|
+
);
|
|
412
|
+
const finalContent = await replacePlaceholders(htmlWithTemplate, page);
|
|
413
|
+
return applyBasePathToHtml(finalContent);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
throw new Error(`Unable to render page ${page.url}: ${error.message}`, {
|
|
416
|
+
cause: error,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
386
419
|
};
|
|
387
420
|
|
|
388
421
|
const createPages = async (pages, distDir = dirs.dist) => {
|
|
@@ -390,12 +423,20 @@ const createPages = async (pages, distDir = dirs.dist) => {
|
|
|
390
423
|
pages,
|
|
391
424
|
async (page) => {
|
|
392
425
|
const renderedHtml = await render(page);
|
|
393
|
-
|
|
394
|
-
|
|
426
|
+
let html = renderedHtml;
|
|
427
|
+
if (minificationEnabled()) {
|
|
428
|
+
try {
|
|
429
|
+
html = await minifyHtml(renderedHtml);
|
|
430
|
+
} catch (error) {
|
|
431
|
+
throw new Error(`Unable to minify HTML for ${page.url}: ${error.message}`, {
|
|
432
|
+
cause: error,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
395
436
|
const pagePath = page.notFound
|
|
396
437
|
? path.join(distDir, "404.html")
|
|
397
|
-
: path.join(distDir, page.
|
|
398
|
-
await fsExtra.ensureDir(
|
|
438
|
+
: path.join(distDir, routeToOutputPath(page.route || page.url));
|
|
439
|
+
await fsExtra.ensureDir(path.dirname(pagePath));
|
|
399
440
|
await fs.writeFile(pagePath, html);
|
|
400
441
|
if (page.folder) {
|
|
401
442
|
await createPages(page.pages, distDir);
|
|
@@ -422,7 +463,7 @@ const addLinks = async (pages, parent) => {
|
|
|
422
463
|
? page.meta.tags
|
|
423
464
|
.map(
|
|
424
465
|
(tag) =>
|
|
425
|
-
`<a class="${defaultConfig.tag_class}" href="
|
|
466
|
+
`<a class="${defaultConfig.tag_class}" href="${withBasePath(`/tags/${tag}`)}">${tag}</a>`,
|
|
426
467
|
)
|
|
427
468
|
.join("")
|
|
428
469
|
: "";
|
|
@@ -432,7 +473,7 @@ const addLinks = async (pages, parent) => {
|
|
|
432
473
|
: ` ${defaultConfig.breadcrumb_separator} <a class="${defaultConfig.breadcrumb_class}" href="${page.url}">${crumbTitle}</a>`;
|
|
433
474
|
page.meta.breadcrumbs = parent
|
|
434
475
|
? parent.meta.breadcrumbs + crumb
|
|
435
|
-
: `<a class="${defaultConfig.breadcrumb_class}" href="/">Home</a>` +
|
|
476
|
+
: `<a class="${defaultConfig.breadcrumb_class}" href="${withBasePath("/")}">Home</a>` +
|
|
436
477
|
crumb;
|
|
437
478
|
|
|
438
479
|
// Generate prev/next links based on sibling position (only for content pages, not folders or index)
|
package/src/partials.js
CHANGED
|
@@ -8,6 +8,7 @@ import { dirs, defaultConfig } from "./config.js";
|
|
|
8
8
|
import { marked } from "./markdown.js";
|
|
9
9
|
import { loadData } from "./data.js";
|
|
10
10
|
import { getResponsiveImage } from "./assets.js";
|
|
11
|
+
import { withBasePath, withoutBasePath } from "./urls.js";
|
|
11
12
|
|
|
12
13
|
const partialCache = new Map();
|
|
13
14
|
const imageExtensionRegex = /\.(png|jpe?g|webp)(?=([?#]|$))/i;
|
|
@@ -130,8 +131,7 @@ const loadPartialData = async (partialName) => {
|
|
|
130
131
|
return partialData;
|
|
131
132
|
}
|
|
132
133
|
|
|
133
|
-
|
|
134
|
-
return { content: `<p>Include "${partialName}" not found.</p>`, raw: true };
|
|
134
|
+
throw new Error(`Partial "${partialName}" was not found in ${dirs.partials}`);
|
|
135
135
|
};
|
|
136
136
|
|
|
137
137
|
const loadPartial = async (partialName) => {
|
|
@@ -140,13 +140,14 @@ const loadPartial = async (partialName) => {
|
|
|
140
140
|
};
|
|
141
141
|
|
|
142
142
|
const shouldRewriteImageUrl = (url) => {
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
const route = withoutBasePath(url);
|
|
144
|
+
if (!route || !route.startsWith("/images/")) return false;
|
|
145
|
+
return imageExtensionRegex.test(route);
|
|
145
146
|
};
|
|
146
147
|
|
|
147
148
|
const rewriteImageUrl = (url) => {
|
|
148
149
|
if (!shouldRewriteImageUrl(url)) return url;
|
|
149
|
-
return url.replace(imageExtensionRegex, ".webp");
|
|
150
|
+
return withBasePath(withoutBasePath(url).replace(imageExtensionRegex, ".webp"));
|
|
150
151
|
};
|
|
151
152
|
|
|
152
153
|
const rewriteSrcset = (srcset) =>
|
|
@@ -299,7 +300,10 @@ const replacePlaceholders = async (template, values) => {
|
|
|
299
300
|
try {
|
|
300
301
|
template = eta.renderString(template, templateData);
|
|
301
302
|
} catch (error) {
|
|
302
|
-
|
|
303
|
+
const source = values.filePath || values.url || values.title || "template";
|
|
304
|
+
throw new Error(`Unable to render ${source}: ${error.message}`, {
|
|
305
|
+
cause: error,
|
|
306
|
+
});
|
|
303
307
|
}
|
|
304
308
|
|
|
305
309
|
// Restore code blocks
|
package/src/rss.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "fs/promises";
|
|
|
2
2
|
import fsExtra from "fs-extra";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { defaultConfig } from "./config.js";
|
|
5
|
+
import { withBasePath } from "./urls.js";
|
|
5
6
|
|
|
6
7
|
// Escape XML special characters
|
|
7
8
|
const escapeXml = (str) => {
|
|
@@ -40,8 +41,8 @@ const generateRssFeed = (feedConfig, pages, siteUrl) => {
|
|
|
40
41
|
folder,
|
|
41
42
|
} = typeof feedConfig === "string" ? { folder: feedConfig } : feedConfig;
|
|
42
43
|
|
|
43
|
-
const feedUrl = `${siteUrl}
|
|
44
|
-
const feedLink = `${siteUrl}
|
|
44
|
+
const feedUrl = `${siteUrl}${withBasePath(`/${folder}/rss.xml`)}`;
|
|
45
|
+
const feedLink = `${siteUrl}${withBasePath(`/${folder}`)}`;
|
|
45
46
|
|
|
46
47
|
// Sort pages by date (newest first)
|
|
47
48
|
const sortedPages = [...pages].sort((a, b) => {
|
|
@@ -94,7 +95,7 @@ const findPagesInFolder = (pages, folderName) => {
|
|
|
94
95
|
const searchPages = (pageList) => {
|
|
95
96
|
for (const page of pageList) {
|
|
96
97
|
// Check if this page's URL starts with the folder path
|
|
97
|
-
if (page.
|
|
98
|
+
if (page.route?.startsWith(`/${folderName}/`) && !page.folder) {
|
|
98
99
|
found.push(page);
|
|
99
100
|
}
|
|
100
101
|
// Recursively search nested pages
|
package/src/server.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import http from "http";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
import { baseDir, defaultConfig } from "./config.js";
|
|
6
|
+
import { withoutBasePath } from "./urls.js";
|
|
7
|
+
|
|
8
|
+
const contentTypes = {
|
|
9
|
+
".css": "text/css; charset=utf-8",
|
|
10
|
+
".gif": "image/gif",
|
|
11
|
+
".html": "text/html; charset=utf-8",
|
|
12
|
+
".ico": "image/x-icon",
|
|
13
|
+
".jpeg": "image/jpeg",
|
|
14
|
+
".jpg": "image/jpeg",
|
|
15
|
+
".js": "text/javascript; charset=utf-8",
|
|
16
|
+
".json": "application/json; charset=utf-8",
|
|
17
|
+
".png": "image/png",
|
|
18
|
+
".svg": "image/svg+xml",
|
|
19
|
+
".txt": "text/plain; charset=utf-8",
|
|
20
|
+
".webmanifest": "application/manifest+json; charset=utf-8",
|
|
21
|
+
".webp": "image/webp",
|
|
22
|
+
".xml": "application/xml; charset=utf-8",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const readResponseFile = async (outputDirectory, pathname) => {
|
|
26
|
+
const route = withoutBasePath(pathname);
|
|
27
|
+
const relativePath = route.replace(/^\/+/, "");
|
|
28
|
+
const candidates = path.extname(relativePath)
|
|
29
|
+
? [relativePath]
|
|
30
|
+
: [relativePath, path.join(relativePath, "index.html")];
|
|
31
|
+
|
|
32
|
+
for (const candidate of candidates) {
|
|
33
|
+
const filePath = path.resolve(outputDirectory, candidate);
|
|
34
|
+
if (!filePath.startsWith(`${outputDirectory}${path.sep}`)) continue;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
return { body: await fs.readFile(filePath), filePath, status: 200 };
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error.code !== "ENOENT" && error.code !== "EISDIR") throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const notFoundPath = path.join(outputDirectory, "404.html");
|
|
44
|
+
try {
|
|
45
|
+
return { body: await fs.readFile(notFoundPath), filePath: notFoundPath, status: 404 };
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error.code !== "ENOENT") throw error;
|
|
48
|
+
return {
|
|
49
|
+
body: Buffer.from("Not Found"),
|
|
50
|
+
filePath: "404.txt",
|
|
51
|
+
status: 404,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const startServer = async (
|
|
57
|
+
outputDir = "dist",
|
|
58
|
+
port = defaultConfig.server_port || 3000,
|
|
59
|
+
) => {
|
|
60
|
+
const outputDirectory = path.resolve(baseDir, outputDir);
|
|
61
|
+
const server = http.createServer(async (request, response) => {
|
|
62
|
+
try {
|
|
63
|
+
const url = new URL(request.url || "/", "http://localhost");
|
|
64
|
+
const pathname = decodeURIComponent(url.pathname);
|
|
65
|
+
const result = await readResponseFile(outputDirectory, pathname);
|
|
66
|
+
const contentType =
|
|
67
|
+
contentTypes[path.extname(result.filePath).toLowerCase()] ||
|
|
68
|
+
"application/octet-stream";
|
|
69
|
+
|
|
70
|
+
response.writeHead(result.status, { "Content-Type": contentType });
|
|
71
|
+
response.end(request.method === "HEAD" ? undefined : result.body);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
response.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
74
|
+
response.end(`Server error: ${error.message}`);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await new Promise((resolve, reject) => {
|
|
79
|
+
server.once("error", reject);
|
|
80
|
+
server.listen(port, "127.0.0.1", resolve);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const address = server.address();
|
|
84
|
+
const actualPort = typeof address === "object" ? address.port : port;
|
|
85
|
+
console.log(`Swifty server running at http://localhost:${actualPort}`);
|
|
86
|
+
return server;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export { readResponseFile, startServer };
|
|
90
|
+
export default startServer;
|
package/src/sitemap.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "fs/promises";
|
|
|
2
2
|
import fsExtra from "fs-extra";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { defaultConfig } from "./config.js";
|
|
5
|
+
import { withBasePath } from "./urls.js";
|
|
5
6
|
|
|
6
7
|
const escapeXml = (str) =>
|
|
7
8
|
String(str || "")
|
|
@@ -69,7 +70,9 @@ ${urls}
|
|
|
69
70
|
const generateRobotsTxt = (siteUrl = "") => {
|
|
70
71
|
const lines = ["User-agent: *", "Allow: /"];
|
|
71
72
|
if (siteUrl) {
|
|
72
|
-
lines.push(
|
|
73
|
+
lines.push(
|
|
74
|
+
`Sitemap: ${siteUrl.replace(/\/$/, "")}${withBasePath("/sitemap.xml")}`,
|
|
75
|
+
);
|
|
73
76
|
}
|
|
74
77
|
return `${lines.join("\n")}\n`;
|
|
75
78
|
};
|