@daz4126/swifty 2.11.0 → 3.0.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 +15 -4
- package/package.json +3 -2
- package/src/assets.js +116 -46
- package/src/build.js +57 -1
- package/src/cli.js +51 -7
- package/src/client/IDIOMORPH-LICENSE.txt +13 -0
- package/src/client/idiomorph.esm.js +1385 -0
- package/src/client/swifty-navigation.js +399 -0
- package/src/concurrency.js +25 -0
- package/src/config.js +36 -2
- package/src/data.js +70 -0
- package/src/init.js +39 -33
- package/src/layout.js +16 -11
- package/src/pages.js +231 -119
- 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 +36 -9
package/src/layout.js
CHANGED
|
@@ -7,6 +7,15 @@ import { getCssImports, getJsImports, getCssPreloads, getJsPreloads } from "./as
|
|
|
7
7
|
const layoutCache = new Map();
|
|
8
8
|
let template = null;
|
|
9
9
|
|
|
10
|
+
const escapeAttr = (value) =>
|
|
11
|
+
String(value || "")
|
|
12
|
+
.replace(/&/g, "&")
|
|
13
|
+
.replace(/"/g, """)
|
|
14
|
+
.replace(/</g, "<")
|
|
15
|
+
.replace(/>/g, ">");
|
|
16
|
+
|
|
17
|
+
const navigationEnabled = () => defaultConfig.morphing !== false;
|
|
18
|
+
|
|
10
19
|
const getLayout = async (layoutName) => {
|
|
11
20
|
if (!layoutName) return null;
|
|
12
21
|
if (!layoutCache.has(layoutName)) {
|
|
@@ -30,17 +39,13 @@ const createTemplate = async () => {
|
|
|
30
39
|
const preconnectHints = [
|
|
31
40
|
'<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>',
|
|
32
41
|
'<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">',
|
|
33
|
-
...(defaultConfig.turbo ? [
|
|
34
|
-
'<link rel="preconnect" href="https://esm.sh" crossorigin>',
|
|
35
|
-
'<link rel="dns-prefetch" href="https://esm.sh">',
|
|
36
|
-
] : []),
|
|
37
42
|
].join('\n');
|
|
38
43
|
|
|
39
|
-
const
|
|
40
|
-
? `<script type="module"
|
|
44
|
+
const navigationScript = navigationEnabled()
|
|
45
|
+
? `<script type="module" src="/swifty/swifty-navigation.js" data-swifty-navigation data-target="${escapeAttr(defaultConfig.morph_target || "main")}" data-prefetching="${defaultConfig.prefetching === false ? "off" : "intent"}" data-cache-size="${escapeAttr(defaultConfig.navigation_cache_size || 20)}" data-cache-ttl="${escapeAttr(defaultConfig.navigation_cache_ttl || 15)}"></script>`
|
|
41
46
|
: '';
|
|
42
47
|
const livereloadScript = process.env.SWIFTY_WATCH
|
|
43
|
-
? `<script>document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] + '
|
|
48
|
+
? `<script>document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] + ':${defaultConfig.livereload_port || 35729}/livereload.js?snipver=1"></' + 'script>')</script>`
|
|
44
49
|
: '';
|
|
45
50
|
|
|
46
51
|
// Preload local assets for faster loading
|
|
@@ -54,15 +59,15 @@ const createTemplate = async () => {
|
|
|
54
59
|
const highlightCSS = `<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/monokai-sublime.min.css">`;
|
|
55
60
|
|
|
56
61
|
// Order: preconnect hints -> preloads -> actual assets -> scripts
|
|
57
|
-
const template = templateContent.replace('</head>', `${preconnectHints}\n${preloads}\n${
|
|
62
|
+
const template = templateContent.replace('</head>', `${preconnectHints}\n${preloads}\n${navigationScript}\n${highlightCSS}\n${imports}\n${livereloadScript}\n</head>`);
|
|
58
63
|
return template;
|
|
59
64
|
};
|
|
60
65
|
|
|
61
66
|
const applyLayoutAndWrapContent = async (page,content) => {
|
|
62
|
-
const layoutContent = await getLayout(page.
|
|
67
|
+
const layoutContent = await getLayout(page.meta.layout !== undefined ? page.meta.layout : page.layout);
|
|
63
68
|
if (!layoutContent) return content;
|
|
64
69
|
// Use function to avoid $` special replacement patterns in content
|
|
65
|
-
return layoutContent.replace(
|
|
70
|
+
return layoutContent.replace(/<%=\s*content\s*%>/g, () => content);
|
|
66
71
|
};
|
|
67
72
|
|
|
68
73
|
const getTemplate = async () => {
|
|
@@ -77,4 +82,4 @@ const resetCaches = async () => {
|
|
|
77
82
|
template = await createTemplate();
|
|
78
83
|
};
|
|
79
84
|
|
|
80
|
-
export { getLayout, applyLayoutAndWrapContent, getTemplate, resetCaches };
|
|
85
|
+
export { getLayout, applyLayoutAndWrapContent, getTemplate, resetCaches };
|
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,93 +63,102 @@ 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
|
-
|
|
133
|
-
// For directories, we defer recursion separately
|
|
134
|
-
return { page, isDirectory };
|
|
135
|
-
});
|
|
136
147
|
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
|
146
160
|
// Skip draft pages in production builds
|
|
147
|
-
if (page.
|
|
161
|
+
if (page.meta.draft && !process.env.SWIFTY_WATCH) return;
|
|
148
162
|
|
|
149
163
|
// Skip scheduled pages (future date) in production builds
|
|
150
164
|
if (page.dateObj && page.dateObj > new Date() && !process.env.SWIFTY_WATCH) return;
|
|
@@ -152,22 +166,80 @@ const generatePages = async (sourceDir, baseDir = sourceDir, parent) => {
|
|
|
152
166
|
if (isDirectory) {
|
|
153
167
|
page.pages = await generatePages(page.filePath, baseDir, page);
|
|
154
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
|
+
|
|
155
179
|
page.pages.sort((a, b) => {
|
|
156
|
-
if (a.
|
|
157
|
-
return a.
|
|
180
|
+
if (a.meta.position && b.meta.position) {
|
|
181
|
+
return a.meta.position - b.meta.position;
|
|
158
182
|
}
|
|
159
183
|
const dateA = a.dateObj || new Date(a.created_at);
|
|
160
184
|
const dateB = b.dateObj || new Date(b.created_at);
|
|
161
|
-
const sortOrder = (
|
|
185
|
+
const sortOrder = (mergedConfig.date_sort_order || "desc").toLowerCase();
|
|
162
186
|
return sortOrder === "asc" ? dateA - dateB : dateB - dateA;
|
|
163
187
|
});
|
|
164
188
|
|
|
165
|
-
|
|
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
|
+
}
|
|
166
238
|
}
|
|
167
239
|
|
|
168
240
|
// Add tags
|
|
169
|
-
if (page.
|
|
170
|
-
page.
|
|
241
|
+
if (page.meta.tags) {
|
|
242
|
+
page.meta.tags.forEach((tag) => addToTagMap(tag, page));
|
|
171
243
|
}
|
|
172
244
|
|
|
173
245
|
pages.push(page);
|
|
@@ -175,10 +247,7 @@ const generatePages = async (sourceDir, baseDir = sourceDir, parent) => {
|
|
|
175
247
|
pageIndexUrls.add(page.url);
|
|
176
248
|
pageIndex.push({ url: page.url, title: page.title, nav: page.nav });
|
|
177
249
|
}
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
// Await all directory recursion
|
|
181
|
-
await Promise.all(directoryPromises);
|
|
250
|
+
}, getBuildConcurrency());
|
|
182
251
|
} catch (err) {
|
|
183
252
|
console.error("Error reading directory:", err);
|
|
184
253
|
}
|
|
@@ -197,7 +266,7 @@ const generatePages = async (sourceDir, baseDir = sourceDir, parent) => {
|
|
|
197
266
|
undefined,
|
|
198
267
|
defaultConfig.dateFormat,
|
|
199
268
|
),
|
|
200
|
-
|
|
269
|
+
meta: { ...config },
|
|
201
270
|
pages: [],
|
|
202
271
|
};
|
|
203
272
|
|
|
@@ -211,7 +280,7 @@ const generatePages = async (sourceDir, baseDir = sourceDir, parent) => {
|
|
|
211
280
|
),
|
|
212
281
|
url: `/tags/${tag}`,
|
|
213
282
|
layout: tagLayout ? "tags" : defaultConfig.default_layout_name,
|
|
214
|
-
|
|
283
|
+
meta: { ...config, title: `Pages tagged with ${capitalize(tag)}` },
|
|
215
284
|
};
|
|
216
285
|
page.content = await generateLinkList("tags", pagesForTag);
|
|
217
286
|
|
|
@@ -224,24 +293,25 @@ const generatePages = async (sourceDir, baseDir = sourceDir, parent) => {
|
|
|
224
293
|
};
|
|
225
294
|
|
|
226
295
|
const generateLinkList = async (name, pages) => {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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(
|
|
231
306
|
defaultConfig.default_link_name || "links",
|
|
232
307
|
);
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
fileExists ? partialPath : linksPath,
|
|
241
|
-
"utf-8",
|
|
242
|
-
);
|
|
243
|
-
const content = await Promise.all(
|
|
244
|
-
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(),
|
|
245
315
|
);
|
|
246
316
|
return content.join("\n");
|
|
247
317
|
} else {
|
|
@@ -261,7 +331,7 @@ const render = async (page) => {
|
|
|
261
331
|
// Use function to avoid $` special replacement patterns in content
|
|
262
332
|
const template = await getTemplate();
|
|
263
333
|
const htmlWithTemplate = template.replace(
|
|
264
|
-
|
|
334
|
+
/<%=\s*content\s*%>/g,
|
|
265
335
|
() => wrappedContent,
|
|
266
336
|
);
|
|
267
337
|
const finalContent = await replacePlaceholders(htmlWithTemplate, page);
|
|
@@ -269,45 +339,78 @@ const render = async (page) => {
|
|
|
269
339
|
};
|
|
270
340
|
|
|
271
341
|
const createPages = async (pages, distDir = dirs.dist) => {
|
|
272
|
-
await
|
|
273
|
-
pages
|
|
342
|
+
await mapLimit(
|
|
343
|
+
pages,
|
|
344
|
+
async (page) => {
|
|
274
345
|
const html = await render(page);
|
|
275
346
|
const pageDir = path.join(distDir, page.url);
|
|
276
347
|
const pagePath = path.join(distDir, page.url, "index.html");
|
|
277
348
|
await fsExtra.ensureDir(pageDir);
|
|
278
349
|
await fs.writeFile(pagePath, html);
|
|
279
350
|
if (page.folder) {
|
|
280
|
-
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
|
+
}
|
|
281
356
|
}
|
|
282
|
-
}
|
|
357
|
+
},
|
|
358
|
+
getBuildConcurrency(),
|
|
283
359
|
);
|
|
284
360
|
};
|
|
285
361
|
|
|
286
362
|
const addLinks = async (pages, parent) => {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
|
292
372
|
.map(
|
|
293
373
|
(tag) =>
|
|
294
374
|
`<a class="${defaultConfig.tag_class}" href="/tags/${tag}">${tag}</a>`,
|
|
295
375
|
)
|
|
296
376
|
.join("")
|
|
297
377
|
: "";
|
|
378
|
+
const crumbTitle = page.meta.title || page.title || page.name;
|
|
298
379
|
const crumb = page.root
|
|
299
380
|
? ""
|
|
300
|
-
: ` ${defaultConfig.breadcrumb_separator} <a class="${defaultConfig.breadcrumb_class}" href="${page.url}">${
|
|
301
|
-
page.
|
|
302
|
-
? 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
|
|
303
384
|
: `<a class="${defaultConfig.breadcrumb_class}" href="/">Home</a>` +
|
|
304
385
|
crumb;
|
|
305
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
|
+
|
|
306
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;
|
|
307
410
|
const [links_to_children, links_to_siblings, links_to_self_and_siblings, nav_links] =
|
|
308
411
|
await Promise.all([
|
|
309
|
-
|
|
310
|
-
? generateLinkList(page.filename,
|
|
412
|
+
childPages
|
|
413
|
+
? generateLinkList(page.filename, childPages)
|
|
311
414
|
: Promise.resolve(""),
|
|
312
415
|
generateLinkList(
|
|
313
416
|
page.parent?.filename || "pages",
|
|
@@ -317,15 +420,24 @@ const addLinks = async (pages, parent) => {
|
|
|
317
420
|
generateLinkList("nav", pageIndex.filter((p) => p.nav)),
|
|
318
421
|
]);
|
|
319
422
|
|
|
320
|
-
page.
|
|
321
|
-
page.
|
|
322
|
-
page.
|
|
323
|
-
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;
|
|
324
427
|
|
|
325
428
|
if (page.pages) {
|
|
326
429
|
await addLinks(page.pages, page);
|
|
327
430
|
}
|
|
328
|
-
|
|
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(),
|
|
329
441
|
);
|
|
330
442
|
};
|
|
331
443
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// pagination.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Split array into chunks of specified size
|
|
5
|
+
* @param {Array} items - Items to paginate
|
|
6
|
+
* @param {number} pageSize - Items per page
|
|
7
|
+
* @returns {Array[]} - Array of page chunks
|
|
8
|
+
*/
|
|
9
|
+
export const chunkPages = (items, pageSize) => {
|
|
10
|
+
const chunks = [];
|
|
11
|
+
for (let i = 0; i < items.length; i += pageSize) {
|
|
12
|
+
chunks.push(items.slice(i, i + pageSize));
|
|
13
|
+
}
|
|
14
|
+
return chunks;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate pagination navigation HTML
|
|
19
|
+
* @param {object} options - Pagination options
|
|
20
|
+
* @param {number} options.currentPage - Current page number (1-indexed)
|
|
21
|
+
* @param {number} options.totalPages - Total number of pages
|
|
22
|
+
* @param {string} options.baseUrl - Base URL for the paginated folder (with trailing slash)
|
|
23
|
+
* @param {object} options.config - Config with CSS class names
|
|
24
|
+
* @returns {string} - HTML for pagination navigation
|
|
25
|
+
*/
|
|
26
|
+
export const generatePaginationNav = ({ currentPage, totalPages, baseUrl, config = {} }) => {
|
|
27
|
+
if (totalPages <= 1) return '';
|
|
28
|
+
|
|
29
|
+
const paginationClass = config.pagination_class || 'swifty_pagination';
|
|
30
|
+
const linkClass = config.pagination_link_class || 'swifty_pagination_link';
|
|
31
|
+
const currentClass = config.pagination_current_class || 'swifty_pagination_current';
|
|
32
|
+
|
|
33
|
+
const links = [];
|
|
34
|
+
|
|
35
|
+
// Previous link
|
|
36
|
+
if (currentPage > 1) {
|
|
37
|
+
const prevUrl = currentPage === 2 ? baseUrl : `${baseUrl}page/${currentPage - 1}/`;
|
|
38
|
+
links.push(`<a href="${prevUrl}" class="${linkClass} swifty_pagination_prev">« Previous</a>`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Page numbers
|
|
42
|
+
for (let i = 1; i <= totalPages; i++) {
|
|
43
|
+
const pageUrl = i === 1 ? baseUrl : `${baseUrl}page/${i}/`;
|
|
44
|
+
if (i === currentPage) {
|
|
45
|
+
links.push(`<span class="${currentClass}">${i}</span>`);
|
|
46
|
+
} else {
|
|
47
|
+
links.push(`<a href="${pageUrl}" class="${linkClass}">${i}</a>`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Next link
|
|
52
|
+
if (currentPage < totalPages) {
|
|
53
|
+
const nextUrl = `${baseUrl}page/${currentPage + 1}/`;
|
|
54
|
+
links.push(`<a href="${nextUrl}" class="${linkClass} swifty_pagination_next">Next »</a>`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return `<nav class="${paginationClass}">${links.join(' ')}</nav>`;
|
|
58
|
+
};
|