2025-09-05 14:59:21 +08:00

1200 lines
48 KiB
JavaScript

import { useNuxt, loadNuxtModuleInstance, createResolver, addTemplate, extendPages, tryUseNuxt, defineNuxtModule, useLogger, hasNuxtModule, getNuxtModuleVersion, hasNuxtModuleCompatibility, addServerImports, addServerPlugin, addServerHandler, findPath, addPrerenderRoutes } from '@nuxt/kit';
import { withHttps, withBase, parseURL, joinURL, withoutLeadingSlash, withoutTrailingSlash, withLeadingSlash } from 'ufo';
import { installNuxtSiteConfig } from 'nuxt-site-config-kit';
import { defu } from 'defu';
import { readPackageJSON } from 'pkg-types';
import { statSync, existsSync } from 'node:fs';
import { extname, relative, dirname } from 'pathe';
import { createPathFilter, splitForLocales, mergeOnKey } from '../dist/runtime/utils-pure.js';
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 { normaliseDate } from '../dist/runtime/nitro/sitemap/urlset/normalise.js';
async function resolveUrls(urls, ctx) {
if (typeof urls === "function")
urls = urls();
urls = await urls;
try {
urls = JSON.parse(JSON.stringify(urls));
} catch (e) {
ctx.logger.warn(`Failed to serialize ${typeof urls} \`${ctx.path}\`, please make sure that the urls resolve as a valid array without circular dependencies.`);
ctx.logger.error(e);
return [];
}
return urls;
}
function deepForEachPage(pages, callback, opts, 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}`;
}
let didCallback = false;
if (opts.isI18nMicro) {
const localePattern = /\/:locale\(([^)]+)\)/;
const match = localePattern.exec(currentPath || "");
if (match) {
const locales = match[1].split("|");
locales.forEach((locale) => {
const subPage = { ...page };
const localizedPath = (currentPath || "").replace(localePattern, `/${locale}`);
subPage.name += opts.routesNameSeparator + locale;
subPage.path = localizedPath;
callback(subPage, localizedPath || "", depth);
didCallback = true;
});
}
}
if (!didCallback) {
callback(page, currentPath || "", depth);
}
if (page.children) {
deepForEachPage(page.children, callback, opts, currentPath, depth + 1);
}
});
}
function convertNuxtPagesToSitemapEntries(pages, config) {
const pathFilter = createPathFilter(config.filter);
const routesNameSeparator = config.routesNameSeparator || "___";
let flattenedPages = [];
deepForEachPage(
pages,
(page, loc, depth) => {
flattenedPages.push({ page, loc, depth });
},
{
...config,
routesNameSeparator: config.routesNameSeparator || "___"
}
);
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;
});
if (config.strategy === "prefix_and_default") {
flattenedPages = flattenedPages.filter((p) => {
if (p.page?.name) {
const [, locale] = p.page.name.split(routesNameSeparator);
return locale !== config.defaultLocale || p.page.name.endsWith("__default");
}
return true;
});
}
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 {
}
}
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 { _sitemap } = config.normalisedLocales.find((l) => l.code === locale) || { _sitemap: locale };
acc[name].push({ ...e, _sitemap: config.isI18nMapped ? _sitemap : 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._sitemap;
delete e.page;
delete e.locale;
return { ...e };
}).filter(Boolean);
}
return entries.map((entry) => {
const alternatives = entries.map((entry2) => {
const locale2 = config.normalisedLocales.find((l) => l.code === entry2.locale);
if (!pathFilter(entry2.loc))
return false;
const href = locale2?.domain ? withHttps(withBase(entry2.loc, locale2?.domain)) : entry2.loc;
return {
hreflang: locale2?._hreflang,
href
};
}).filter(Boolean);
const xDefault = entries.find((a) => a.locale === config.defaultLocale);
if (xDefault && alternatives.length && pathFilter(xDefault.loc)) {
const locale2 = config.normalisedLocales.find((l) => l.code === xDefault.locale);
const href = locale2?.domain ? withHttps(withBase(xDefault.loc, locale2?.domain)) : xDefault.loc;
alternatives.push({
hreflang: "x-default",
href
});
}
const e = { ...entry };
if (config.isI18nMapped) {
const { _sitemap } = config.normalisedLocales.find((l) => l.code === entry.locale) || { _sitemap: locale };
e._sitemap = _sitemap;
}
delete e.page;
delete e.locale;
return {
...e,
alternatives
};
});
}).filter(Boolean).flat();
}
function generateExtraRoutesFromNuxtConfig(nuxt = useNuxt()) {
const filterForValidPage = (p) => p && !extname(p) && !p.startsWith("/api/") && !p.startsWith("/_");
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).filter(filterForValidPage);
const prerenderUrls = (nuxt.options.nitro.prerender?.routes || []).filter(filterForValidPage);
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);
const fileName = `${module.replace("/", "-")}.d.ts`;
addTemplate({
filename: `module/${fileName}`,
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/${fileName}`) });
});
nuxt.hooks.hook("nitro:config", (config) => {
config.typescript = config.typescript || {};
config.typescript.tsConfig = config.typescript.tsConfig || {};
config.typescript.tsConfig.include = config.typescript.tsConfig.include || [];
config.typescript.tsConfig.include.push(`./module/${fileName}`);
});
}
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) {
nitroConfig = nitroConfig || tryUseNuxt()?.options?.nitro;
if (provider === "stackblitz")
return "stackblitz";
let preset;
if (nitroConfig && nitroConfig?.preset)
preset = nitroConfig.preset;
if (!preset)
preset = env.NITRO_PRESET || env.SERVER_PRESET || detectTarget() || "node-server";
return preset.replace("_", "-");
}
function extractSitemapMetaFromHtml(html, options) {
options = options || { images: true, videos: 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\s+(?:[^>]*?\s)?src=["']((?!data:|blob:|file:)[^"']+?)["'][^>]*>/gi;
let match;
while ((match = imgRegex.exec(mainMatch[1])) !== null) {
if (match.index === imgRegex.lastIndex)
imgRegex.lastIndex++;
let url = match[1];
if (url.startsWith("/"))
url = tryUseNuxt() ? withSiteUrl(url) : url;
images.add(url);
}
}
if (images.size > 0)
payload.images = [...images].map((i) => ({ loc: i }));
}
if (options?.videos) {
const videos = [];
const mainRegex = /<main[^>]*>([\s\S]*?)<\/main>/;
const mainMatch = mainRegex.exec(html);
if (mainMatch?.[1] && mainMatch[1].includes("<video")) {
const videoRegex = /<video[^>]*>([\s\S]*?)<\/video>/g;
const videoAttrRegex = /<video[^>]*\ssrc="([^"]+)"(?:[^>]*\sposter="([^"]+)")?/;
const videoPosterRegex = /<video[^>]*\sposter="([^"]+)"/;
const videoTitleRegex = /<video[^>]*\sdata-title="([^"]+)"/;
const videoDescriptionRegex = /<video[^>]*\sdata-description="([^"]+)"/;
const sourceRegex = /<source[^>]*\ssrc="([^"]+)"/g;
let videoMatch;
while ((videoMatch = videoRegex.exec(mainMatch[1])) !== null) {
const videoContent = videoMatch[1];
const videoTag = videoMatch[0];
const videoAttrMatch = videoAttrRegex.exec(videoTag);
const videoSrc = videoAttrMatch ? videoAttrMatch[1] : "";
const poster = (videoPosterRegex.exec(videoTag) || [])[1] || "";
const title = (videoTitleRegex.exec(videoTag) || [])[1] || "";
const description = (videoDescriptionRegex.exec(videoTag) || [])[1] || "";
const sources = [];
let sourceMatch;
while ((sourceMatch = sourceRegex.exec(videoContent)) !== null) {
sources.push({
src: sourceMatch[1],
poster,
title,
description
});
}
if (videoSrc) {
videos.push({
src: videoSrc,
poster,
title,
description,
sources: []
});
}
if (sources.length > 0) {
videos.push(...sources);
}
}
}
if (videos.length > 0) {
payload.videos = videos.map(
(video) => ({
content_loc: video.src,
thumbnail_loc: video.poster,
title: video.title,
description: video.description
})
);
}
}
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;
}
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__/`) || routes.includes(`/sitemap.xml`) || routes.includes(`/${sitemapName}`) || routes.includes("/sitemap_index.xml");
}
function isNuxtGenerate(nuxt = useNuxt()) {
return nuxt.options._generate || [
"static",
"github-pages"
].includes(resolveNitroPreset());
}
const NuxtRedirectHtmlRegex = /<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=([^"]+)"><\/head><\/html>/;
function setupPrerenderHandler(_options, nuxt = useNuxt()) {
const { runtimeConfig: options, logger } = _options;
const prerenderedRoutes = nuxt.options.nitro.prerender?.routes || [];
let prerenderSitemap = isNuxtGenerate() || includesSitemapRoot(options.sitemapName, prerenderedRoutes);
if (resolveNitroPreset() === "vercel-edge") {
logger.warn("Runtime sitemaps are not supported on Vercel Edge, falling back to prerendering sitemaps.");
prerenderSitemap = true;
}
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;
});
nitro.hooks.hook("prerender:generate", async (route) => {
const html = route.contents;
if (!route.fileName?.endsWith(".html") || !html || ["/200.html", "/404.html"].includes(route.route))
return;
if (html.match(NuxtRedirectHtmlRegex)) {
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 { _sitemap } = options.autoI18n.locales.find((l) => l.code === locale) || { _sitemap: locale };
route._sitemap._sitemap = _sitemap;
}
}
route._sitemap = defu(extractSitemapMetaFromHtml(html, {
images: options.discoverImages,
videos: options.discoverVideos,
// 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 = "/__sitemap__/devtools";
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: "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 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("/_"))
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(ctx) {
const { localeCode, pageLocales, nuxtI18nConfig, forcedStrategy, normalisedLocales } = ctx;
const locale = normalisedLocales.find((l) => l.code === localeCode);
let path = pageLocales;
switch (forcedStrategy ?? nuxtI18nConfig.strategy) {
case "prefix_except_default":
case "prefix_and_default":
path = localeCode === nuxtI18nConfig.defaultLocale ? pageLocales : joinURL(localeCode, pageLocales);
break;
case "prefix":
path = joinURL(localeCode, pageLocales);
break;
}
return locale?.domain ? withHttps(withBase(path, locale.domain)) : path;
}
function normalizeLocales(nuxtI18nConfig) {
let locales = nuxtI18nConfig.locales || [];
let onlyLocales = nuxtI18nConfig?.bundle?.onlyLocales || [];
onlyLocales = typeof onlyLocales === "string" ? [onlyLocales] : onlyLocales;
locales = mergeOnKey(locales.map((locale) => typeof locale === "string" ? { code: locale } : locale), "code");
if (onlyLocales.length) {
locales = locales.filter((locale) => onlyLocales.includes(locale.code));
}
return locales.map((locale) => {
if (locale.iso && !locale.language) {
locale.language = locale.iso;
}
locale._hreflang = locale.language || locale.code;
locale._sitemap = locale.language || locale.code;
return locale;
});
}
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(`[@nuxtjs/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: "@nuxtjs/sitemap",
compatibility: {
nuxt: ">=3.9.0",
bridge: false
},
configKey: "sitemap"
},
defaults: {
enabled: true,
credits: true,
cacheMaxAgeSeconds: 60 * 10,
// cache for 10 minutes
minify: false,
debug: false,
defaultSitemapsChunkSize: 1e3,
autoLastmod: false,
discoverImages: true,
discoverVideos: true,
dynamicUrlsApiEndpoint: "/api/_sitemap-urls",
urls: [],
sortEntries: true,
sitemapsPathPrefix: "/__sitemap__/",
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/**", "/_**"],
// sources
sources: [],
excludeAppSources: [],
inferStaticPagesAsRoutes: true
},
async setup(config, nuxt) {
const { resolve } = createResolver(import.meta.url);
const { name, version } = await readPackageJSON(resolve("../package.json"));
const logger = useLogger(name);
logger.level = config.debug || nuxt.options.debug ? 4 : 3;
if (config.enabled === false) {
logger.debug("The module is disabled, skipping setup.");
return;
}
nuxt.options.alias["#sitemap"] = resolve("./runtime");
nuxt.options.nitro.alias = nuxt.options.nitro.alias || {};
nuxt.options.nitro.alias["#sitemap"] = resolve("./runtime");
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 normalizedSitemaps = typeof config.sitemaps === "boolean" ? {} : config.sitemaps || {};
if (!nuxt.options._prepare && Object.keys(normalizedSitemaps).length) {
const isSitemapIndexOnly = typeof normalizedSitemaps?.index !== "undefined" && Object.keys(normalizedSitemaps).length === 1;
if (!isSitemapIndexOnly) {
const warnForIgnoredKey = (key) => {
logger.warn(`You are using multiple-sitemaps but have provided \`sitemap.${key}\` in your Nuxt config. This will be ignored, please move it to the child sitemap config.`);
logger.warn("Learn more at: https://nuxtseo.com/sitemap/guides/multi-sitemaps");
};
switch (true) {
case (config?.sources?.length || 0) > 0:
warnForIgnoredKey("sources");
break;
case config?.includeAppSources !== void 0:
warnForIgnoredKey("includeAppSources");
break;
}
}
}
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.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 = [];
let usingI18nPages = false;
const i18nModule = ["@nuxtjs/i18n", "nuxt-i18n-micro"].find((s) => hasNuxtModule(s));
if (i18nModule) {
const i18nVersion = await getNuxtModuleVersion(i18nModule);
if (i18nVersion && i18nModule === "@nuxtjs/i18n" && !await hasNuxtModuleCompatibility(i18nModule, ">=8"))
logger.warn(`You are using ${i18nModule} v${i18nVersion}. For the best compatibility, please upgrade to ${i18nModule} v8.0.0 or higher.`);
nuxtI18nConfig = await getNuxtModuleOptions(i18nModule) || {};
if (typeof nuxtI18nConfig.includeDefaultLocaleRoute !== "undefined") {
nuxtI18nConfig.strategy = nuxtI18nConfig.includeDefaultLocaleRoute ? "prefix" : "prefix_except_default";
}
normalisedLocales = normalizeLocales(nuxtI18nConfig);
usingI18nPages = !!Object.keys(nuxtI18nConfig.pages || {}).length;
if (usingI18nPages && !hasDisabledAutoI18n) {
const i18nPagesSources = {
context: {
name: `${i18nModule}: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)?._hreflang || l,
href: generatePathForI18nPages({ localeCode: l, pageLocales: pageLocales[l], nuxtI18nConfig, normalisedLocales })
}));
if (alternatives.length && nuxtI18nConfig.defaultLocale && pageLocales[nuxtI18nConfig.defaultLocale])
alternatives.push({ hreflang: "x-default", href: generatePathForI18nPages({ normalisedLocales, localeCode: nuxtI18nConfig.defaultLocale, pageLocales: pageLocales[nuxtI18nConfig.defaultLocale], nuxtI18nConfig }) });
i18nPagesSources.urls.push({
_sitemap: locale._sitemap,
loc: generatePathForI18nPages({ normalisedLocales, localeCode, pageLocales: pageLocales[localeCode], nuxtI18nConfig }),
alternatives
});
if (nuxtI18nConfig.strategy === "prefix_and_default" && localeCode === nuxtI18nConfig.defaultLocale) {
i18nPagesSources.urls.push({
_sitemap: locale._sitemap,
loc: generatePathForI18nPages({ normalisedLocales, 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 ${i18nModule} but have not configured any locales, this will cause issues with ${name}. 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
};
}
let canI18nMap = config.sitemaps !== false && nuxtI18nConfig.strategy !== "no_prefix";
if (typeof config.sitemaps === "object") {
const isSitemapIndexOnly = typeof config.sitemaps.index !== "undefined" && Object.keys(config.sitemaps).length === 1;
if (!isSitemapIndexOnly)
canI18nMap = false;
}
if (canI18nMap && resolvedAutoI18n) {
config.sitemaps = { index: [...config.sitemaps?.index || [], ...config.appendSitemaps || []] };
for (const locale of resolvedAutoI18n.locales)
config.sitemaps[locale._sitemap] = { includeAppSources: true };
isI18nMapped = true;
usingMultiSitemaps = true;
}
}
nuxt.hooks.hook("robots:config", (robotsConfig) => {
robotsConfig.sitemap.push(usingMultiSitemaps ? "/sitemap_index.xml" : `/${config.sitemapName}`);
});
nuxt.hooks.hook("modules:done", async () => {
const robotsModuleName = ["nuxt-simple-robots", "@nuxtjs/robots"].find((s) => hasNuxtModule(s));
let needsRobotsPolyfill = true;
if (robotsModuleName) {
const robotsVersion = await getNuxtModuleVersion(robotsModuleName);
if (robotsVersion && !await hasNuxtModuleCompatibility(robotsModuleName, ">=4"))
logger.warn(`You are using ${robotsModuleName} v${robotsVersion}. For the best compatibility, please upgrade to ${robotsModuleName} v4.0.0 or higher.`);
else
needsRobotsPolyfill = false;
}
if (needsRobotsPolyfill) {
addServerImports([{
name: "getPathRobotConfigPolyfill",
as: "getPathRobotConfig",
from: resolve("./runtime/nitro/composables/getPathRobotConfigPolyfill")
}]);
}
});
extendTypes(name, async ({ typesPath }) => {
return `
declare module 'nitropack' {
interface PrerenderRoute {
_sitemap?: import('${typesPath}').SitemapUrl
}
interface NitroRouteRules {
index?: boolean
sitemap?: import('${typesPath}').SitemapItemDefaults
}
interface NitroRouteConfig {
index?: boolean
sitemap?: import('${typesPath}').SitemapItemDefaults
}
interface NitroRuntimeHooks {
'sitemap:index-resolved': (ctx: import('${typesPath}').SitemapIndexRenderCtx) => void | Promise<void>
'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",
"Cache-Control": config.cacheMaxAgeSeconds ? `public, max-age=${config.cacheMaxAgeSeconds}, must-revalidate` : "no-cache, no-store",
"X-Sitemap-Prerendered": (/* @__PURE__ */ new Date()).toISOString()
};
}
if (!nuxt.options.dev && !isNuxtGenerate() && config.cacheMaxAgeSeconds && config.runtimeCacheStorage !== false) {
routeRules[nitroPreset.includes("vercel") ? "isr" : "swr"] = config.cacheMaxAgeSeconds;
routeRules.cache = {
// handle multi-tenancy
swr: true,
maxAge: config.cacheMaxAgeSeconds,
varies: ["X-Forwarded-Host", "X-Forwarded-Proto", "Host"]
};
if (typeof config.runtimeCacheStorage === "object")
routeRules.cache.base = "sitemap";
}
nuxt.options.nitro.routeRules["/sitemap.xsl"] = {
headers: {
"Content-Type": "application/xslt+xml"
}
};
if (usingMultiSitemaps) {
nuxt.options.nitro.routeRules["/sitemap.xml"] = { redirect: "/sitemap_index.xml" };
nuxt.options.nitro.routeRules["/sitemap_index.xml"] = routeRules;
if (typeof config.sitemaps === "object") {
for (const k in config.sitemaps) {
nuxt.options.nitro.routeRules[joinURL(config.sitemapsPathPrefix, `/${k}.xml`)] = routeRules;
nuxt.options.nitro.routeRules[`/${k}-sitemap.xml`] = { redirect: joinURL(config.sitemapsPathPrefix, `${k}.xml`) };
}
} 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/nitro/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/nitro/routes/sitemap_index.xml"),
lazy: true,
middleware: false
});
addServerHandler({
route: joinURL(config.sitemapsPathPrefix, `/**:sitemap`),
handler: resolve("./runtime/nitro/routes/sitemap/[sitemap].xml"),
lazy: true,
middleware: false
});
sitemaps.index = {
sitemapName: "index",
_route: withBase("sitemap_index.xml", nuxt.options.app.baseURL || "/"),
// @ts-expect-error untyped
sitemaps: [...config.sitemaps.index || [], ...config.appendSitemaps || []]
};
if (typeof config.sitemaps === "object") {
for (const sitemapName in config.sitemaps) {
if (sitemapName === "index")
continue;
const definition = config.sitemaps[sitemapName];
sitemaps[sitemapName] = defu(
{
sitemapName,
_route: withBase(joinURL(config.sitemapsPathPrefix, `${sitemapName}.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 {
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 && usingI18nPages && !hasDisabledAutoI18n) {
const pages = nuxtI18nConfig?.pages || {};
for (const sitemapName in sitemaps) {
let mapToI18nPages = function(path) {
if (typeof path !== "string")
return [path];
const withoutSlashes = withoutTrailingSlash(withoutLeadingSlash(path)).replace("/index", "");
if (withoutSlashes in pages) {
const pageLocales = pages[withoutSlashes];
if (pageLocales) {
return Object.keys(pageLocales).map((localeCode) => withLeadingSlash(generatePathForI18nPages({
localeCode,
pageLocales: pageLocales[localeCode],
nuxtI18nConfig,
normalisedLocales
})));
}
}
let match = [path];
Object.values(pages).forEach((pageLocales) => {
if (nuxtI18nConfig.defaultLocale in pageLocales && pageLocales[nuxtI18nConfig.defaultLocale] === path)
match = Object.keys(pageLocales).map((localeCode) => withLeadingSlash(generatePathForI18nPages({ localeCode, pageLocales: pageLocales[localeCode], nuxtI18nConfig, normalisedLocales })));
});
return match;
};
if (["index", "chunks"].includes(sitemapName))
continue;
const sitemap = sitemaps[sitemapName];
sitemap.include = (sitemap.include || []).flatMap((path) => mapToI18nPages(path));
sitemap.exclude = (sitemap.exclude || []).flatMap((path) => mapToI18nPages(path));
}
}
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,
cacheMaxAgeSeconds: nuxt.options.dev ? 0 : config.cacheMaxAgeSeconds,
autoLastmod: config.autoLastmod,
defaultSitemapsChunkSize: config.defaultSitemapsChunkSize,
minify: config.minify,
sortEntries: config.sortEntries,
debug: config.debug,
// needed for nuxt/content integration and prerendering
discoverImages: config.discoverImages,
discoverVideos: config.discoverVideos,
sitemapsPathPrefix: config.sitemapsPathPrefix,
/* @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.sitemap = runtimeConfig;
if (config.debug || nuxt.options.dev) {
addServerHandler({
route: "/__sitemap__/debug.json",
handler: resolve("./runtime/nitro/routes/__sitemap__/debug")
});
setupDevToolsUI(config, resolve);
}
if (!config.inferStaticPagesAsRoutes)
config.excludeAppSources = true;
const imports = [
{
from: resolve("./runtime/nitro/composables/defineSitemapEventHandler"),
name: "defineSitemapEventHandler"
},
{
from: resolve("./runtime/nitro/composables/asSitemapUrl"),
name: "asSitemapUrl"
}
];
addServerImports(imports);
const pagesPromise = createPagesPromise();
const nitroPromise = createNitroPromise();
let resolvedConfigUrls = false;
nuxt.hooks.hook("nitro:config", (nitroConfig) => {
nitroConfig.virtual["#sitemap-virtual/global-sources.mjs"] = async () => {
const { prerenderUrls, routeRules: routeRules2 } = generateExtraRoutesFromNuxtConfig();
const prerenderUrlsFinal = [
...prerenderUrls,
...((await nitroPromise)._prerenderedRoutes || []).filter((r) => {
const lastSegment = r.route.split("/").pop();
const isExplicitFile = !!lastSegment?.match(/\.[0-9a-z]+$/i)?.[0];
if (r.error || ["/200.html", "/404.html", "/index.html"].includes(r.route))
return false;
return r.contentType?.includes("text/html") || !isExplicitFile;
}).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,
filter: {
include: normalizeFilters(config.include),
exclude: normalizeFilters(config.exclude)
},
isI18nMicro: i18nModule === "nuxt-i18n-micro"
});
if (!pageSource.length) {
pageSource.push(nuxt.options.app.baseURL || "/");
}
if (!resolvedConfigUrls && config.urls) {
if (config.urls) {
userGlobalSources.push({
context: {
name: "sitemap:urls",
description: "Set with the `sitemap.urls` config."
},
urls: await resolveUrls(config.urls, { path: "sitemap:urls", logger })
});
}
resolvedConfigUrls = true;
}
const globalSources = [
...userGlobalSources.map((s) => {
if (typeof s === "string" || Array.isArray(s)) {
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[`#sitemap-virtual/child-sources.mjs`] = async () => {
for (const sitemapName of extraSitemapModules) {
sitemapSources[sitemapName] = sitemapSources[sitemapName] || [];
const definition = config.sitemaps[sitemapName];
if (!sitemapSources[sitemapName].length) {
if (definition.urls) {
sitemapSources[sitemapName].push({
context: {
name: `sitemaps:${sitemapName}:urls`,
description: "Set with the `sitemap.urls` config."
},
urls: await resolveUrls(definition.urls, { path: `sitemaps:${sitemapName}:urls`, logger })
});
}
if (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" || Array.isArray(s)) {
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/nitro/routes/sitemap.xsl")
});
config.xsl = withBase(config.xsl, nuxt.options.app.baseURL);
if (prerenderSitemap)
addPrerenderRoutes(config.xsl);
}
addServerHandler({
route: `/${config.sitemapName}`,
handler: resolve("./runtime/nitro/routes/sitemap.xml")
});
setupPrerenderHandler({ runtimeConfig, logger });
}
});
export { module as default };