Files
ipal-kit/dist/modules/seo/buildSitemapEntries.js

197 lines
7.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { archiveFieldName } from '../content/index.js';
import { buildLocalizedPath, getLocaleCodes, getLocalizedSlugs } from '../i18n/index.js';
import { buildHreflangAlternates } from './hreflang.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',
'error',
'not-found'
]);
/** 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({
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'
});
// Home slug as a per-locale MAP so every language's homepage collapses to its
// root in the sitemap (/pl, /de, /en) — not just the default locale. Priority:
// caller-provided homeSlug > full map from the homepage relationship > single
// default-locale slug > 'home'. slugMapAllLocales already builds the map (it's
// used for archive prefixes below); reusing it here fixes /de/startseite
// appearing in the sitemap instead of /de.
const resolvedHomeSlug = homeSlug ?? slugMapAllLocales(settings.homepage, locales) ?? 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 a separate <url> for EACH locale. Google's sitemap spec for
// localized sites requires one <url> per language version (each with its
// own <loc> plus xhtml:link alternates), not a single default-locale <url>
// with the others hidden only in alternates. Emitting only the default
// locale makes GSC count 8 URLs instead of 8×3 — a real reporting gap.
for (const locale of getLocaleCodes(config)){
const entry = entryFor(raw, locale, 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