@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/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
- const templateContent = await fs.readFile(templatePath, 'utf-8');
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
- let protectionSequence = 0;
2
-
3
- const protectBlocks = (content, pattern) => {
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 values = protectCssValues(css);
6
+ const result = new CleanCSS({
7
+ level: 1,
8
+ rebase: false,
9
+ }).minify(css);
97
10
 
98
- const minified = values.content
99
- .replace(/\/\*[\s\S]*?\*\//g, "")
100
- .replace(/\s+/g, " ")
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 values.restore(minified);
15
+ return result.styles;
106
16
  };
107
17
 
108
- const minifyJs = (js) => {
109
- const strings = protectBlocks(js, /(["'`])(?:\\[\s\S]|(?!\1)[^\\])*\1/g);
110
-
111
- const minified = strings.content
112
- .split("\n")
113
- .map((line) => line.trim())
114
- .filter(Boolean)
115
- .join("\n");
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 strings.restore(minified);
27
+ return result.code || "";
118
28
  };
119
29
 
120
- const minifyHtml = (html) => {
121
- const blocks = protectBlocks(
122
- html,
123
- /<(pre|code|textarea|script|style)\b[^>]*>[\s\S]*?<\/\1>/gi,
124
- );
125
- const strings = protectBlocks(
126
- blocks.content,
127
- /(["'])(?:\\[\s\S]|(?!\1)[^\\])*\1/g,
128
- );
129
-
130
- const minified = strings.content
131
- .replace(/<!--(?!\[if|<!|>)[\s\S]*?-->/g, "")
132
- .replace(/\s+/g, " ")
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
- const markdownContent = await fs.readFile(filePath, "utf-8");
53
- const { data, content } = matter(markdownContent);
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
- url: root ? "/" : finalPath,
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: dirConfig.page_count || page.meta.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 (err) {
299
- console.error("Error reading directory:", err);
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
- url: "/tags",
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
- url: `/tags/${tag}`,
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
- const replacedContent = await replacePlaceholders(page.content, page);
376
- const htmlContent = marked.parse(replacedContent); // Markdown processed once
377
- const wrappedContent = await applyLayoutAndWrapContent(page, htmlContent);
378
- // Use function to avoid $` special replacement patterns in content
379
- const template = await getTemplate();
380
- const htmlWithTemplate = template.replace(
381
- /<%=\s*content\s*%>/g,
382
- () => wrappedContent,
383
- );
384
- const finalContent = await replacePlaceholders(htmlWithTemplate, page);
385
- return finalContent;
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
- const html = minificationEnabled() ? minifyHtml(renderedHtml) : renderedHtml;
394
- const pageDir = page.notFound ? distDir : path.join(distDir, page.url);
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.url, "index.html");
398
- await fsExtra.ensureDir(pageDir);
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="/tags/${tag}">${tag}</a>`,
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
- console.warn(`Include "${partialName}" not found.`);
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
- if (!url || !url.startsWith("/images/")) return false;
144
- return imageExtensionRegex.test(url);
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
- console.warn(`Eta template error: ${error.message}`);
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}/${folder}/rss.xml`;
44
- const feedLink = `${siteUrl}/${folder}`;
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.url && page.url.startsWith(`/${folderName}/`) && !page.folder) {
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(`Sitemap: ${siteUrl.replace(/\/$/, "")}/sitemap.xml`);
73
+ lines.push(
74
+ `Sitemap: ${siteUrl.replace(/\/$/, "")}${withBasePath("/sitemap.xml")}`,
75
+ );
73
76
  }
74
77
  return `${lines.join("\n")}\n`;
75
78
  };