import { statSync } from "fs"; import path from "path"; import grabPageServerPath from "../functions/server/web-pages/grab-page-server-path"; import grabAllPages from "./grab-all-pages"; import grabDirNames from "./grab-dir-names"; import grabOrigin from "./grab-origin"; import grabRelativePath from "./grab-relative-path"; import isDevelopment from "./is-development"; const ErrorPagePaths = ["/404", "/500"]; /** * # Generate a sitemap.xml string for all pages * Dynamic routes are only included through a `sitemap` export in their * `.server.ts` file, or through `additional_paths`. */ export default async function generateSitemap(params) { const entries = await grabSitemapEntries(params); const origin = (params?.origin || grabOrigin()).replace(/\/+$/, ""); const urls = entries.map((entry) => { const loc = entry.path.match(/^https?:\/\//) ? entry.path : `${origin}${entry.path}`; const lines = [` ${escapeXml(loc)}`]; if (entry.lastmod) { const lastmod = entry.lastmod instanceof Date ? entry.lastmod.toISOString() : entry.lastmod; lines.push(` ${escapeXml(lastmod)}`); } if (entry.changefreq) { lines.push(` ${entry.changefreq}`); } if (typeof entry.priority == "number") { const priority = Math.min(Math.max(entry.priority, 0), 1); lines.push(` ${priority.toFixed(1)}`); } return ` \n${lines.join("\n")}\n `; }); return [ ``, ``, ...urls, ``, ``, ].join("\n"); } export async function grabSitemapEntries(params) { const use_file_lastmod = params?.use_file_lastmod ?? true; const entries = []; const pages = grabAllPages({ exclude_api: true }); for (let i = 0; i < pages.length; i++) { const page = pages[i]; if (!page.local_path) continue; const page_path = page.url_path.replace(/\/index$/, "") || "/"; if (ErrorPagePaths.includes(page_path)) continue; const page_sitemap_paths = await grabPageSitemapPaths({ file_path: page.local_path, route: page_path, }); if (page_sitemap_paths) { entries.push(...page_sitemap_paths.map(toSitemapEntry)); continue; } if (page.url_path.match(/\[/)) continue; entries.push({ path: page_path, lastmod: use_file_lastmod ? statSync(page.local_path).mtime : undefined, }); } const additional_paths = typeof params?.additional_paths == "function" ? await params.additional_paths() : params?.additional_paths; entries.push(...(additional_paths || []).map(toSitemapEntry)); const seen = new Set(); const final_entries = []; for (const entry of entries) { const path = normalizePath(entry.path, params?.trailing_slash); if (seen.has(path)) continue; if (isExcluded(path, params?.exclude)) continue; seen.add(path); final_entries.push({ changefreq: params?.default_changefreq, priority: params?.default_priority, ...entry, path, }); } return final_entries; } /** * # Run the `sitemap` export of a page's server file, if any */ async function grabPageSitemapPaths({ file_path, route, }) { const { server_file_path } = grabPageServerPath({ file_path }); if (!server_file_path) return; const { ROOT_DIR } = grabDirNames(); const server_ctx = global.BUNEXT_SSR_BUNDLER_CTX_MAP?.[grabRelativePath(server_file_path)]; const final_server_path = server_ctx?.local_path ? path.join(ROOT_DIR, server_ctx.path) : server_file_path; // Bust the import cache in development so edits to the server file // are picked up without a restart. const server_module = await import(isDevelopment() ? `${final_server_path}?t=${Date.now()}` : final_server_path); if (typeof server_module.sitemap != "function") return; try { return await server_module.sitemap({ route }); } catch (error) { throw new Error(`\`sitemap\` export of ${grabRelativePath(server_file_path)} failed: ${error?.message || error}`); } } function toSitemapEntry(path) { return typeof path == "string" ? { path } : path; } function normalizePath(path, trailing_slash) { if (path.match(/^https?:\/\//)) return path; let new_path = `/${path}`.replace(/\/{2,}/g, "/"); if (new_path != "/") new_path = new_path.replace(/\/$/, ""); if (trailing_slash && new_path != "/") new_path += "/"; return new_path; } function isExcluded(path, exclude) { if (!exclude?.length) return false; const bare_path = path.replace(/^https?:\/\/[^/]+/, "").replace(/(.)\/$/, "$1") || "/"; for (const pattern of exclude) { if (pattern instanceof RegExp) { if (pattern.test(path) || pattern.test(bare_path)) return true; continue; } const normalized = normalizePath(pattern.replace(/\/\*$/, "")); if (pattern.endsWith("/*")) { if (bare_path == normalized || bare_path.startsWith(`${normalized.replace(/\/$/, "")}/`)) { return true; } } else if (bare_path == normalized) { return true; } } return false; } function escapeXml(value) { return value .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); }