UNPKG

sitemap-ts

Version:

Sitemap generator for TypeScript projects

152 lines (144 loc) 5.7 kB
import { writeFileSync } from 'node:fs'; import { SitemapStream, streamToPromise } from 'sitemap'; import format from 'xml-formatter'; import { ensurePrefix, slash } from '@antfu/utils'; import { isAbsolute, resolve, parse, join } from 'node:path'; import fg from 'fast-glob'; const defaultOptions = { hostname: "http://localhost/", dynamicRoutes: [], exclude: [], externalSitemaps: [], basePath: "", outDir: "dist", extensions: "html", changefreq: "daily", priority: 1, lastmod: /* @__PURE__ */ new Date(), readable: false, generateRobotsTxt: true, robots: [{ userAgent: "*", allow: "/" }] }; function resolveOptions(userOptions) { return Object.assign( {}, defaultOptions, userOptions ); } function getResolvedPath(file, resolvedOptions) { if (isAbsolute(resolvedOptions.outDir)) return resolve(`${resolvedOptions.outDir}/${file}`); return resolve(`${ensurePrefix("./", resolvedOptions.outDir)}/${file}`); } function removeMaybeSuffix(suffix, str) { if (!str.endsWith(suffix)) return str; return str.slice(0, -suffix.length); } var RobotCorrespondences = /* @__PURE__ */ ((RobotCorrespondences2) => { RobotCorrespondences2["userAgent"] = "User-agent"; RobotCorrespondences2["allow"] = "Allow"; RobotCorrespondences2["disallow"] = "Disallow"; RobotCorrespondences2["crawlDelay"] = "Crawl-delay"; RobotCorrespondences2["cleanParam"] = "Clean-param"; return RobotCorrespondences2; })(RobotCorrespondences || {}); function getRules(options) { const rules = []; options.forEach((rule) => { const keys = Object.keys(RobotCorrespondences).filter((key) => typeof rule[key] !== "undefined"); keys.forEach((key) => { const values = Array.isArray(rule[key]) ? rule[key] : [rule[key]]; values.forEach((value) => { rules.push({ key: RobotCorrespondences[key], value }); }); }); }); return rules; } function getContent(rules, hostname, externalSitemaps) { return rules.map((rule) => `${rule.key}: ${String(rule.value).trim()}`).join("\n").concat(` Sitemap: ${getFinalSitemapPath(hostname)}`).concat(externalSitemaps.map((s) => ` Sitemap: ${s.startsWith("http") ? s : getFinalSitemapPath(hostname, s)}`).join("")); } function getFinalSitemapPath(hostname, file = "/sitemap.xml") { return `${removeMaybeSuffix("/", hostname)}${ensurePrefix("/", file)}`; } function getRoutes(options) { const ext = typeof options.extensions === "string" ? [options.extensions] : options.extensions; const strExt = ext.map((e) => `**/*.${e}`); return [ ...fg.sync(strExt, { cwd: options.outDir }).map((route) => { let r = route; ext.forEach((e) => { const regex = new RegExp(`index.${e}`, "g"); r = r.replace(regex, ""); }); const parsedRoute = parse(r); return slash(join("/", parsedRoute.dir, parsedRoute.name)); }), ...options.dynamicRoutes.map((route) => slash(join("/", join(parse(route).dir, parse(route).name)))) ].filter((route) => !options.exclude.includes(route)); } function getOptionByRoute(options, route) { if (options instanceof Date || typeof options === "string" || typeof options === "number") return options; const givenRoutes = Object.keys(options); if (givenRoutes.includes(route)) return options[route]; if (givenRoutes.includes("*")) return options["*"]; return void 0; } function getFormattedSitemap(options, routes) { return routes.map((route) => { const hostNamePath = removeMaybeSuffix("/", options.hostname); const routePath = options.basePath ? ensurePrefix("/", options.basePath) + ensurePrefix("/", route) : ensurePrefix("/", route); const url = new URL(routePath, hostNamePath).href; const formattedSitemap = { url, changefreq: getOptionByRoute(options.changefreq, route) ?? defaultOptions.changefreq, priority: getOptionByRoute(options.priority, route) ?? defaultOptions.priority, lastmod: getOptionByRoute(options.lastmod, route) ?? defaultOptions.lastmod }; if (options.i18n) { const strategy = options.i18n.strategy ?? "suffix"; const languages = options.i18n.languages.map((str) => ({ lang: str, url: str === options.i18n?.defaultLanguage ? url : new URL(strategy === "prefix" ? ensurePrefix("/", str) + routePath : removeMaybeSuffix("/", routePath) + ensurePrefix("/", str), hostNamePath).href })); return Object.assign(formattedSitemap, { links: options.i18n.defaultLanguage ? [...languages, { lang: "x-default", url }] : languages }); } return formattedSitemap; }); } function generateSitemap(options = {}) { const resolvedOptions = resolveOptions(options); if (resolvedOptions.generateRobotsTxt) { const robotRules = getRules(resolvedOptions.robots); const robotContent = getContent(robotRules, resolvedOptions.hostname, resolvedOptions.externalSitemaps); writeFileSync(getResolvedPath("robots.txt", resolvedOptions), robotContent); } const routes = getRoutes(resolvedOptions); if (!routes.length) return; const formattedSitemap = getFormattedSitemap(resolvedOptions, routes); const stream = new SitemapStream({ xmlns: resolvedOptions.xmlns }); formattedSitemap.forEach((item) => stream.write(item)); streamToPromise(stream).then((sitemap) => { const utfSitemap = sitemap.toString("utf-8"); const formattedSitemap2 = resolvedOptions.readable ? format(utfSitemap) : utfSitemap; writeFileSync(getResolvedPath("sitemap.xml", resolvedOptions), formattedSitemap2); }); stream.end(); } export { generateSitemap as default, generateSitemap, getFormattedSitemap, getRoutes, resolveOptions };