Files
ipal-kit/dist/modules/seo/buildSitemapEntries.js
T
2026-08-03 12:24:28 +02:00

158 lines
5.2 KiB
JavaScript

import { archiveFieldName } from '../content/index.js';
import { buildLocalizedPath, getLocalizedSlugs } from '../i18n/index.js';
import { buildHreflangAlternates } from './hreflang.js';
/** Skip drafts and anything flagged noindex in the SEO tab. */ function isIndexable(doc) {
if (doc._status && doc._status !== 'published') {
return false;
}
if (doc.meta?.noindex) {
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({
config,
slugField: doc.slug
}) : {};
const path = buildLocalizedPath({
config,
homeSlug,
locale,
prefix,
slugs
});
if (!path) {
return null;
}
const origin = baseUrl.replace(/\/$/, '');
const languages = buildHreflangAlternates({
baseUrl,
config,
homeSlug,
prefix,
slugs
});
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({ baseUrl, changeFrequency = 'weekly', config, content, homeSlug, pagesSlug = 'pages', payload, settingsSlug = 'site-settings' }) {
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,
depth: 1,
locale: 'all'
});
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,
depth: 0,
limit: 0,
locale: 'all',
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