Add sitemap generation utility
This commit is contained in:
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
import type { BunextGenerateSitemapParams, BunextSitemapEntry } from "../types";
|
||||
/**
|
||||
* # 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 function generateSitemap(params?: BunextGenerateSitemapParams): Promise<string>;
|
||||
export declare function grabSitemapEntries(params?: BunextGenerateSitemapParams): Promise<BunextSitemapEntry[]>;
|
||||
Vendored
+165
@@ -0,0 +1,165 @@
|
||||
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 = [` <loc>${escapeXml(loc)}</loc>`];
|
||||
if (entry.lastmod) {
|
||||
const lastmod = entry.lastmod instanceof Date
|
||||
? entry.lastmod.toISOString()
|
||||
: entry.lastmod;
|
||||
lines.push(` <lastmod>${escapeXml(lastmod)}</lastmod>`);
|
||||
}
|
||||
if (entry.changefreq) {
|
||||
lines.push(` <changefreq>${entry.changefreq}</changefreq>`);
|
||||
}
|
||||
if (typeof entry.priority == "number") {
|
||||
const priority = Math.min(Math.max(entry.priority, 0), 1);
|
||||
lines.push(` <priority>${priority.toFixed(1)}</priority>`);
|
||||
}
|
||||
return ` <url>\n${lines.join("\n")}\n </url>`;
|
||||
});
|
||||
return [
|
||||
`<?xml version="1.0" encoding="UTF-8"?>`,
|
||||
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`,
|
||||
...urls,
|
||||
`</urlset>`,
|
||||
``,
|
||||
].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, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
Reference in New Issue
Block a user