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 = /]*>([\s\S]*?)<\/main>/; const mainMatch = mainRegex.exec(html); if (mainMatch?.[1] && mainMatch[1].includes("]*?\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 = /]*>([\s\S]*?)<\/main>/; const mainMatch = mainRegex.exec(html); if (mainMatch?.[1] && mainMatch[1].includes("]*>([\s\S]*?)<\/video>/g; const videoAttrRegex = /]*\ssrc="([^"]+)"(?:[^>]*\sposter="([^"]+)")?/; const videoPosterRegex = /]*\sposter="([^"]+)"/; const videoTitleRegex = /]*\sdata-title="([^"]+)"/; const videoDescriptionRegex = /]*\sdata-description="([^"]+)"/; const sourceRegex = /]*\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(/]+property="article:modified_time"[^>]+content="([^"]+)"/)?.[1] || html.match(/]+content="([^"]+)"[^>]+property="article:modified_time"/)?.[1]; if (articleModifiedTime) payload.lastmod = articleModifiedTime; } if (options?.alternatives) { const alternatives = (html.match(/]+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 = /<\/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 'sitemap:resolved': (ctx: import('${typesPath}').SitemapRenderCtx) => void | Promise 'sitemap:output': (ctx: import('${typesPath}').SitemapOutputHookCtx) => void | Promise } } 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 };