@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/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, "&lt;")
15
+ .replace(/>/g, "&gt;");
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 turboScript = defaultConfig.turbo
40
- ? `<script type="module">import * as Turbo from 'https://esm.sh/@hotwired/turbo';</script>`
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] + ':35729/livereload.js?snipver=1"></' + 'script>')</script>`
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${turboScript}\n${highlightCSS}\n${imports}\n${livereloadScript}\n</head>`);
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.data.layout !== undefined ? page.data.layout : page.layout);
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(/\{\{\s*content\s*\}\}/g, () => content);
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?.data, ...folderConfig };
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
- // Collect promises for processing all files
68
- const filePromises = files.map(async (file) => {
69
- const filePath = path.join(sourceDir, file.name);
70
- const stats = await getValidStats(filePath);
71
- if (!stats) return null;
72
-
73
- const root = file.name === "index.md" && !parent;
74
- const relativePath = path.relative(baseDir, filePath).replace(/\\/g, "/");
75
- const finalPath = `/${relativePath.replace(/\.md$/, "")}`;
76
- const name = root
77
- ? "Home"
78
- : capitalize(file.name.replace(/\.md$/, "").replace(/-/g, " "));
79
- const isDirectory = stats.isDirectory();
80
- const layoutFileExists =
81
- parent &&
82
- (await fsExtra.pathExists(`${dirs.layouts}/${parent.filename}.html`));
83
- const layout = layoutFileExists
84
- ? parent.filename
85
- : parent
86
- ? parent.layout
87
- : config.default_layout_name;
88
-
89
- const page = {
90
- name,
91
- root,
92
- layout,
93
- filePath,
94
- filename: file.name.replace(/\.md$/, ""),
95
- url: root ? "/" : finalPath,
96
- nav: !parent && !root,
97
- parent: parent
98
- ? { title: parent.data.title, url: parent.url }
99
- : undefined,
100
- folder: isDirectory,
101
- title: name,
102
- created_at: new Date(stats.birthtime).toLocaleDateString(
103
- undefined,
104
- dateFormat,
105
- ),
106
- updated_at: new Date(stats.mtime).toLocaleDateString(
107
- undefined,
108
- dateFormat,
109
- ),
110
- date: new Date(stats.birthtime).toLocaleDateString(
111
- undefined,
112
- dateFormat,
113
- ),
114
- data: root ? { ...defaultConfig } : { ...config },
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
- if (path.extname(file.name) === ".md") {
118
- const markdownContent = await fs.readFile(filePath, "utf-8");
119
- const { data, content } = matter(markdownContent);
120
- Object.assign(page, { data: { ...page.data, ...data }, content });
121
-
122
- // If front matter has a date, parse and format it
123
- if (data.date) {
124
- const parsedDate = parseDate(data.date);
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
- // Await all file processing
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
- const directoryPromises = fileResults.map(async (result) => {
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.data.draft && !process.env.SWIFTY_WATCH) return;
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.data.position && b.data.position) {
157
- return a.data.position - b.data.position;
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 = (config.date_sort_order || "desc").toLowerCase();
185
+ const sortOrder = (mergedConfig.date_sort_order || "desc").toLowerCase();
162
186
  return sortOrder === "asc" ? dateA - dateB : dateB - dateA;
163
187
  });
164
188
 
165
- page.content = await generateLinkList(page.filename, page.pages);
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.data.tags) {
170
- page.data.tags.forEach((tag) => addToTagMap(tag, 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
- data: { ...config },
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
- data: { ...config, title: `Pages tagged with ${capitalize(tag)}` },
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 partialFile = `${name}.md`;
228
- const partialPath = path.join(dirs.partials, partialFile);
229
- const linksPath = path.join(
230
- dirs.partials,
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
- // Check if either file exists in the 'partials' folder (in parallel)
234
- const [fileExists, defaultExists] = await Promise.all([
235
- fsExtra.pathExists(partialPath),
236
- fsExtra.pathExists(linksPath),
237
- ]);
238
- if (fileExists || defaultExists) {
239
- const partialContent = await fs.readFile(
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
- /\{\{\s*content\s*\}\}/g,
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 Promise.all(
273
- pages.map(async (page) => {
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); // Recursive still needs to await
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
- await Promise.all(
288
- pages.map(async (page) => {
289
- page.data ||= {};
290
- page.data.links_to_tags = page?.data?.tags?.length
291
- ? page.data.tags
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}">${page.name}</a>`;
301
- page.data.breadcrumbs = parent
302
- ? parent.data.breadcrumbs + crumb
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
- page.pages
310
- ? generateLinkList(page.filename, page.pages)
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.data.links_to_children = links_to_children;
321
- page.data.links_to_siblings = links_to_siblings;
322
- page.data.links_to_self_and_siblings = links_to_self_and_siblings;
323
- page.data.nav_links = nav_links;
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">&laquo; 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 &raquo;</a>`);
55
+ }
56
+
57
+ return `<nav class="${paginationClass}">${links.join(' ')}</nav>`;
58
+ };