1200 lines
48 KiB
JavaScript
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 };
|