From e4329f76b177700386689f141e64853d41082dec Mon Sep 17 00:00:00 2001 From: rasm-its Date: Wed, 9 Sep 2026 22:21:04 +0200 Subject: [PATCH] Fixed buildSitemapEntries.ts --- dist/modules/seo/buildSitemapEntries.js | 8 +++++++- dist/modules/seo/buildSitemapEntries.js.map | 2 +- src/modules/seo/buildSitemapEntries.ts | 11 ++++++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/dist/modules/seo/buildSitemapEntries.js b/dist/modules/seo/buildSitemapEntries.js index 6db0b55..a8a27f6 100644 --- a/dist/modules/seo/buildSitemapEntries.js +++ b/dist/modules/seo/buildSitemapEntries.js @@ -110,7 +110,13 @@ import { buildHreflangAlternates } from './hreflang.js'; depth: 1, locale: 'all' }); - const resolvedHomeSlug = homeSlug ?? extractSlugInLocale(settings.homepage, defaultLocale) ?? 'home'; + // 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 = [ diff --git a/dist/modules/seo/buildSitemapEntries.js.map b/dist/modules/seo/buildSitemapEntries.js.map index a63ed89..d9f07c3 100644 --- a/dist/modules/seo/buildSitemapEntries.js.map +++ b/dist/modules/seo/buildSitemapEntries.js.map @@ -1 +1 @@ -{"version":3,"sources":["../../../src/modules/seo/buildSitemapEntries.ts"],"sourcesContent":["import type { BasePayload } from 'payload'\n\nimport type { ContentOption } from '../content/index.js'\nimport type { I18nConfig } from '../i18n/index.js'\n\nimport { archiveFieldName } from '../content/index.js'\nimport { buildLocalizedPath, getLocaleCodes , getLocalizedSlugs } from '../i18n/index.js'\nimport { buildHreflangAlternates } from './hreflang.js'\n\n/**\n * One sitemap entry, shaped for Next's `app/sitemap.ts`.\n *\n * `alternates.languages` is the important part for a multilingual site: Next\n * renders it as `` per URL, which is\n * exactly what Google uses to connect language versions. A sitemap without it\n * is the common, weaker kind.\n */\nexport type SitemapEntry = {\n alternates?: { languages: Record }\n changeFrequency?: 'always' | 'daily' | 'hourly' | 'monthly' | 'never' | 'weekly' | 'yearly'\n lastModified?: Date | string\n priority?: number\n url: string\n}\n\ntype CollectionEntry = {\n /** Localized segment for entries (archive page slugs), when applicable. */\n prefixSlugs?: Record\n slug: string\n}\n\ntype BuildSitemapArgs = {\n /** Absolute origin, e.g. 'https://example.com'. Required for valid sitemap URLs. */\n baseUrl: string\n changeFrequency?: SitemapEntry['changeFrequency']\n config: I18nConfig\n /** Archive-backed collections, same value as the plugin option. */\n content?: ContentOption\n /**\n * Slug of the page that is the site root (collapses to /{locale}).\n * Read from System Pages when omitted.\n */\n homeSlug?: Record | string\n /** Pages collection slug. Defaults to 'pages'. */\n pagesSlug?: string\n payload: BasePayload\n /** SiteSettings global slug. Defaults to 'site-settings'. */\n settingsSlug?: string\n}\n\ntype DocRow = {\n _status?: string\n id: number | string\n meta?: { noindex?: boolean } | null\n slug?: unknown\n updatedAt?: string\n}\n\n/**\n * Slugs that must never appear in the sitemap — error/system pages that exist as\n * documents (e.g. a '404' page in the Pages collection) but should not be\n * indexed. A sitemap should list only real, HTTP-200 content; a '/pl/404' entry\n * is an audit finding. Matched against the slug in any locale.\n */\nconst EXCLUDED_SITEMAP_SLUGS = new Set(['404', '500', 'error', 'not-found'])\n\n/** True if the doc's slug (in any locale) is an excluded system/error slug. */\nfunction hasExcludedSlug(slug: unknown): boolean {\n if (typeof slug === 'string') {return EXCLUDED_SITEMAP_SLUGS.has(slug)}\n if (slug && typeof slug === 'object') {\n for (const value of Object.values(slug as Record)) {\n if (typeof value === 'string' && EXCLUDED_SITEMAP_SLUGS.has(value)) {return true}\n }\n }\n return false\n}\n\n/** Skip drafts, noindex, and system/error pages (404 etc.). */\nfunction isIndexable(doc: DocRow): boolean {\n if (doc._status && doc._status !== 'published') {return false}\n if (doc.meta?.noindex) {return false}\n if (hasExcludedSlug(doc.slug)) {return false}\n return true\n}\n\n/**\n * Turns one document into a sitemap entry per default locale isn't needed —\n * one entry with all languages as alternates is the correct, de-duplicated\n * shape. The `url` is the current locale's path; `alternates.languages` carries\n * every locale (including itself, per Google's guidance).\n */\nfunction entryFor(\n doc: DocRow,\n locale: string,\n config: I18nConfig,\n baseUrl: string,\n homeSlug: Record | string | undefined,\n prefix: Record | undefined,\n changeFrequency: SitemapEntry['changeFrequency'],\n): null | SitemapEntry {\n const slugs =\n doc.slug && typeof doc.slug === 'object'\n ? getLocalizedSlugs({ config, slugField: doc.slug as Record })\n : {}\n\n const path = buildLocalizedPath({ config, homeSlug, locale, prefix, slugs })\n if (!path) {return null}\n\n const origin = baseUrl.replace(/\\/$/, '')\n const languages = buildHreflangAlternates({ baseUrl, config, homeSlug, prefix, slugs })\n\n return {\n url: `${origin}${path}`,\n ...(doc.updatedAt ? { lastModified: doc.updatedAt } : {}),\n ...(changeFrequency ? { changeFrequency } : {}),\n ...(Object.keys(languages).length ? { alternates: { languages } } : {}),\n }\n}\n\n/**\n * Collects every public URL — pages and archive entries — as sitemap entries\n * with per-URL hreflang and lastmod.\n *\n * Reuses the same path/hreflang builders as page metadata, so the sitemap can't\n * drift from what the pages actually render (a classic source of sitemap bugs:\n * a URL listed one way and served another). Drafts and noindex documents are\n * omitted.\n *\n * ```ts\n * // app/sitemap.ts\n * export default async function sitemap() {\n * return buildSitemapEntries({\n * payload: await getPayload({ config }),\n * config: i18nConfig,\n * baseUrl: process.env.NEXT_PUBLIC_SERVER_URL!,\n * content: contentConfig,\n * })\n * }\n * ```\n */\nexport async function buildSitemapEntries({\n baseUrl,\n changeFrequency = 'weekly',\n config,\n content,\n homeSlug,\n pagesSlug = 'pages',\n payload,\n settingsSlug = 'site-settings',\n}: BuildSitemapArgs): Promise {\n const locales = config.locales.map((l) => l.code)\n const defaultLocale = config.defaultLocale\n\n // Resolve homeSlug and archive prefixes from System Pages (read once, in all\n // locales so archive prefixes are available per language).\n const settings = (await payload.findGlobal({\n slug: settingsSlug as never,\n depth: 1,\n locale: 'all' as never,\n })) as Record\n\n const resolvedHomeSlug =\n homeSlug ?? extractSlugInLocale(settings.homepage, defaultLocale) ?? 'home'\n\n // Which collections to walk: pages (no prefix) + each content collection with\n // its archive-page slugs as the localized prefix.\n const collections: CollectionEntry[] = [{ slug: pagesSlug }]\n\n for (const c of content?.collections ?? []) {\n const archive = settings[archiveFieldName(c.slug)]\n const prefixSlugs = archive ? slugMapAllLocales(archive, locales) : undefined\n collections.push({ slug: c.slug, prefixSlugs })\n }\n\n const entries: SitemapEntry[] = []\n\n for (const collection of collections) {\n // Read every doc once, across locales, so one row yields all its language\n // alternates without re-querying per locale.\n const result = await payload.find({\n collection: collection.slug as never,\n depth: 0,\n limit: 0, // no pagination — sitemap wants everything\n locale: 'all' as never,\n pagination: false as never,\n })\n\n for (const raw of result.docs as DocRow[]) {\n if (!isIndexable(raw)) {continue}\n\n // Emit a separate for EACH locale. Google's sitemap spec for\n // localized sites requires one per language version (each with its\n // own plus xhtml:link alternates), not a single default-locale \n // with the others hidden only in alternates. Emitting only the default\n // locale makes GSC count 8 URLs instead of 8×3 — a real reporting gap.\n for (const locale of getLocaleCodes(config)) {\n const entry = entryFor(\n raw,\n locale,\n config,\n baseUrl,\n resolvedHomeSlug,\n collection.prefixSlugs,\n changeFrequency,\n )\n if (entry) {entries.push(entry)}\n }\n }\n }\n\n return entries\n}\n\n/** Pulls a slug string from a populated relationship in a specific locale. */\nfunction extractSlugInLocale(rel: unknown, locale: string): string | undefined {\n if (!rel || typeof rel !== 'object') {return undefined}\n const slug = (rel as { slug?: unknown }).slug\n if (typeof slug === 'string') {return slug}\n if (slug && typeof slug === 'object') {\n const v = (slug as Record)[locale]\n return typeof v === 'string' ? v : undefined\n }\n return undefined\n}\n\n/** Builds a locale→slug map from a populated archive relationship. */\nfunction slugMapAllLocales(rel: unknown, locales: string[]): Record | undefined {\n if (!rel || typeof rel !== 'object') {return undefined}\n const slug = (rel as { slug?: unknown }).slug\n if (!slug || typeof slug !== 'object') {return undefined}\n\n const map: Record = {}\n for (const locale of locales) {\n const v = (slug as Record)[locale]\n if (typeof v === 'string') {map[locale] = v}\n }\n return Object.keys(map).length ? map : undefined\n}\n"],"names":["archiveFieldName","buildLocalizedPath","getLocaleCodes","getLocalizedSlugs","buildHreflangAlternates","EXCLUDED_SITEMAP_SLUGS","Set","hasExcludedSlug","slug","has","value","Object","values","isIndexable","doc","_status","meta","noindex","entryFor","locale","config","baseUrl","homeSlug","prefix","changeFrequency","slugs","slugField","path","origin","replace","languages","url","updatedAt","lastModified","keys","length","alternates","buildSitemapEntries","content","pagesSlug","payload","settingsSlug","locales","map","l","code","defaultLocale","settings","findGlobal","depth","resolvedHomeSlug","extractSlugInLocale","homepage","collections","c","archive","prefixSlugs","slugMapAllLocales","undefined","push","entries","collection","result","find","limit","pagination","raw","docs","entry","rel","v"],"mappings":"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"} \ No newline at end of file +{"version":3,"sources":["../../../src/modules/seo/buildSitemapEntries.ts"],"sourcesContent":["import type { BasePayload } from 'payload'\n\nimport type { ContentOption } from '../content/index.js'\nimport type { I18nConfig } from '../i18n/index.js'\n\nimport { archiveFieldName } from '../content/index.js'\nimport { buildLocalizedPath, getLocaleCodes , getLocalizedSlugs } from '../i18n/index.js'\nimport { buildHreflangAlternates } from './hreflang.js'\n\n/**\n * One sitemap entry, shaped for Next's `app/sitemap.ts`.\n *\n * `alternates.languages` is the important part for a multilingual site: Next\n * renders it as `` per URL, which is\n * exactly what Google uses to connect language versions. A sitemap without it\n * is the common, weaker kind.\n */\nexport type SitemapEntry = {\n alternates?: { languages: Record }\n changeFrequency?: 'always' | 'daily' | 'hourly' | 'monthly' | 'never' | 'weekly' | 'yearly'\n lastModified?: Date | string\n priority?: number\n url: string\n}\n\ntype CollectionEntry = {\n /** Localized segment for entries (archive page slugs), when applicable. */\n prefixSlugs?: Record\n slug: string\n}\n\ntype BuildSitemapArgs = {\n /** Absolute origin, e.g. 'https://example.com'. Required for valid sitemap URLs. */\n baseUrl: string\n changeFrequency?: SitemapEntry['changeFrequency']\n config: I18nConfig\n /** Archive-backed collections, same value as the plugin option. */\n content?: ContentOption\n /**\n * Slug of the page that is the site root (collapses to /{locale}).\n * Read from System Pages when omitted.\n */\n homeSlug?: Record | string\n /** Pages collection slug. Defaults to 'pages'. */\n pagesSlug?: string\n payload: BasePayload\n /** SiteSettings global slug. Defaults to 'site-settings'. */\n settingsSlug?: string\n}\n\ntype DocRow = {\n _status?: string\n id: number | string\n meta?: { noindex?: boolean } | null\n slug?: unknown\n updatedAt?: string\n}\n\n/**\n * Slugs that must never appear in the sitemap — error/system pages that exist as\n * documents (e.g. a '404' page in the Pages collection) but should not be\n * indexed. A sitemap should list only real, HTTP-200 content; a '/pl/404' entry\n * is an audit finding. Matched against the slug in any locale.\n */\nconst EXCLUDED_SITEMAP_SLUGS = new Set(['404', '500', 'error', 'not-found'])\n\n/** True if the doc's slug (in any locale) is an excluded system/error slug. */\nfunction hasExcludedSlug(slug: unknown): boolean {\n if (typeof slug === 'string') {return EXCLUDED_SITEMAP_SLUGS.has(slug)}\n if (slug && typeof slug === 'object') {\n for (const value of Object.values(slug as Record)) {\n if (typeof value === 'string' && EXCLUDED_SITEMAP_SLUGS.has(value)) {return true}\n }\n }\n return false\n}\n\n/** Skip drafts, noindex, and system/error pages (404 etc.). */\nfunction isIndexable(doc: DocRow): boolean {\n if (doc._status && doc._status !== 'published') {return false}\n if (doc.meta?.noindex) {return false}\n if (hasExcludedSlug(doc.slug)) {return false}\n return true\n}\n\n/**\n * Turns one document into a sitemap entry per default locale isn't needed —\n * one entry with all languages as alternates is the correct, de-duplicated\n * shape. The `url` is the current locale's path; `alternates.languages` carries\n * every locale (including itself, per Google's guidance).\n */\nfunction entryFor(\n doc: DocRow,\n locale: string,\n config: I18nConfig,\n baseUrl: string,\n homeSlug: Record | string | undefined,\n prefix: Record | undefined,\n changeFrequency: SitemapEntry['changeFrequency'],\n): null | SitemapEntry {\n const slugs =\n doc.slug && typeof doc.slug === 'object'\n ? getLocalizedSlugs({ config, slugField: doc.slug as Record })\n : {}\n\n const path = buildLocalizedPath({ config, homeSlug, locale, prefix, slugs })\n if (!path) {return null}\n\n const origin = baseUrl.replace(/\\/$/, '')\n const languages = buildHreflangAlternates({ baseUrl, config, homeSlug, prefix, slugs })\n\n return {\n url: `${origin}${path}`,\n ...(doc.updatedAt ? { lastModified: doc.updatedAt } : {}),\n ...(changeFrequency ? { changeFrequency } : {}),\n ...(Object.keys(languages).length ? { alternates: { languages } } : {}),\n }\n}\n\n/**\n * Collects every public URL — pages and archive entries — as sitemap entries\n * with per-URL hreflang and lastmod.\n *\n * Reuses the same path/hreflang builders as page metadata, so the sitemap can't\n * drift from what the pages actually render (a classic source of sitemap bugs:\n * a URL listed one way and served another). Drafts and noindex documents are\n * omitted.\n *\n * ```ts\n * // app/sitemap.ts\n * export default async function sitemap() {\n * return buildSitemapEntries({\n * payload: await getPayload({ config }),\n * config: i18nConfig,\n * baseUrl: process.env.NEXT_PUBLIC_SERVER_URL!,\n * content: contentConfig,\n * })\n * }\n * ```\n */\nexport async function buildSitemapEntries({\n baseUrl,\n changeFrequency = 'weekly',\n config,\n content,\n homeSlug,\n pagesSlug = 'pages',\n payload,\n settingsSlug = 'site-settings',\n}: BuildSitemapArgs): Promise {\n const locales = config.locales.map((l) => l.code)\n const defaultLocale = config.defaultLocale\n\n // Resolve homeSlug and archive prefixes from System Pages (read once, in all\n // locales so archive prefixes are available per language).\n const settings = (await payload.findGlobal({\n slug: settingsSlug as never,\n depth: 1,\n locale: 'all' as never,\n })) as Record\n\n // Home slug as a per-locale MAP so every language's homepage collapses to its\n // root in the sitemap (/pl, /de, /en) — not just the default locale. Priority:\n // caller-provided homeSlug > full map from the homepage relationship > single\n // default-locale slug > 'home'. slugMapAllLocales already builds the map (it's\n // used for archive prefixes below); reusing it here fixes /de/startseite\n // appearing in the sitemap instead of /de.\n const resolvedHomeSlug =\n homeSlug ??\n slugMapAllLocales(settings.homepage, locales) ??\n extractSlugInLocale(settings.homepage, defaultLocale) ??\n 'home'\n\n // Which collections to walk: pages (no prefix) + each content collection with\n // its archive-page slugs as the localized prefix.\n const collections: CollectionEntry[] = [{ slug: pagesSlug }]\n\n for (const c of content?.collections ?? []) {\n const archive = settings[archiveFieldName(c.slug)]\n const prefixSlugs = archive ? slugMapAllLocales(archive, locales) : undefined\n collections.push({ slug: c.slug, prefixSlugs })\n }\n\n const entries: SitemapEntry[] = []\n\n for (const collection of collections) {\n // Read every doc once, across locales, so one row yields all its language\n // alternates without re-querying per locale.\n const result = await payload.find({\n collection: collection.slug as never,\n depth: 0,\n limit: 0, // no pagination — sitemap wants everything\n locale: 'all' as never,\n pagination: false as never,\n })\n\n for (const raw of result.docs as DocRow[]) {\n if (!isIndexable(raw)) {continue}\n\n // Emit a separate for EACH locale. Google's sitemap spec for\n // localized sites requires one per language version (each with its\n // own plus xhtml:link alternates), not a single default-locale \n // with the others hidden only in alternates. Emitting only the default\n // locale makes GSC count 8 URLs instead of 8×3 — a real reporting gap.\n for (const locale of getLocaleCodes(config)) {\n const entry = entryFor(\n raw,\n locale,\n config,\n baseUrl,\n resolvedHomeSlug,\n collection.prefixSlugs,\n changeFrequency,\n )\n if (entry) {entries.push(entry)}\n }\n }\n }\n\n return entries\n}\n\n/** Pulls a slug string from a populated relationship in a specific locale. */\nfunction extractSlugInLocale(rel: unknown, locale: string): string | undefined {\n if (!rel || typeof rel !== 'object') {return undefined}\n const slug = (rel as { slug?: unknown }).slug\n if (typeof slug === 'string') {return slug}\n if (slug && typeof slug === 'object') {\n const v = (slug as Record)[locale]\n return typeof v === 'string' ? v : undefined\n }\n return undefined\n}\n\n/** Builds a locale→slug map from a populated archive relationship. */\nfunction slugMapAllLocales(rel: unknown, locales: string[]): Record | undefined {\n if (!rel || typeof rel !== 'object') {return undefined}\n const slug = (rel as { slug?: unknown }).slug\n if (!slug || typeof slug !== 'object') {return undefined}\n\n const map: Record = {}\n for (const locale of locales) {\n const v = (slug as Record)[locale]\n if (typeof v === 'string') {map[locale] = v}\n }\n return Object.keys(map).length ? map : undefined\n}\n"],"names":["archiveFieldName","buildLocalizedPath","getLocaleCodes","getLocalizedSlugs","buildHreflangAlternates","EXCLUDED_SITEMAP_SLUGS","Set","hasExcludedSlug","slug","has","value","Object","values","isIndexable","doc","_status","meta","noindex","entryFor","locale","config","baseUrl","homeSlug","prefix","changeFrequency","slugs","slugField","path","origin","replace","languages","url","updatedAt","lastModified","keys","length","alternates","buildSitemapEntries","content","pagesSlug","payload","settingsSlug","locales","map","l","code","defaultLocale","settings","findGlobal","depth","resolvedHomeSlug","slugMapAllLocales","homepage","extractSlugInLocale","collections","c","archive","prefixSlugs","undefined","push","entries","collection","result","find","limit","pagination","raw","docs","entry","rel","v"],"mappings":"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"} \ No newline at end of file diff --git a/src/modules/seo/buildSitemapEntries.ts b/src/modules/seo/buildSitemapEntries.ts index 502b127..ac86842 100644 --- a/src/modules/seo/buildSitemapEntries.ts +++ b/src/modules/seo/buildSitemapEntries.ts @@ -159,8 +159,17 @@ export async function buildSitemapEntries({ locale: 'all' as never, })) as Record + // 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 ?? extractSlugInLocale(settings.homepage, defaultLocale) ?? 'home' + 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.