import { removeIfMatchPattern } from '../utils/array.js'; import { defaultSitemapTransformer } from '../utils/defaults.js'; import { createDefaultLocaleReplace, entityEscapedUrl, generateUrl, isNextInternalUrl, } from '../utils/url.js'; export class UrlSetBuilder { config; manifest; constructor(config, manifest) { this.config = config; this.manifest = manifest; } /** * Returns absolute url by combining siteUrl and path w.r.t trailingSlash config * @param siteUrl * @param path * @param trailingSlash * @returns */ absoluteUrl(siteUrl, path, trailingSlash) { const url = generateUrl(siteUrl, trailingSlash ? `${path}/` : path); if (!trailingSlash && url.endsWith('/')) { return url.slice(0, url.length - 1); } return entityEscapedUrl(url); } /** * Normalize sitemap fields to include absolute urls * @param field */ normalizeSitemapField(field) { // Handle trailing Slash const trailingSlash = 'trailingSlash' in field ? field.trailingSlash : this.config?.trailingSlash; return { ...field, trailingSlash, loc: this.absoluteUrl(this.config?.siteUrl, field?.loc, trailingSlash), alternateRefs: (field.alternateRefs ?? []).map((alternateRef) => ({ href: alternateRef.hrefIsAbsolute ? alternateRef.href : this.absoluteUrl(alternateRef.href, field.loc, trailingSlash), hreflang: alternateRef.hreflang, })), }; } /** * Create a unique url set */ async createUrlSet() { // Load i18n routes const i18n = this.manifest?.routes?.i18n; // Init all page keys const allKeys = [ ...Object.keys(this.manifest?.build.pages), ...(this.manifest?.build?.ampFirstPages ?? []), ...(this.manifest?.preRender ? Object.keys(this.manifest?.preRender.routes) : []), ]; // Filter out next.js internal urls and generate urls based on sitemap let urlSet = allKeys.filter((x) => !isNextInternalUrl(x)); // Remove default locale if i18n is enabled let defaultLocale; if (i18n) { defaultLocale = i18n.defaultLocale; const replaceDefaultLocale = createDefaultLocaleReplace(defaultLocale); urlSet = urlSet.map(replaceDefaultLocale); } // Remove the urls based on this.config?.exclude array if (this.config?.exclude && this.config?.exclude.length > 0) { urlSet = removeIfMatchPattern(urlSet, this.config?.exclude); } urlSet = [...new Set(urlSet)]; // Remove routes which don't exist const notFoundRoutes = (this.manifest?.preRender?.notFoundRoutes ?? []); urlSet = urlSet.filter((url) => { return (!notFoundRoutes.includes(url) && !notFoundRoutes.includes(`/${defaultLocale}${url}`)); }); // Create sitemap fields based on transformation const sitemapFields = []; // transform using relative urls // Create a map of fields by loc to quickly find collisions const mapFieldsByLoc = {}; for (const url of urlSet) { const sitemapField = await this.config?.transform?.(this.config, url); if (!sitemapField?.loc) continue; sitemapFields.push(sitemapField); // Add link on field to map by loc if (this.config?.additionalPaths) { mapFieldsByLoc[sitemapField.loc] = sitemapField; } } if (this.config?.additionalPaths) { const additions = (await this.config?.additionalPaths({ ...this.config, transform: this.config?.transform ?? defaultSitemapTransformer, })) ?? []; for (const field of additions) { if (!field?.loc) continue; const collision = mapFieldsByLoc[field.loc]; // Update first entry if (collision) { // Mutate common entry between sitemapFields and mapFieldsByLoc (spread operator don't work) Object.entries(field).forEach(([key, value]) => (collision[key] = value)); continue; } sitemapFields.push(field); } } return sitemapFields.map((x) => this.normalizeSitemapField(x)); } }