@daz4126/swifty 2.10.0 → 2.12.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/README.md +16 -3
- package/package.json +3 -2
- package/src/assets.js +106 -54
- package/src/build.js +57 -1
- package/src/cli.js +51 -7
- package/src/concurrency.js +25 -0
- package/src/config.js +22 -2
- package/src/data.js +70 -0
- package/src/init.js +36 -33
- package/src/layout.js +3 -3
- package/src/pages.js +236 -118
- package/src/pagination.js +58 -0
- package/src/partials.js +236 -42
- package/src/rss.js +6 -4
- package/src/sitemap.js +89 -0
- package/src/watcher.js +40 -12
package/src/init.js
CHANGED
|
@@ -2,62 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import path from "path";
|
|
5
|
-
import { fileURLToPath } from "url";
|
|
6
|
-
import { dirname } from "path";
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"js/": null,
|
|
19
|
-
"images/": null,
|
|
20
|
-
"config.yaml": `sitename: Swifty
|
|
6
|
+
function getStructure(sitename) {
|
|
7
|
+
return {
|
|
8
|
+
"pages/index.md": `# Welcome to ${sitename}\n\nThis is my home page.`,
|
|
9
|
+
"layouts/": null,
|
|
10
|
+
"partials/": null,
|
|
11
|
+
"css/": null,
|
|
12
|
+
"js/": null,
|
|
13
|
+
"images/": null,
|
|
14
|
+
"data/": null,
|
|
15
|
+
"config.yaml": `sitename: ${sitename}
|
|
21
16
|
breadcrumb_separator: "»"
|
|
22
17
|
breadcrumb_class: swifty_breadcrumb
|
|
23
18
|
link_class: swifty_link
|
|
24
|
-
tag_class:
|
|
25
|
-
default_layout_name:
|
|
19
|
+
tag_class: swifty_tag
|
|
20
|
+
default_layout_name: default
|
|
26
21
|
default_link_name: links
|
|
27
22
|
max_image_size: 800
|
|
28
23
|
|
|
29
|
-
dateFormat:
|
|
24
|
+
dateFormat:
|
|
30
25
|
weekday: short
|
|
31
26
|
month: short
|
|
32
27
|
day: numeric
|
|
33
28
|
year: numeric`,
|
|
34
|
-
|
|
29
|
+
"template.html": `<!DOCTYPE html>
|
|
35
30
|
<html lang="en">
|
|
36
31
|
<head>
|
|
37
32
|
<meta charset="UTF-8">
|
|
38
33
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
39
|
-
<
|
|
40
|
-
<link rel="icon" href="favicon-16x16.png" sizes="16x16" type="image/x-icon">
|
|
41
|
-
<link rel="icon" href="favicon-32x32.png" sizes="32x32" type="image/png">
|
|
42
|
-
<link rel="apple-touch-icon" href="path/to/apple-touch-icon.png">
|
|
43
|
-
<link rel="icon" sizes="192x192" href="android-chrome-192x19.png">
|
|
44
|
-
<link rel="icon" sizes="512x512" href="android-chrome-512x512.png">
|
|
45
|
-
<title>{{ sitename }} || {{ title }}</title>
|
|
34
|
+
<title><%= sitename %> | <%= title %></title>
|
|
46
35
|
</head>
|
|
47
36
|
<body>
|
|
48
37
|
<header>
|
|
49
|
-
<h1
|
|
38
|
+
<h1>${sitename}</h1>
|
|
50
39
|
</header>
|
|
51
40
|
<main>
|
|
52
|
-
|
|
41
|
+
<%= content %>
|
|
53
42
|
</main>
|
|
54
43
|
<footer>
|
|
55
44
|
<p>This site was built with Swifty, the super speedy static site generator.</p>
|
|
56
45
|
</footer>
|
|
57
46
|
</body>
|
|
58
47
|
</html>
|
|
59
|
-
|
|
60
|
-
};
|
|
48
|
+
`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
61
51
|
|
|
62
52
|
function createStructure(basePath, structure) {
|
|
63
53
|
Object.entries(structure).forEach(([filePath, content]) => {
|
|
@@ -71,7 +61,20 @@ function createStructure(basePath, structure) {
|
|
|
71
61
|
});
|
|
72
62
|
}
|
|
73
63
|
|
|
74
|
-
|
|
75
|
-
|
|
64
|
+
export default function init(sitename) {
|
|
65
|
+
const projectRoot = path.join(process.cwd(), sitename);
|
|
66
|
+
|
|
67
|
+
// Check if folder already exists
|
|
68
|
+
if (fs.existsSync(projectRoot)) {
|
|
69
|
+
console.error(`Error: Folder "${sitename}" already exists.`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
76
72
|
|
|
77
|
-
|
|
73
|
+
const structure = getStructure(sitename);
|
|
74
|
+
createStructure(projectRoot, structure);
|
|
75
|
+
|
|
76
|
+
console.log(`Site "${sitename}" created successfully!`);
|
|
77
|
+
console.log(`\nNext steps:`);
|
|
78
|
+
console.log(` cd ${sitename}`);
|
|
79
|
+
console.log(` npx swifty start`);
|
|
80
|
+
}
|
package/src/layout.js
CHANGED
|
@@ -40,7 +40,7 @@ const createTemplate = async () => {
|
|
|
40
40
|
? `<script type="module">import * as Turbo from 'https://esm.sh/@hotwired/turbo';</script>`
|
|
41
41
|
: '';
|
|
42
42
|
const livereloadScript = process.env.SWIFTY_WATCH
|
|
43
|
-
? `<script>document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] + '
|
|
43
|
+
? `<script>document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] + ':${defaultConfig.livereload_port || 35729}/livereload.js?snipver=1"></' + 'script>')</script>`
|
|
44
44
|
: '';
|
|
45
45
|
|
|
46
46
|
// Preload local assets for faster loading
|
|
@@ -59,10 +59,10 @@ const createTemplate = async () => {
|
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
const applyLayoutAndWrapContent = async (page,content) => {
|
|
62
|
-
const layoutContent = await getLayout(page.
|
|
62
|
+
const layoutContent = await getLayout(page.meta.layout !== undefined ? page.meta.layout : page.layout);
|
|
63
63
|
if (!layoutContent) return content;
|
|
64
64
|
// Use function to avoid $` special replacement patterns in content
|
|
65
|
-
return layoutContent.replace(
|
|
65
|
+
return layoutContent.replace(/<%=\s*content\s*%>/g, () => content);
|
|
66
66
|
};
|
|
67
67
|
|
|
68
68
|
const getTemplate = async () => {
|
package/src/pages.js
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
import { replacePlaceholders } from "./partials.js";
|
|
3
3
|
import { dirs, defaultConfig, loadConfig } from "./config.js";
|
|
4
4
|
import { getTemplate, applyLayoutAndWrapContent } from "./layout.js";
|
|
5
|
+
import { chunkPages, generatePaginationNav } from "./pagination.js";
|
|
5
6
|
import { marked } from "marked";
|
|
6
7
|
import matter from "gray-matter";
|
|
7
8
|
import fs from "fs/promises";
|
|
8
9
|
import fsExtra from "fs-extra";
|
|
9
10
|
import path from "path";
|
|
11
|
+
import { mapLimit } from "./concurrency.js";
|
|
10
12
|
|
|
11
13
|
// Returns stats if valid (directory or .md file), null otherwise
|
|
12
14
|
const getValidStats = async (filePath) => {
|
|
@@ -42,6 +44,9 @@ const parseDate = (dateValue) => {
|
|
|
42
44
|
const tagsMap = new Map();
|
|
43
45
|
const pageIndex = [];
|
|
44
46
|
const pageIndexUrls = new Set();
|
|
47
|
+
const partialExtensions = [".md", ".html"];
|
|
48
|
+
|
|
49
|
+
const getBuildConcurrency = () => defaultConfig.build_concurrency || 16;
|
|
45
50
|
|
|
46
51
|
const addToTagMap = (tag, page) => {
|
|
47
52
|
if (!tagsMap.has(tag)) tagsMap.set(tag, []);
|
|
@@ -58,110 +63,183 @@ const generatePages = async (sourceDir, baseDir = sourceDir, parent) => {
|
|
|
58
63
|
|
|
59
64
|
const pages = [];
|
|
60
65
|
const folderConfig = await loadConfig(sourceDir);
|
|
61
|
-
const config = { ...defaultConfig, ...parent?.
|
|
66
|
+
const config = { ...defaultConfig, ...parent?.meta, ...folderConfig };
|
|
62
67
|
const { dateFormat } = config;
|
|
63
68
|
|
|
64
69
|
try {
|
|
65
70
|
const files = await fs.readdir(sourceDir, { withFileTypes: true });
|
|
66
71
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
72
|
+
const fileResults = await mapLimit(
|
|
73
|
+
files,
|
|
74
|
+
async (file) => {
|
|
75
|
+
const filePath = path.join(sourceDir, file.name);
|
|
76
|
+
const stats = await getValidStats(filePath);
|
|
77
|
+
if (!stats) return null;
|
|
78
|
+
|
|
79
|
+
const root = file.name === "index.md" && !parent;
|
|
80
|
+
const relativePath = path.relative(baseDir, filePath).replace(/\\/g, "/");
|
|
81
|
+
const finalPath = `/${relativePath.replace(/\.md$/, "")}`;
|
|
82
|
+
const name = root
|
|
83
|
+
? "Home"
|
|
84
|
+
: capitalize(file.name.replace(/\.md$/, "").replace(/-/g, " "));
|
|
85
|
+
const isDirectory = stats.isDirectory();
|
|
86
|
+
const layoutFileExists =
|
|
87
|
+
parent &&
|
|
88
|
+
(await fsExtra.pathExists(`${dirs.layouts}/${parent.filename}.html`));
|
|
89
|
+
const layout = layoutFileExists
|
|
90
|
+
? parent.filename
|
|
91
|
+
: parent
|
|
92
|
+
? parent.layout
|
|
93
|
+
: config.default_layout_name;
|
|
94
|
+
|
|
95
|
+
const page = {
|
|
96
|
+
name,
|
|
97
|
+
root,
|
|
98
|
+
layout,
|
|
99
|
+
filePath,
|
|
100
|
+
filename: file.name.replace(/\.md$/, ""),
|
|
101
|
+
url: root ? "/" : finalPath,
|
|
102
|
+
nav: !parent && !root,
|
|
103
|
+
parent: parent
|
|
104
|
+
? { title: parent.meta.title, url: parent.url }
|
|
105
|
+
: undefined,
|
|
106
|
+
folder: isDirectory,
|
|
107
|
+
title: name,
|
|
108
|
+
createdAtObj: stats.birthtime,
|
|
109
|
+
updatedAtObj: stats.mtime,
|
|
110
|
+
created_at: new Date(stats.birthtime).toLocaleDateString(
|
|
111
|
+
undefined,
|
|
112
|
+
dateFormat,
|
|
113
|
+
),
|
|
114
|
+
updated_at: new Date(stats.mtime).toLocaleDateString(
|
|
115
|
+
undefined,
|
|
116
|
+
dateFormat,
|
|
117
|
+
),
|
|
118
|
+
date: new Date(stats.birthtime).toLocaleDateString(
|
|
119
|
+
undefined,
|
|
120
|
+
dateFormat,
|
|
121
|
+
),
|
|
122
|
+
meta: root ? { ...defaultConfig } : { ...config },
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
if (path.extname(file.name) === ".md") {
|
|
126
|
+
const markdownContent = await fs.readFile(filePath, "utf-8");
|
|
127
|
+
const { data, content } = matter(markdownContent);
|
|
128
|
+
Object.assign(page, { meta: { ...page.meta, ...data }, content });
|
|
129
|
+
page.title = page.meta.title || page.title;
|
|
130
|
+
page.name = page.meta.title || page.name;
|
|
131
|
+
|
|
132
|
+
// Allow front matter to override nav (opt in or opt out)
|
|
133
|
+
if (typeof data.nav === "boolean") {
|
|
134
|
+
page.nav = data.nav;
|
|
135
|
+
}
|
|
116
136
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (parsedDate) {
|
|
126
|
-
page.dateObj = parsedDate;
|
|
127
|
-
page.date = parsedDate.toLocaleDateString(undefined, dateFormat);
|
|
128
|
-
page.data.date = page.date;
|
|
137
|
+
// If front matter has a date, parse and format it
|
|
138
|
+
if (data.date) {
|
|
139
|
+
const parsedDate = parseDate(data.date);
|
|
140
|
+
if (parsedDate) {
|
|
141
|
+
page.dateObj = parsedDate;
|
|
142
|
+
page.date = parsedDate.toLocaleDateString(undefined, dateFormat);
|
|
143
|
+
page.meta.date = page.date;
|
|
144
|
+
}
|
|
129
145
|
}
|
|
130
146
|
}
|
|
131
|
-
}
|
|
132
147
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const fileResults = await Promise.all(filePromises);
|
|
148
|
+
// For directories, we defer recursion separately
|
|
149
|
+
return { page, isDirectory };
|
|
150
|
+
},
|
|
151
|
+
getBuildConcurrency(),
|
|
152
|
+
);
|
|
139
153
|
|
|
140
154
|
// Now handle directories recursively
|
|
141
|
-
|
|
155
|
+
await mapLimit(fileResults, async (result) => {
|
|
142
156
|
if (!result) return;
|
|
143
157
|
|
|
144
158
|
const { page, isDirectory } = result;
|
|
145
159
|
|
|
160
|
+
// Skip draft pages in production builds
|
|
161
|
+
if (page.meta.draft && !process.env.SWIFTY_WATCH) return;
|
|
162
|
+
|
|
163
|
+
// Skip scheduled pages (future date) in production builds
|
|
164
|
+
if (page.dateObj && page.dateObj > new Date() && !process.env.SWIFTY_WATCH) return;
|
|
165
|
+
|
|
146
166
|
if (isDirectory) {
|
|
147
167
|
page.pages = await generatePages(page.filePath, baseDir, page);
|
|
148
168
|
|
|
169
|
+
// Load folder's own config for pagination settings
|
|
170
|
+
const dirConfig = await loadConfig(page.filePath);
|
|
171
|
+
const mergedConfig = {
|
|
172
|
+
...page.meta,
|
|
173
|
+
...dirConfig,
|
|
174
|
+
// Only set default page_count if explicitly specified in either page meta or dir config
|
|
175
|
+
page_count: dirConfig.page_count || page.meta.page_count
|
|
176
|
+
};
|
|
177
|
+
page.meta = mergedConfig;
|
|
178
|
+
|
|
149
179
|
page.pages.sort((a, b) => {
|
|
150
|
-
if (a.
|
|
151
|
-
return a.
|
|
180
|
+
if (a.meta.position && b.meta.position) {
|
|
181
|
+
return a.meta.position - b.meta.position;
|
|
152
182
|
}
|
|
153
183
|
const dateA = a.dateObj || new Date(a.created_at);
|
|
154
184
|
const dateB = b.dateObj || new Date(b.created_at);
|
|
155
|
-
const sortOrder = (
|
|
185
|
+
const sortOrder = (mergedConfig.date_sort_order || "desc").toLowerCase();
|
|
156
186
|
return sortOrder === "asc" ? dateA - dateB : dateB - dateA;
|
|
157
187
|
});
|
|
158
188
|
|
|
159
|
-
|
|
189
|
+
// Handle pagination if page_count is set
|
|
190
|
+
const pageCount = mergedConfig.page_count;
|
|
191
|
+
if (pageCount && page.pages.length > pageCount) {
|
|
192
|
+
page.allPages = page.pages;
|
|
193
|
+
const chunks = chunkPages(page.pages, pageCount);
|
|
194
|
+
const totalPages = chunks.length;
|
|
195
|
+
const baseUrl = `${page.url}/`;
|
|
196
|
+
|
|
197
|
+
// Store visible pages for first page (used by links_to_children)
|
|
198
|
+
page.visiblePages = chunks[0];
|
|
199
|
+
|
|
200
|
+
// First page content
|
|
201
|
+
page.content = await generateLinkList(page.filename, chunks[0]);
|
|
202
|
+
page.meta.pagination = generatePaginationNav({
|
|
203
|
+
currentPage: 1,
|
|
204
|
+
totalPages,
|
|
205
|
+
baseUrl,
|
|
206
|
+
config: page.meta,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Create pagination pages for page 2, 3, etc.
|
|
210
|
+
page.paginatedPages = [];
|
|
211
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
212
|
+
const pageNum = i + 1;
|
|
213
|
+
const paginatedPage = {
|
|
214
|
+
name: `${page.name} - Page ${pageNum}`,
|
|
215
|
+
title: `${page.meta.title || page.name} - Page ${pageNum}`,
|
|
216
|
+
url: `${page.url}/page/${pageNum}`,
|
|
217
|
+
folder: false,
|
|
218
|
+
layout: page.layout,
|
|
219
|
+
parent: { title: page.meta.title || page.name, url: page.url },
|
|
220
|
+
meta: {
|
|
221
|
+
...page.meta,
|
|
222
|
+
title: `${page.meta.title || page.name} - Page ${pageNum}`,
|
|
223
|
+
pagination: generatePaginationNav({
|
|
224
|
+
currentPage: pageNum,
|
|
225
|
+
totalPages,
|
|
226
|
+
baseUrl,
|
|
227
|
+
config: page.meta,
|
|
228
|
+
}),
|
|
229
|
+
},
|
|
230
|
+
content: await generateLinkList(page.filename, chunks[i]),
|
|
231
|
+
};
|
|
232
|
+
page.paginatedPages.push(paginatedPage);
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
page.content = await generateLinkList(page.filename, page.pages);
|
|
236
|
+
page.meta.pagination = '';
|
|
237
|
+
}
|
|
160
238
|
}
|
|
161
239
|
|
|
162
240
|
// Add tags
|
|
163
|
-
if (page.
|
|
164
|
-
page.
|
|
241
|
+
if (page.meta.tags) {
|
|
242
|
+
page.meta.tags.forEach((tag) => addToTagMap(tag, page));
|
|
165
243
|
}
|
|
166
244
|
|
|
167
245
|
pages.push(page);
|
|
@@ -169,10 +247,7 @@ const generatePages = async (sourceDir, baseDir = sourceDir, parent) => {
|
|
|
169
247
|
pageIndexUrls.add(page.url);
|
|
170
248
|
pageIndex.push({ url: page.url, title: page.title, nav: page.nav });
|
|
171
249
|
}
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
// Await all directory recursion
|
|
175
|
-
await Promise.all(directoryPromises);
|
|
250
|
+
}, getBuildConcurrency());
|
|
176
251
|
} catch (err) {
|
|
177
252
|
console.error("Error reading directory:", err);
|
|
178
253
|
}
|
|
@@ -191,7 +266,7 @@ const generatePages = async (sourceDir, baseDir = sourceDir, parent) => {
|
|
|
191
266
|
undefined,
|
|
192
267
|
defaultConfig.dateFormat,
|
|
193
268
|
),
|
|
194
|
-
|
|
269
|
+
meta: { ...config },
|
|
195
270
|
pages: [],
|
|
196
271
|
};
|
|
197
272
|
|
|
@@ -205,7 +280,7 @@ const generatePages = async (sourceDir, baseDir = sourceDir, parent) => {
|
|
|
205
280
|
),
|
|
206
281
|
url: `/tags/${tag}`,
|
|
207
282
|
layout: tagLayout ? "tags" : defaultConfig.default_layout_name,
|
|
208
|
-
|
|
283
|
+
meta: { ...config, title: `Pages tagged with ${capitalize(tag)}` },
|
|
209
284
|
};
|
|
210
285
|
page.content = await generateLinkList("tags", pagesForTag);
|
|
211
286
|
|
|
@@ -218,24 +293,25 @@ const generatePages = async (sourceDir, baseDir = sourceDir, parent) => {
|
|
|
218
293
|
};
|
|
219
294
|
|
|
220
295
|
const generateLinkList = async (name, pages) => {
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
296
|
+
const findPartialPath = async (partialName) => {
|
|
297
|
+
for (const ext of partialExtensions) {
|
|
298
|
+
const partialPath = path.join(dirs.partials, `${partialName}${ext}`);
|
|
299
|
+
if (await fsExtra.pathExists(partialPath)) return partialPath;
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const partialPath = await findPartialPath(name);
|
|
305
|
+
const defaultPath = await findPartialPath(
|
|
225
306
|
defaultConfig.default_link_name || "links",
|
|
226
307
|
);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
fileExists ? partialPath : linksPath,
|
|
235
|
-
"utf-8",
|
|
236
|
-
);
|
|
237
|
-
const content = await Promise.all(
|
|
238
|
-
pages.map((page) => replacePlaceholders(partialContent, page)),
|
|
308
|
+
|
|
309
|
+
if (partialPath || defaultPath) {
|
|
310
|
+
const partialContent = await fs.readFile(partialPath || defaultPath, "utf-8");
|
|
311
|
+
const content = await mapLimit(
|
|
312
|
+
pages,
|
|
313
|
+
(page) => replacePlaceholders(partialContent, page),
|
|
314
|
+
getBuildConcurrency(),
|
|
239
315
|
);
|
|
240
316
|
return content.join("\n");
|
|
241
317
|
} else {
|
|
@@ -255,7 +331,7 @@ const render = async (page) => {
|
|
|
255
331
|
// Use function to avoid $` special replacement patterns in content
|
|
256
332
|
const template = await getTemplate();
|
|
257
333
|
const htmlWithTemplate = template.replace(
|
|
258
|
-
|
|
334
|
+
/<%=\s*content\s*%>/g,
|
|
259
335
|
() => wrappedContent,
|
|
260
336
|
);
|
|
261
337
|
const finalContent = await replacePlaceholders(htmlWithTemplate, page);
|
|
@@ -263,45 +339,78 @@ const render = async (page) => {
|
|
|
263
339
|
};
|
|
264
340
|
|
|
265
341
|
const createPages = async (pages, distDir = dirs.dist) => {
|
|
266
|
-
await
|
|
267
|
-
pages
|
|
342
|
+
await mapLimit(
|
|
343
|
+
pages,
|
|
344
|
+
async (page) => {
|
|
268
345
|
const html = await render(page);
|
|
269
346
|
const pageDir = path.join(distDir, page.url);
|
|
270
347
|
const pagePath = path.join(distDir, page.url, "index.html");
|
|
271
348
|
await fsExtra.ensureDir(pageDir);
|
|
272
349
|
await fs.writeFile(pagePath, html);
|
|
273
350
|
if (page.folder) {
|
|
274
|
-
await createPages(page.pages, distDir);
|
|
351
|
+
await createPages(page.pages, distDir);
|
|
352
|
+
// Write pagination pages if they exist
|
|
353
|
+
if (page.paginatedPages?.length) {
|
|
354
|
+
await createPages(page.paginatedPages, distDir);
|
|
355
|
+
}
|
|
275
356
|
}
|
|
276
|
-
}
|
|
357
|
+
},
|
|
358
|
+
getBuildConcurrency(),
|
|
277
359
|
);
|
|
278
360
|
};
|
|
279
361
|
|
|
280
362
|
const addLinks = async (pages, parent) => {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
363
|
+
// Filter out folders and index pages for prev/next calculation (only content pages)
|
|
364
|
+
const contentPages = pages.filter((p) => !p.folder && p.filename !== 'index');
|
|
365
|
+
|
|
366
|
+
await mapLimit(
|
|
367
|
+
pages,
|
|
368
|
+
async (page) => {
|
|
369
|
+
page.meta ||= {};
|
|
370
|
+
page.meta.links_to_tags = page?.meta?.tags?.length
|
|
371
|
+
? page.meta.tags
|
|
286
372
|
.map(
|
|
287
373
|
(tag) =>
|
|
288
374
|
`<a class="${defaultConfig.tag_class}" href="/tags/${tag}">${tag}</a>`,
|
|
289
375
|
)
|
|
290
376
|
.join("")
|
|
291
377
|
: "";
|
|
378
|
+
const crumbTitle = page.meta.title || page.title || page.name;
|
|
292
379
|
const crumb = page.root
|
|
293
380
|
? ""
|
|
294
|
-
: ` ${defaultConfig.breadcrumb_separator} <a class="${defaultConfig.breadcrumb_class}" href="${page.url}">${
|
|
295
|
-
page.
|
|
296
|
-
? parent.
|
|
381
|
+
: ` ${defaultConfig.breadcrumb_separator} <a class="${defaultConfig.breadcrumb_class}" href="${page.url}">${crumbTitle}</a>`;
|
|
382
|
+
page.meta.breadcrumbs = parent
|
|
383
|
+
? parent.meta.breadcrumbs + crumb
|
|
297
384
|
: `<a class="${defaultConfig.breadcrumb_class}" href="/">Home</a>` +
|
|
298
385
|
crumb;
|
|
299
386
|
|
|
387
|
+
// Generate prev/next links based on sibling position (only for content pages, not folders or index)
|
|
388
|
+
const linkClass = defaultConfig.prev_next_class || defaultConfig.link_class || '';
|
|
389
|
+
const classAttr = linkClass ? ` class="${linkClass}"` : '';
|
|
390
|
+
|
|
391
|
+
if (!page.folder && page.filename !== 'index') {
|
|
392
|
+
const pageIndex = contentPages.indexOf(page);
|
|
393
|
+
const prevSibling = pageIndex > 0 ? contentPages[pageIndex - 1] : null;
|
|
394
|
+
const nextSibling = pageIndex < contentPages.length - 1 ? contentPages[pageIndex + 1] : null;
|
|
395
|
+
|
|
396
|
+
page.meta.prev_page = prevSibling
|
|
397
|
+
? `<a href="${prevSibling.url}"${classAttr}>${prevSibling.title || prevSibling.name}</a>`
|
|
398
|
+
: '';
|
|
399
|
+
page.meta.next_page = nextSibling
|
|
400
|
+
? `<a href="${nextSibling.url}"${classAttr}>${nextSibling.title || nextSibling.name}</a>`
|
|
401
|
+
: '';
|
|
402
|
+
} else {
|
|
403
|
+
page.meta.prev_page = '';
|
|
404
|
+
page.meta.next_page = '';
|
|
405
|
+
}
|
|
406
|
+
|
|
300
407
|
// Run independent link generation in parallel
|
|
408
|
+
// Use visiblePages for paginated folders (first page only shows its chunk)
|
|
409
|
+
const childPages = page.visiblePages || page.pages;
|
|
301
410
|
const [links_to_children, links_to_siblings, links_to_self_and_siblings, nav_links] =
|
|
302
411
|
await Promise.all([
|
|
303
|
-
|
|
304
|
-
? generateLinkList(page.filename,
|
|
412
|
+
childPages
|
|
413
|
+
? generateLinkList(page.filename, childPages)
|
|
305
414
|
: Promise.resolve(""),
|
|
306
415
|
generateLinkList(
|
|
307
416
|
page.parent?.filename || "pages",
|
|
@@ -311,15 +420,24 @@ const addLinks = async (pages, parent) => {
|
|
|
311
420
|
generateLinkList("nav", pageIndex.filter((p) => p.nav)),
|
|
312
421
|
]);
|
|
313
422
|
|
|
314
|
-
page.
|
|
315
|
-
page.
|
|
316
|
-
page.
|
|
317
|
-
page.
|
|
423
|
+
page.meta.links_to_children = links_to_children;
|
|
424
|
+
page.meta.links_to_siblings = links_to_siblings;
|
|
425
|
+
page.meta.links_to_self_and_siblings = links_to_self_and_siblings;
|
|
426
|
+
page.meta.nav_links = nav_links;
|
|
318
427
|
|
|
319
428
|
if (page.pages) {
|
|
320
429
|
await addLinks(page.pages, page);
|
|
321
430
|
}
|
|
322
|
-
|
|
431
|
+
|
|
432
|
+
// Process pagination pages - inherit breadcrumbs and nav_links
|
|
433
|
+
if (page.paginatedPages) {
|
|
434
|
+
for (const pp of page.paginatedPages) {
|
|
435
|
+
pp.meta.breadcrumbs = page.meta.breadcrumbs;
|
|
436
|
+
pp.meta.nav_links = nav_links;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
getBuildConcurrency(),
|
|
323
441
|
);
|
|
324
442
|
};
|
|
325
443
|
|