nuxt-simple-sitemap
Version:
Powerfully flexible XML Sitemaps that integrate seamlessly, for Nuxt.
989 lines (976 loc) • 39 kB
JavaScript
import { useNuxt, loadNuxtModuleInstance, createResolver, addTemplate, extendPages, defineNuxtModule, useLogger, hasNuxtModule, getNuxtModuleVersion, hasNuxtModuleCompatibility, addServerImports, addServerPlugin, addServerHandler, findPath, addPrerenderRoutes } from '@nuxt/kit';
import { parseURL, withLeadingSlash, withBase, joinURL, withoutLeadingSlash } from 'ufo';
import { assertSiteConfig, installNuxtSiteConfig } from 'nuxt-site-config-kit';
import { defu, createDefu } from 'defu';
import { statSync, existsSync } from 'node:fs';
import { extname, relative, dirname } from 'pathe';
import { provider, env } from 'std-env';
import { mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import chalk from 'chalk';
import { build } from 'nitropack';
import { withSiteUrl } from 'nuxt-site-config-kit/urls';
import 'site-config-stack/urls';
const version = "4.4.0";
async function resolveUrls(urls) {
if (typeof urls === "function")
urls = urls();
urls = await urls;
return urls;
}
function deepForEachPage(pages, callback, fullpath = null, depth = 0) {
pages.forEach((page) => {
let currentPath;
if (page.path.startsWith("/"))
currentPath = page.path;
else
currentPath = page.path === "" ? fullpath : `${fullpath.replace(/\/$/, "")}/${page.path}`;
callback(page, currentPath || "", depth);
if (page.children)
deepForEachPage(page.children, callback, currentPath, depth + 1);
});
}
function convertNuxtPagesToSitemapEntries(pages, config) {
const routesNameSeparator = config.routesNameSeparator || "___";
let flattenedPages = [];
deepForEachPage(
pages,
(page, loc, depth) => {
flattenedPages.push({ page, loc, depth });
}
);
flattenedPages = flattenedPages.filter((page) => !page.loc.includes(":")).filter((page, idx, arr) => {
return !arr.find((p) => {
return p.loc === page.loc && p.depth > page.depth;
});
}).map((p) => {
delete p.depth;
return p;
});
const pagesWithMeta = flattenedPages.map((p) => {
if (config.autoLastmod && p.page.file) {
try {
const stats = statSync(p.page.file);
if (stats?.mtime)
p.lastmod = stats.mtime;
} catch (e) {
}
}
if (p.page?.meta?.sitemap) {
p = defu(p.page.meta.sitemap, p);
}
return p;
});
const localeGroups = {};
pagesWithMeta.reduce((acc, e) => {
if (e.page.name?.includes(routesNameSeparator)) {
const [name, locale] = e.page.name.split(routesNameSeparator);
if (!acc[name])
acc[name] = [];
const { iso, code } = config.normalisedLocales.find((l) => l.code === locale) || { iso: locale, code: locale };
acc[name].push({ ...e, _sitemap: config.isI18nMapped ? iso || code : void 0, locale });
} else {
acc.default = acc.default || [];
acc.default.push(e);
}
return acc;
}, localeGroups);
return Object.entries(localeGroups).map(([locale, entries]) => {
if (locale === "default") {
return entries.map((e) => {
const [name] = (e.page?.name || "").split(routesNameSeparator);
if (localeGroups[name]?.some((a) => a.locale === config.defaultLocale))
return false;
const defaultLocale = config.normalisedLocales.find((l) => l.code === config.defaultLocale);
if (defaultLocale && config.isI18nMapped)
e._sitemap = defaultLocale.iso || defaultLocale.code;
delete e.page;
delete e.locale;
return { ...e };
}).filter(Boolean);
}
return entries.map((entry) => {
const alternatives = entries.map((entry2) => {
const hreflang = config.normalisedLocales.find((l) => l.code === entry2.locale)?.iso || entry2.locale;
return {
hreflang,
href: entry2.loc
};
});
const xDefault = entries.find((a) => a.locale === config.defaultLocale);
if (xDefault && alternatives.length) {
alternatives.push({
hreflang: "x-default",
href: xDefault.loc
});
}
const e = { ...entry };
if (config.isI18nMapped) {
const { iso, code } = config.normalisedLocales.find((l) => l.code === entry.locale) || { iso: locale, code: locale };
e._sitemap = iso || code;
}
delete e.page;
delete e.locale;
return {
...e,
alternatives
};
});
}).filter(Boolean).flat();
}
function generateExtraRoutesFromNuxtConfig(nuxt = useNuxt()) {
const routeRules = Object.entries(nuxt.options.routeRules || {}).filter(([k, v]) => {
if (k.includes("*") || k.includes(".") || k.includes(":"))
return false;
if (typeof v.index === "boolean" && !v.index)
return false;
return !v.redirect;
}).map(([k]) => k);
const prerenderUrls = (nuxt.options.nitro.prerender?.routes || []).filter((p) => p && !extname(p) && !p.startsWith("/api/"));
return { routeRules, prerenderUrls };
}
async function getNuxtModuleOptions(module, nuxt = useNuxt()) {
const moduleMeta = (typeof module === "string" ? { name: module } : await module.getMeta?.()) || {};
const { nuxtModule } = await loadNuxtModuleInstance(module, nuxt);
let moduleEntry;
for (const m of nuxt.options.modules) {
if (Array.isArray(m) && m.length >= 2) {
const _module = m[0];
const _moduleEntryName = typeof _module === "string" ? _module : (await _module.getMeta?.())?.name || "";
if (_moduleEntryName === moduleMeta.name)
moduleEntry = m;
}
}
let inlineOptions = {};
if (moduleEntry)
inlineOptions = moduleEntry[1];
if (nuxtModule.getOptions)
return nuxtModule.getOptions(inlineOptions, nuxt);
return inlineOptions;
}
function extendTypes(module, template) {
const nuxt = useNuxt();
const { resolve } = createResolver(import.meta.url);
addTemplate({
filename: `module/${module}.d.ts`,
getContents: async () => {
const typesPath = relative(resolve(nuxt.options.rootDir, nuxt.options.buildDir, "module"), resolve("runtime/types"));
const s = await template({ typesPath });
return `// Generated by ${module}
${s}
export {}
`;
}
});
nuxt.hooks.hook("prepare:types", ({ references }) => {
references.push({ path: resolve(nuxt.options.buildDir, `module/${module}.d.ts`) });
});
}
function createPagesPromise(nuxt = useNuxt()) {
return new Promise((resolve) => {
nuxt.hooks.hook("modules:done", () => {
extendPages(resolve);
});
});
}
function createNitroPromise(nuxt = useNuxt()) {
return new Promise((resolve) => {
nuxt.hooks.hook("nitro:init", (nitro) => {
resolve(nitro);
});
});
}
const autodetectableProviders = {
azure_static: "azure",
cloudflare_pages: "cloudflare-pages",
netlify: "netlify",
stormkit: "stormkit",
vercel: "vercel",
cleavr: "cleavr",
stackblitz: "stackblitz"
};
const autodetectableStaticProviders = {
netlify: "netlify-static",
vercel: "vercel-static"
};
function detectTarget(options = {}) {
return options?.static ? autodetectableStaticProviders[provider] : autodetectableProviders[provider];
}
function resolveNitroPreset(nitroConfig) {
if (provider === "stackblitz")
return "stackblitz";
let preset;
if (nitroConfig && nitroConfig?.preset)
preset = nitroConfig.preset;
if (!preset)
preset = env.NITRO_PRESET || detectTarget() || "node-server";
return preset.replace("_", "-");
}
function extractSitemapMetaFromHtml(html, options) {
options = options || { images: true, lastmod: true, alternatives: true };
const payload = {};
if (options?.images) {
const images = /* @__PURE__ */ new Set();
const mainRegex = /<main[^>]*>([\s\S]*?)<\/main>/;
const mainMatch = mainRegex.exec(html);
if (mainMatch?.[1] && mainMatch[1].includes("<img")) {
const imgRegex = /<img[^>]+src="([^">]+)"/g;
let match;
while ((match = imgRegex.exec(mainMatch[1])) !== null) {
if (match.index === imgRegex.lastIndex)
imgRegex.lastIndex++;
let url = match[1];
if (url.startsWith("/"))
url = withSiteUrl(url);
images.add(url);
}
}
if (images.size > 0)
payload.images = [...images].map((i) => ({ loc: i }));
}
if (options?.lastmod) {
const articleModifiedTime = html.match(/<meta[^>]+property="article:modified_time"[^>]+content="([^"]+)"/)?.[1] || html.match(/<meta[^>]+content="([^"]+)"[^>]+property="article:modified_time"/)?.[1];
if (articleModifiedTime)
payload.lastmod = articleModifiedTime;
}
if (options?.alternatives) {
const alternatives = (html.match(/<link[^>]+rel="alternate"[^>]+>/g) || []).map((a) => {
const href = a.match(/href="([^"]+)"/)?.[1];
const hreflang = a.match(/hreflang="([^"]+)"/)?.[1];
return { hreflang, href: parseURL(href).pathname };
}).filter((a) => a.hreflang && a.href);
if (alternatives?.length && (alternatives.length > 1 || alternatives?.[0].hreflang !== "x-default"))
payload.alternatives = alternatives;
}
return payload;
}
const merger = createDefu((obj, key, value) => {
if (Array.isArray(obj[key]) && Array.isArray(value))
obj[key] = Array.from(/* @__PURE__ */ new Set([...obj[key], ...value]));
return obj[key];
});
function mergeOnKey(arr, key) {
const res = {};
arr.forEach((item) => {
const k = item[key];
res[k] = merger(item, res[k] || {});
});
return Object.values(res);
}
function splitForLocales(path, locales) {
const prefix = withLeadingSlash(path).split("/")[1];
if (locales.includes(prefix))
return [prefix, path.replace(`/${prefix}`, "")];
return [null, path];
}
function formatPrerenderRoute(route) {
let str = ` \u251C\u2500 ${route.route} (${route.generateTimeMS}ms)`;
if (route.error) {
const errorColor = chalk[route.error.statusCode === 404 ? "yellow" : "red"];
const errorLead = "\u2514\u2500\u2500";
str += `
\u2502 ${errorLead} ${errorColor(route.error)}`;
}
return chalk.gray(str);
}
function includesSitemapRoot(sitemapName, routes) {
return routes.includes(`/sitemap.xml`) || routes.includes(`/${sitemapName}`) || routes.includes("/sitemap_index.xml");
}
function isNuxtGenerate(nuxt = useNuxt()) {
return nuxt.options._generate || nuxt.options.nitro.static || nuxt.options.nitro.preset === "static";
}
function setupPrerenderHandler(options, nuxt = useNuxt()) {
const prerenderedRoutes = nuxt.options.nitro.prerender?.routes || [];
const prerenderSitemap = isNuxtGenerate() || includesSitemapRoot(options.sitemapName, prerenderedRoutes);
if (nuxt.options.nitro.prerender?.routes)
nuxt.options.nitro.prerender.routes = nuxt.options.nitro.prerender.routes.filter((r) => r && !includesSitemapRoot(options.sitemapName, [r]));
nuxt.hooks.hook("nitro:init", async (nitro) => {
let prerenderer;
nitro.hooks.hook("prerender:init", async (_prerenderer) => {
prerenderer = _prerenderer;
assertSiteConfig("nuxt-simple-sitemap", {
url: "Required to generate absolute canonical URLs for your sitemap."
}, { throwError: false });
});
nitro.hooks.hook("prerender:generate", async (route) => {
const html = route.contents;
if (!route.fileName?.endsWith(".html") || !html)
return;
route._sitemap = defu(route._sitemap, {
loc: route.route
});
if (options.autoI18n && Object.keys(options.sitemaps).length > 1) {
const path = route.route;
const match = splitForLocales(path, options.autoI18n.locales.map((l) => l.code));
const locale = match[0] || options.autoI18n.defaultLocale;
if (options.isI18nMapped) {
const { code, iso } = options.autoI18n.locales.find((l) => l.code === locale) || { code: locale, iso: locale };
route._sitemap._sitemap = iso || code;
}
}
route._sitemap = defu(extractSitemapMetaFromHtml(html, {
images: options.discoverImages,
// TODO configurable?
lastmod: true,
alternatives: true
}), route._sitemap);
});
nitro.hooks.hook("prerender:done", async () => {
await build(prerenderer);
const routes = [];
if (options.debug)
routes.push("/__sitemap__/debug.json");
if (prerenderSitemap) {
routes.push(
options.isMultiSitemap ? "/sitemap_index.xml" : `/${Object.keys(options.sitemaps)[0]}`
);
}
for (const route of routes)
await prerenderRoute(nitro, route);
});
});
}
async function prerenderRoute(nitro, route) {
const start = Date.now();
const _route = { route, fileName: route };
const encodedRoute = encodeURI(route);
const res = await globalThis.$fetch.raw(
withBase(encodedRoute, nitro.options.baseURL),
{
headers: { "x-nitro-prerender": encodedRoute },
retry: nitro.options.prerender.retry,
retryDelay: nitro.options.prerender.retryDelay
}
);
const header = res.headers.get("x-nitro-prerender") || "";
const prerenderUrls = [
...header.split(",").map((i) => i.trim()).map((i) => decodeURIComponent(i)).filter(Boolean)
];
const filePath = join(nitro.options.output.publicDir, _route.fileName);
await mkdir(dirname(filePath), { recursive: true });
const data = res._data;
if (filePath.endsWith("json") || typeof data === "object")
await writeFile(filePath, JSON.stringify(data), "utf8");
else
await writeFile(filePath, data, "utf8");
_route.generateTimeMS = Date.now() - start;
nitro._prerenderedRoutes.push(_route);
nitro.logger.log(formatPrerenderRoute(_route));
for (const url of prerenderUrls)
await prerenderRoute(nitro, url);
}
const DEVTOOLS_UI_ROUTE = "/__nuxt-simple-sitemap";
const DEVTOOLS_UI_LOCAL_PORT = 3030;
function setupDevToolsUI(options, resolve, nuxt = useNuxt()) {
const clientPath = resolve("./client");
const isProductionBuild = existsSync(clientPath);
if (isProductionBuild) {
nuxt.hook("vite:serverCreated", async (server) => {
const sirv = await import('sirv').then((r) => r.default || r);
server.middlewares.use(
DEVTOOLS_UI_ROUTE,
sirv(clientPath, { dev: true, single: true })
);
});
} else {
nuxt.hook("vite:extendConfig", (config) => {
config.server = config.server || {};
config.server.proxy = config.server.proxy || {};
config.server.proxy[DEVTOOLS_UI_ROUTE] = {
target: `http://localhost:${DEVTOOLS_UI_LOCAL_PORT}${DEVTOOLS_UI_ROUTE}`,
changeOrigin: true,
followRedirects: true,
rewrite: (path) => path.replace(DEVTOOLS_UI_ROUTE, "")
};
});
}
nuxt.hook("devtools:customTabs", (tabs) => {
tabs.push({
// unique identifier
name: "nuxt-simple-sitemap",
// title to display in the tab
title: "Sitemap",
// any icon from Iconify, or a URL to an image
icon: "carbon:load-balancer-application",
// iframe view
view: {
type: "iframe",
src: DEVTOOLS_UI_ROUTE
}
});
});
}
function normaliseDate(d) {
if (typeof d === "string") {
d = d.replace("Z", "");
d = d.replace(/\.\d+$/, "");
if (d.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/) || d.match(/^\d{4}-\d{2}-\d{2}$/))
return d;
d = new Date(d);
if (Number.isNaN(d.getTime()))
return false;
}
const z = (n) => `0${n}`.slice(-2);
return `${d.getUTCFullYear()}-${z(d.getUTCMonth() + 1)}-${z(d.getUTCDate())}T${z(d.getUTCHours())}:${z(d.getUTCMinutes())}:${z(d.getUTCSeconds())}+00:00`;
}
function splitPathForI18nLocales(path, autoI18n) {
const locales = autoI18n.strategy === "prefix_except_default" ? autoI18n.locales.filter((l) => l.code !== autoI18n.defaultLocale) : autoI18n.locales;
if (typeof path !== "string" || path.startsWith("/api") || path.startsWith("/_nuxt"))
return path;
const match = splitForLocales(path, locales.map((l) => l.code));
const locale = match[0];
if (locale)
return path;
return [
path,
...locales.map((l) => `/${l.code}${path}`)
];
}
function generatePathForI18nPages({ localeCode, pageLocales, nuxtI18nConfig, forcedStrategy }) {
switch (forcedStrategy ?? nuxtI18nConfig.strategy) {
case "prefix_except_default":
case "prefix_and_default":
return localeCode === nuxtI18nConfig.defaultLocale ? pageLocales : joinURL(localeCode, pageLocales);
case "prefix":
return joinURL(localeCode, pageLocales);
case "no_prefix":
default:
return pageLocales;
}
}
function isValidFilter(filter) {
if (typeof filter === "string")
return true;
if (filter instanceof RegExp)
return true;
if (typeof filter === "object" && typeof filter.regex === "string")
return true;
return false;
}
function normalizeFilters(filters) {
return (filters || []).map((filter) => {
if (!isValidFilter(filter)) {
console.warn(`[Nuxt Simple Sitemap] You have provided an invalid filter: ${filter}, ignoring.`);
return false;
}
return filter instanceof RegExp ? { regex: filter.toString() } : filter;
}).filter(Boolean);
}
const module = defineNuxtModule({
meta: {
name: "nuxt-simple-sitemap",
compatibility: {
nuxt: "^3.9.0",
bridge: false
},
configKey: "sitemap"
},
defaults: {
enabled: true,
credits: true,
cacheMaxAgeSeconds: 60 * 10,
// cache for 10 minutes
debug: false,
defaultSitemapsChunkSize: 1e3,
autoLastmod: false,
discoverImages: true,
dynamicUrlsApiEndpoint: "/api/_sitemap-urls",
urls: [],
sortEntries: true,
xsl: "/__sitemap__/style.xsl",
xslTips: true,
strictNuxtContentPaths: false,
runtimeCacheStorage: true,
sitemapName: "sitemap.xml",
// cacheControlHeader: 'max-age=600, must-revalidate',
defaults: {},
// index sitemap options filtering
include: [],
exclude: ["/_nuxt/**", "/api/**"],
// sources
sources: [],
excludeAppSources: [],
inferStaticPagesAsRoutes: true
},
async setup(config, nuxt) {
const logger = useLogger("nuxt-simple-sitemap");
logger.level = config.debug || nuxt.options.debug ? 4 : 3;
if (config.enabled === false) {
logger.debug("The module is disabled, skipping setup.");
return;
}
config.xslColumns = config.xslColumns || [
{ label: "URL", width: "50%" },
{ label: "Images", width: "25%", select: "count(image:image)" },
{
label: "Last Updated",
width: "25%",
select: "concat(substring(sitemap:lastmod,0,11),concat(' ', substring(sitemap:lastmod,12,5)),concat(' ', substring(sitemap:lastmod,20,6)))"
}
];
if (config.autoLastmod) {
config.defaults = config.defaults || {};
config.defaults.lastmod = normaliseDate(/* @__PURE__ */ new Date());
}
const { resolve } = createResolver(import.meta.url);
await installNuxtSiteConfig();
const userGlobalSources = [
...config.sources || []
];
const appGlobalSources = [];
nuxt.options.nitro.storage = nuxt.options.nitro.storage || {};
if (config.runtimeCacheStorage && !nuxt.options.dev && typeof config.runtimeCacheStorage === "object")
nuxt.options.nitro.storage["nuxt-simple-sitemap"] = config.runtimeCacheStorage;
if (!config.sitemapName.endsWith("xml")) {
const newName = `${config.sitemapName.split(".")[0]}.xml`;
logger.warn(`You have provided a \`sitemapName\` that does not end with \`.xml\`. This is not supported by search engines, renaming to \`${newName}\`.`);
config.sitemapName = newName;
}
config.sitemapName = withoutLeadingSlash(config.sitemapName);
let usingMultiSitemaps = !!config.sitemaps;
let isI18nMapped = false;
let nuxtI18nConfig = {};
let resolvedAutoI18n = typeof config.autoI18n === "boolean" ? false : config.autoI18n || false;
const hasDisabledAutoI18n = typeof config.autoI18n === "boolean" && !config.autoI18n;
let normalisedLocales = [];
if (hasNuxtModule("@nuxtjs/i18n")) {
const i18nVersion = await getNuxtModuleVersion("@nuxtjs/i18n");
if (!await hasNuxtModuleCompatibility("@nuxtjs/i18n", ">=8"))
logger.warn(`You are using @nuxtjs/i18n v${i18nVersion}. For the best compatibility, please upgrade to @nuxtjs/i18n v8.0.0 or higher.`);
nuxtI18nConfig = await getNuxtModuleOptions("@nuxtjs/i18n") || {};
normalisedLocales = mergeOnKey((nuxtI18nConfig.locales || []).map((locale) => typeof locale === "string" ? { code: locale } : locale), "code");
const usingI18nPages = Object.keys(nuxtI18nConfig.pages || {}).length;
if (usingI18nPages && !hasDisabledAutoI18n) {
const i18nPagesSources = {
context: {
name: "@nuxtjs/i18n:pages",
description: "Generated from your i18n.pages config.",
tips: [
"You can disable this with `autoI18n: false`."
]
},
urls: []
};
for (const pageLocales of Object.values(nuxtI18nConfig?.pages)) {
for (const localeCode in pageLocales) {
const locale = normalisedLocales.find((l) => l.code === localeCode);
if (!locale || !pageLocales[localeCode] || pageLocales[localeCode].includes("["))
continue;
const alternatives = Object.keys(pageLocales).map((l) => ({
hreflang: normalisedLocales.find((nl) => nl.code === l)?.iso || l,
href: generatePathForI18nPages({ localeCode: l, pageLocales: pageLocales[l], nuxtI18nConfig })
}));
if (alternatives.length && nuxtI18nConfig.defaultLocale && pageLocales[nuxtI18nConfig.defaultLocale])
alternatives.push({ hreflang: "x-default", href: generatePathForI18nPages({ localeCode: nuxtI18nConfig.defaultLocale, pageLocales: pageLocales[nuxtI18nConfig.defaultLocale], nuxtI18nConfig }) });
i18nPagesSources.urls.push({
_sitemap: locale.iso || locale.code,
loc: generatePathForI18nPages({ localeCode, pageLocales: pageLocales[localeCode], nuxtI18nConfig }),
alternatives
});
if (nuxtI18nConfig.strategy === "prefix_and_default" && localeCode === nuxtI18nConfig.defaultLocale) {
i18nPagesSources.urls.push({
_sitemap: locale.iso || locale.code,
loc: generatePathForI18nPages({ localeCode, pageLocales: pageLocales[localeCode], nuxtI18nConfig, forcedStrategy: "prefix" }),
alternatives
});
}
}
}
appGlobalSources.push(i18nPagesSources);
if (Array.isArray(config.excludeAppSources))
config.excludeAppSources.push("nuxt:pages");
} else {
if (!normalisedLocales.length)
logger.warn("You are using @nuxtjs/i18n but have not configured any locales, this will cause issues with nuxt-simple-sitemap. Please configure `locales`.");
}
const hasSetAutoI18n = typeof config.autoI18n === "object" && Object.keys(config.autoI18n).length;
const hasI18nConfigForAlternatives = nuxtI18nConfig.differentDomains || usingI18nPages || nuxtI18nConfig.strategy !== "no_prefix" && nuxtI18nConfig.locales;
if (!hasSetAutoI18n && !hasDisabledAutoI18n && hasI18nConfigForAlternatives) {
resolvedAutoI18n = {
differentDomains: nuxtI18nConfig.differentDomains,
defaultLocale: nuxtI18nConfig.defaultLocale,
locales: normalisedLocales,
strategy: nuxtI18nConfig.strategy
};
}
if (typeof config.sitemaps === "undefined" && !!resolvedAutoI18n && nuxtI18nConfig.strategy !== "no_prefix") {
config.sitemaps = { index: [] };
for (const locale of resolvedAutoI18n.locales) {
config.sitemaps[locale.iso || locale.code] = { includeAppSources: true };
}
isI18nMapped = true;
usingMultiSitemaps = true;
}
}
let needsRobotsPolyfill = true;
if (hasNuxtModule("nuxt-simple-robots")) {
const robotsVersion = await getNuxtModuleVersion("nuxt-simple-robots");
if (!await hasNuxtModuleCompatibility("nuxt-simple-robots", ">=4"))
logger.warn(`You are using nuxt-simple-robots v${robotsVersion}. For the best compatibility, please upgrade to nuxt-simple-robots v4.0.0 or higher.`);
else
needsRobotsPolyfill = false;
nuxt.hooks.hook("robots:config", (robotsConfig) => {
robotsConfig.sitemap.push(usingMultiSitemaps ? "/sitemap_index.xml" : `/${config.sitemapName}`);
});
}
if (needsRobotsPolyfill) {
addServerImports([{
name: "getPathRobotConfigPolyfill",
as: "getPathRobotConfig",
from: resolve("./runtime/nitro/composables/getPathRobotConfigPolyfill")
}]);
}
extendTypes("nuxt-simple-sitemap", async ({ typesPath }) => {
return `
declare module 'nitropack' {
interface NitroRouteRules {
index?: boolean
sitemap?: import('${typesPath}').SitemapItemDefaults
}
interface NitroRouteConfig {
index?: boolean
sitemap?: import('${typesPath}').SitemapItemDefaults
}
interface NitroRuntimeHooks {
'sitemap:resolved': (ctx: import('${typesPath}').SitemapRenderCtx) => void | Promise<void>
'sitemap:output': (ctx: import('${typesPath}').SitemapOutputHookCtx) => void | Promise<void>
}
}
declare module 'vue-router' {
interface RouteMeta {
sitemap?: import('${typesPath}').SitemapItemDefaults
}
}
`;
});
const nitroPreset = resolveNitroPreset();
const prerenderedRoutes = nuxt.options.nitro.prerender?.routes || [];
const prerenderSitemap = isNuxtGenerate() || includesSitemapRoot(config.sitemapName, prerenderedRoutes);
const routeRules = {};
nuxt.options.nitro.routeRules = nuxt.options.nitro.routeRules || {};
if (prerenderSitemap) {
routeRules.headers = {
"Content-Type": "text/xml; charset=UTF-8"
};
}
if (!nuxt.options.dev && !isNuxtGenerate() && config.cacheMaxAgeSeconds && config.runtimeCacheStorage !== false) {
routeRules[nitroPreset.includes("vercel") ? "isr" : "swr"] = config.cacheMaxAgeSeconds;
routeRules.cache = {
// handle multi-tenancy
varies: ["X-Forwarded-Host", "X-Forwarded-Proto", "Host"]
};
if (typeof config.runtimeCacheStorage === "object")
routeRules.cache.base = "nuxt-simple-sitemap";
}
nuxt.options.nitro.routeRules["/sitemap.xsl"] = {
headers: {
"Content-Type": "application/xslt+xml"
}
};
if (usingMultiSitemaps) {
nuxt.options.nitro.routeRules["/sitemap_index.xml"] = routeRules;
if (typeof config.sitemaps === "object") {
for (const k in config.sitemaps)
nuxt.options.nitro.routeRules[`/${k}-sitemap.xml`] = routeRules;
} else {
nuxt.options.nitro.routeRules[`/${config.sitemapName}`] = routeRules;
}
} else {
nuxt.options.nitro.routeRules[`/${config.sitemapName}`] = routeRules;
}
if (config.experimentalWarmUp)
addServerPlugin(resolve("./runtime/nitro/plugins/warm-up"));
if (config.experimentalCompression)
addServerPlugin(resolve("./runtime/nitro/plugins/compression"));
const isNuxtContentDocumentDriven = !!nuxt.options.content?.documentDriven || config.strictNuxtContentPaths;
if (hasNuxtModule("@nuxt/content")) {
addServerPlugin(resolve("./runtime/nitro/plugins/nuxt-content"));
addServerHandler({
route: "/__sitemap__/nuxt-content-urls.json",
handler: resolve("./runtime/routes/__sitemap__/nuxt-content-urls")
});
const tips = [];
if (nuxt.options.content?.documentDriven)
tips.push("Enabled because you're using `@nuxt/content` with `documentDriven: true`.");
else if (config.strictNuxtContentPaths)
tips.push("Enabled because you've set `config.strictNuxtContentPaths: true`.");
else
tips.push("You can provide a `sitemap` key in your markdown frontmatter to configure specific URLs. Make sure you include a `loc`.");
appGlobalSources.push({
context: {
name: "@nuxt/content:urls",
description: "Generated from your markdown files.",
tips
},
fetch: "/__sitemap__/nuxt-content-urls.json"
});
}
const hasLegacyDefaultApiSource = !!await findPath(resolve(nuxt.options.serverDir, "api/_sitemap-urls"));
if (
// make sure they didn't manually add it as a source
!config.sources?.includes("/api/_sitemap-urls") && (hasLegacyDefaultApiSource || config.dynamicUrlsApiEndpoint !== "/api/_sitemap-urls")
) {
userGlobalSources.push({
context: {
name: "dynamicUrlsApiEndpoint",
description: "Generated from your dynamicUrlsApiEndpoint config.",
tips: [
"The `dynamicUrlsApiEndpoint` config is deprecated.",
hasLegacyDefaultApiSource ? "Consider renaming the `api/_sitemap-urls` file and add it the `sitemap.sources` config instead. This provides more explicit sitemap generation." : "Consider switching to using the `sitemap.sources` config which also supports fetch options."
]
},
fetch: hasLegacyDefaultApiSource ? "/api/_sitemap-urls" : config.dynamicUrlsApiEndpoint
});
} else {
config.dynamicUrlsApiEndpoint = false;
}
const sitemaps = {};
if (usingMultiSitemaps) {
addServerHandler({
route: "/sitemap_index.xml",
handler: resolve("./runtime/routes/sitemap_index.xml")
});
sitemaps.index = {
sitemapName: "index",
_route: withBase("sitemap_index.xml", nuxt.options.app.baseURL || "/"),
// TODO better index support
// @ts-expect-error untyped
sitemaps: config.sitemaps.index || []
};
if (typeof config.sitemaps === "object") {
for (const sitemapName in config.sitemaps) {
if (sitemapName === "index")
continue;
addServerHandler({
route: `/${sitemapName}-sitemap.xml`,
handler: resolve("./runtime/middleware/[sitemap]-sitemap.xml")
});
const definition = config.sitemaps[sitemapName];
sitemaps[sitemapName] = defu(
{
sitemapName,
_route: withBase(`${sitemapName}-sitemap.xml`, nuxt.options.app.baseURL || "/"),
_hasSourceChunk: typeof definition.urls !== "undefined" || definition.sources?.length || !!definition.dynamicUrlsApiEndpoint
},
{ ...definition, urls: void 0, sources: void 0 },
{ include: config.include, exclude: config.exclude }
);
}
} else {
addServerHandler({
handler: resolve("./runtime/middleware/[sitemap]-sitemap.xml")
});
sitemaps.chunks = {
sitemapName: "chunks",
defaults: config.defaults,
include: config.include,
exclude: config.exclude,
includeAppSources: true
};
}
} else {
sitemaps[config.sitemapName] = {
sitemapName: config.sitemapName,
route: withBase(config.sitemapName, nuxt.options.app.baseURL || "/"),
// will contain the xml
defaults: config.defaults,
include: config.include,
exclude: config.exclude,
includeAppSources: true
};
}
if (resolvedAutoI18n && resolvedAutoI18n.locales && resolvedAutoI18n.strategy !== "no_prefix") {
const i18n = resolvedAutoI18n;
for (const sitemapName in sitemaps) {
if (["index", "chunks"].includes(sitemapName))
continue;
const sitemap = sitemaps[sitemapName];
sitemap.include = (sitemap.include || []).map((path) => splitPathForI18nLocales(path, i18n)).flat();
sitemap.exclude = (sitemap.exclude || []).map((path) => splitPathForI18nLocales(path, i18n)).flat();
}
}
for (const sitemapName in sitemaps) {
const sitemap = sitemaps[sitemapName];
sitemap.include = normalizeFilters(sitemap.include);
sitemap.exclude = normalizeFilters(sitemap.exclude);
}
const runtimeConfig = {
isI18nMapped,
sitemapName: config.sitemapName,
isMultiSitemap: usingMultiSitemaps,
excludeAppSources: config.excludeAppSources,
autoLastmod: config.autoLastmod,
defaultSitemapsChunkSize: config.defaultSitemapsChunkSize,
sortEntries: config.sortEntries,
debug: config.debug,
// needed for nuxt/content integration and prerendering
discoverImages: config.discoverImages,
/* @nuxt/content */
isNuxtContentDocumentDriven,
/* xsl styling */
xsl: config.xsl,
xslTips: config.xslTips,
xslColumns: config.xslColumns,
credits: config.credits,
version,
sitemaps
};
if (resolvedAutoI18n)
runtimeConfig.autoI18n = resolvedAutoI18n;
nuxt.options.runtimeConfig["nuxt-simple-sitemap"] = runtimeConfig;
if (config.debug || nuxt.options.dev) {
addServerHandler({
route: "/__sitemap__/debug.json",
handler: resolve("./runtime/routes/__sitemap__/debug")
});
setupDevToolsUI(config, resolve);
}
if (!config.inferStaticPagesAsRoutes)
config.excludeAppSources = true;
const imports = [
{
from: resolve("./runtime/composables/defineSitemapEventHandler"),
name: "defineSitemapEventHandler"
},
{
from: resolve("./runtime/composables/asSitemapUrl"),
name: "asSitemapUrl"
}
];
addServerImports(imports);
const pagesPromise = createPagesPromise();
const nitroPromise = createNitroPromise();
let resolvedConfigUrls = false;
nuxt.hooks.hook("nitro:config", (nitroConfig) => {
nitroConfig.virtual["#nuxt-simple-sitemap/global-sources.mjs"] = async () => {
const { prerenderUrls, routeRules: routeRules2 } = generateExtraRoutesFromNuxtConfig();
const prerenderUrlsFinal = [
...prerenderUrls,
...((await nitroPromise)._prerenderedRoutes || []).filter((r) => (!r.fileName || r.fileName.endsWith(".html")) && !r.route.endsWith(".html") && !r.route.startsWith("/api/")).map((r) => r._sitemap)
];
const pageSource = convertNuxtPagesToSitemapEntries(await pagesPromise, {
isI18nMapped,
autoLastmod: config.autoLastmod,
defaultLocale: nuxtI18nConfig.defaultLocale || "en",
strategy: nuxtI18nConfig.strategy || "no_prefix",
routesNameSeparator: nuxtI18nConfig.routesNameSeparator,
normalisedLocales
});
if (!resolvedConfigUrls) {
config.urls && userGlobalSources.push({
context: {
name: "sitemap:urls",
description: "Set with the `sitemap.urls` config."
},
urls: await resolveUrls(config.urls)
});
resolvedConfigUrls = true;
}
const globalSources = [
...userGlobalSources.map((s) => {
if (typeof s === "string") {
return {
sourceType: "user",
fetch: s
};
}
s.sourceType = "user";
return s;
}),
...(config.excludeAppSources === true ? [] : [
...appGlobalSources,
{
context: {
name: "nuxt:pages",
description: "Generated from your static page files.",
tips: [
"Can be disabled with `{ excludeAppSources: ['nuxt:pages'] }`."
]
},
urls: pageSource
},
{
context: {
name: "nuxt:route-rules",
description: "Generated from your route rules config.",
tips: [
"Can be disabled with `{ excludeAppSources: ['nuxt:route-rules'] }`."
]
},
urls: routeRules2
},
{
context: {
name: "nuxt:prerender",
description: "Generated at build time when prerendering.",
tips: [
"Can be disabled with `{ excludeAppSources: ['nuxt:prerender'] }`."
]
},
urls: prerenderUrlsFinal
}
]).filter((s) => !config.excludeAppSources.includes(s.context.name) && (!!s.urls?.length || !!s.fetch)).map((s) => {
s.sourceType = "app";
return s;
})
];
return `export const sources = ${JSON.stringify(globalSources, null, 4)}`;
};
const extraSitemapModules = typeof config.sitemaps == "object" ? Object.keys(config.sitemaps).filter((n) => n !== "index") : [];
const sitemapSources = {};
nitroConfig.virtual[`#nuxt-simple-sitemap/child-sources.mjs`] = async () => {
for (const sitemapName of extraSitemapModules) {
sitemapSources[sitemapName] = sitemapSources[sitemapName] || [];
const definition = config.sitemaps[sitemapName];
if (!sitemapSources[sitemapName].length) {
definition.urls && sitemapSources[sitemapName].push({
context: {
name: `sitemaps:${sitemapName}:urls`,
description: "Set with the `sitemap.urls` config."
},
urls: await resolveUrls(definition.urls)
});
definition.dynamicUrlsApiEndpoint && sitemapSources[sitemapName].push({
context: {
name: `${sitemapName}:dynamicUrlsApiEndpoint`,
description: `Generated from your ${sitemapName}:dynamicUrlsApiEndpoint config.`,
tips: [
`You should switch to using the \`sitemaps.${sitemapName}.sources\` config which also supports fetch options.`
]
},
fetch: definition.dynamicUrlsApiEndpoint
});
sitemapSources[sitemapName].push(
...(definition.sources || []).map((s) => {
if (typeof s === "string") {
return {
sourceType: "user",
fetch: s
};
}
s.sourceType = "user";
return s;
})
);
}
}
return `export const sources = ${JSON.stringify(sitemapSources, null, 4)}`;
};
});
if (config.xsl === "/__sitemap__/style.xsl") {
addServerHandler({
route: config.xsl,
handler: resolve("./runtime/routes/sitemap.xsl")
});
config.xsl = withBase(config.xsl, nuxt.options.app.baseURL);
if (prerenderSitemap)
addPrerenderRoutes(config.xsl);
}
addServerHandler({
route: `/${config.sitemapName}`,
handler: resolve("./runtime/routes/sitemap.xml")
});
setupPrerenderHandler(runtimeConfig);
}
});
export { module as default };