1.2.0: local SEO structured data (LocalBusiness, Service, FAQPage), noindex per page, exclude 404 from sitemap, robots param docs

This commit is contained in:
2026-09-08 13:14:05 +02:00
parent 60f07fddc9
commit 7cd3cbaae5
4 changed files with 122 additions and 101 deletions
+46 -44
View File
@@ -1,13 +1,31 @@
import { archiveFieldName } from '../content/index.js';
import { buildLocalizedPath, getLocalizedSlugs } from '../i18n/index.js';
import { getLocalizedSlugs } from '../i18n/index.js';
import { buildLocalizedPath } 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;
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;
}
/**
@@ -17,26 +35,24 @@ import { buildHreflangAlternates } from './hreflang.js';
* 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
slugField: doc.slug,
config
}) : {};
const path = buildLocalizedPath({
slugs,
locale,
config,
homeSlug,
locale,
prefix,
slugs
prefix
});
if (!path) {
return null;
}
if (!path) return null;
const origin = baseUrl.replace(/\/$/, '');
const languages = buildHreflangAlternates({
baseUrl,
slugs,
config,
baseUrl,
homeSlug,
prefix,
slugs
prefix
});
return {
url: `${origin}${path}`,
@@ -73,15 +89,15 @@ import { buildHreflangAlternates } from './hreflang.js';
* })
* }
* ```
*/ export async function buildSitemapEntries({ baseUrl, changeFrequency = 'weekly', config, content, homeSlug, pagesSlug = 'pages', payload, settingsSlug = 'site-settings' }) {
*/ 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,
depth: 1,
locale: 'all'
locale: 'all',
depth: 1
});
const resolvedHomeSlug = homeSlug ?? extractSlugInLocale(settings.homepage, defaultLocale) ?? 'home';
// Which collections to walk: pages (no prefix) + each content collection with
@@ -105,32 +121,24 @@ import { buildHreflangAlternates } from './hreflang.js';
// alternates without re-querying per locale.
const result = await payload.find({
collection: collection.slug,
locale: 'all',
depth: 0,
limit: 0,
locale: 'all',
pagination: false
});
for (const raw of result.docs){
if (!isIndexable(raw)) {
continue;
}
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);
}
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;
}
if (!rel || typeof rel !== 'object') return undefined;
const slug = rel.slug;
if (typeof slug === 'string') {
return slug;
}
if (typeof slug === 'string') return slug;
if (slug && typeof slug === 'object') {
const v = slug[locale];
return typeof v === 'string' ? v : undefined;
@@ -138,19 +146,13 @@ import { buildHreflangAlternates } from './hreflang.js';
return undefined;
}
/** Builds a locale→slug map from a populated archive relationship. */ function slugMapAllLocales(rel, locales) {
if (!rel || typeof rel !== 'object') {
return undefined;
}
if (!rel || typeof rel !== 'object') return undefined;
const slug = rel.slug;
if (!slug || typeof slug !== 'object') {
return undefined;
}
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;
}
if (typeof v === 'string') map[locale] = v;
}
return Object.keys(map).length ? map : undefined;
}