160 lines
5.9 KiB
JavaScript
160 lines
5.9 KiB
JavaScript
import { getLocalizedSlugs } from '../i18n/index.js';
|
|
import { buildLocalizedPath } from '../i18n/index.js';
|
|
import { buildHreflangAlternates } from './hreflang.js';
|
|
import { archiveFieldName } from '../content/index.js';
|
|
/**
|
|
* Slugs that must never appear in the sitemap — error/system pages that exist as
|
|
* documents (e.g. a '404' page in the Pages collection) but should not be
|
|
* indexed. A sitemap should list only real, HTTP-200 content; a '/pl/404' entry
|
|
* is an audit finding. Matched against the slug in any locale.
|
|
*/ const EXCLUDED_SITEMAP_SLUGS = new Set([
|
|
'404',
|
|
'500',
|
|
'not-found',
|
|
'error'
|
|
]);
|
|
/** True if the doc's slug (in any locale) is an excluded system/error slug. */ function hasExcludedSlug(slug) {
|
|
if (typeof slug === 'string') return EXCLUDED_SITEMAP_SLUGS.has(slug);
|
|
if (slug && typeof slug === 'object') {
|
|
for (const value of Object.values(slug)){
|
|
if (typeof value === 'string' && EXCLUDED_SITEMAP_SLUGS.has(value)) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
/** Skip drafts, noindex, and system/error pages (404 etc.). */ function isIndexable(doc) {
|
|
if (doc._status && doc._status !== 'published') return false;
|
|
if (doc.meta?.noindex) return false;
|
|
if (hasExcludedSlug(doc.slug)) return false;
|
|
return true;
|
|
}
|
|
/**
|
|
* Turns one document into a sitemap entry per default locale isn't needed —
|
|
* one entry with all languages as alternates is the correct, de-duplicated
|
|
* shape. The `url` is the current locale's path; `alternates.languages` carries
|
|
* every locale (including itself, per Google's guidance).
|
|
*/ function entryFor(doc, locale, config, baseUrl, homeSlug, prefix, changeFrequency) {
|
|
const slugs = doc.slug && typeof doc.slug === 'object' ? getLocalizedSlugs({
|
|
slugField: doc.slug,
|
|
config
|
|
}) : {};
|
|
const path = buildLocalizedPath({
|
|
slugs,
|
|
locale,
|
|
config,
|
|
homeSlug,
|
|
prefix
|
|
});
|
|
if (!path) return null;
|
|
const origin = baseUrl.replace(/\/$/, '');
|
|
const languages = buildHreflangAlternates({
|
|
slugs,
|
|
config,
|
|
baseUrl,
|
|
homeSlug,
|
|
prefix
|
|
});
|
|
return {
|
|
url: `${origin}${path}`,
|
|
...doc.updatedAt ? {
|
|
lastModified: doc.updatedAt
|
|
} : {},
|
|
...changeFrequency ? {
|
|
changeFrequency
|
|
} : {},
|
|
...Object.keys(languages).length ? {
|
|
alternates: {
|
|
languages
|
|
}
|
|
} : {}
|
|
};
|
|
}
|
|
/**
|
|
* Collects every public URL — pages and archive entries — as sitemap entries
|
|
* with per-URL hreflang and lastmod.
|
|
*
|
|
* Reuses the same path/hreflang builders as page metadata, so the sitemap can't
|
|
* drift from what the pages actually render (a classic source of sitemap bugs:
|
|
* a URL listed one way and served another). Drafts and noindex documents are
|
|
* omitted.
|
|
*
|
|
* ```ts
|
|
* // app/sitemap.ts
|
|
* export default async function sitemap() {
|
|
* return buildSitemapEntries({
|
|
* payload: await getPayload({ config }),
|
|
* config: i18nConfig,
|
|
* baseUrl: process.env.NEXT_PUBLIC_SERVER_URL!,
|
|
* content: contentConfig,
|
|
* })
|
|
* }
|
|
* ```
|
|
*/ export async function buildSitemapEntries({ payload, config, baseUrl, pagesSlug = 'pages', content, settingsSlug = 'site-settings', homeSlug, changeFrequency = 'weekly' }) {
|
|
const locales = config.locales.map((l)=>l.code);
|
|
const defaultLocale = config.defaultLocale;
|
|
// Resolve homeSlug and archive prefixes from System Pages (read once, in all
|
|
// locales so archive prefixes are available per language).
|
|
const settings = await payload.findGlobal({
|
|
slug: settingsSlug,
|
|
locale: 'all',
|
|
depth: 1
|
|
});
|
|
const resolvedHomeSlug = homeSlug ?? extractSlugInLocale(settings.homepage, defaultLocale) ?? 'home';
|
|
// Which collections to walk: pages (no prefix) + each content collection with
|
|
// its archive-page slugs as the localized prefix.
|
|
const collections = [
|
|
{
|
|
slug: pagesSlug
|
|
}
|
|
];
|
|
for (const c of content?.collections ?? []){
|
|
const archive = settings[archiveFieldName(c.slug)];
|
|
const prefixSlugs = archive ? slugMapAllLocales(archive, locales) : undefined;
|
|
collections.push({
|
|
slug: c.slug,
|
|
prefixSlugs
|
|
});
|
|
}
|
|
const entries = [];
|
|
for (const collection of collections){
|
|
// Read every doc once, across locales, so one row yields all its language
|
|
// alternates without re-querying per locale.
|
|
const result = await payload.find({
|
|
collection: collection.slug,
|
|
locale: 'all',
|
|
depth: 0,
|
|
limit: 0,
|
|
pagination: false
|
|
});
|
|
for (const raw of result.docs){
|
|
if (!isIndexable(raw)) continue;
|
|
// Emit the entry under the default locale's URL; alternates cover the rest.
|
|
const entry = entryFor(raw, defaultLocale, config, baseUrl, resolvedHomeSlug, collection.prefixSlugs, changeFrequency);
|
|
if (entry) entries.push(entry);
|
|
}
|
|
}
|
|
return entries;
|
|
}
|
|
/** Pulls a slug string from a populated relationship in a specific locale. */ function extractSlugInLocale(rel, locale) {
|
|
if (!rel || typeof rel !== 'object') return undefined;
|
|
const slug = rel.slug;
|
|
if (typeof slug === 'string') return slug;
|
|
if (slug && typeof slug === 'object') {
|
|
const v = slug[locale];
|
|
return typeof v === 'string' ? v : undefined;
|
|
}
|
|
return undefined;
|
|
}
|
|
/** Builds a locale→slug map from a populated archive relationship. */ function slugMapAllLocales(rel, locales) {
|
|
if (!rel || typeof rel !== 'object') return undefined;
|
|
const slug = rel.slug;
|
|
if (!slug || typeof slug !== 'object') return undefined;
|
|
const map = {};
|
|
for (const locale of locales){
|
|
const v = slug[locale];
|
|
if (typeof v === 'string') map[locale] = v;
|
|
}
|
|
return Object.keys(map).length ? map : undefined;
|
|
}
|
|
|
|
//# sourceMappingURL=buildSitemapEntries.js.map
|