added auto generating for sitemap.

This commit is contained in:
2026-07-31 23:12:43 +02:00
parent 24a1fe0d60
commit f6b4b36df8
10 changed files with 428 additions and 12 deletions
+14
View File
@@ -453,6 +453,20 @@ rm -f *.db *.db-shm *.db-wal && pnpm generate:types && pnpm dev
Adres wpisów = slug strony-archiwum. Zmiana tytułu strony przenosi sekcję. Kolejny Adres wpisów = slug strony-archiwum. Zmiana tytułu strony przenosi sekcję. Kolejny
typ treści (realizacje) = kolejna kolekcja + kolejna pozycja w content.config. typ treści (realizacje) = kolejna kolekcja + kolejna pozycja w content.config.
### Sitemapa i robots.txt
`createContentHelpers` oddaje gotowe handlery — dodaj `i18n` i `baseUrl` do jego
argumentów (patrz seo.md), potem dwa pliki po jednej linii:
```ts
// app/sitemap.ts
export { sitemap as default } from '@/lib/content'
// app/robots.ts
export { robots as default } from '@/lib/content'
```
Sitemapa z hreflangiem per URL, lastmod, wpisami bloga; pomija drafty i noindex.
### Analytics ### Analytics
**Site Integrations** → GA4 Measurement ID albo GTM Container ID. Tagi ładują **Site Integrations** → GA4 Measurement ID albo GTM Container ID. Tagi ładują
+51
View File
@@ -197,3 +197,54 @@ const slugs = await slugsAcrossLocales({ payload, collection: 'pages', id, confi
zamienia KAŻDE zlokalizowane pole w mapę, co jest dobre dla sluga i złe dla zamienia KAŻDE zlokalizowane pole w mapę, co jest dobre dla sluga i złe dla
reszty (mapa w title rozłożyłaby składanie tytułu). Dlatego czyta tylko slug, reszty (mapa w title rozłożyłaby składanie tytułu). Dlatego czyta tylko slug,
depth 0. depth 0.
## Sitemapa i robots.txt
Plugin składa sitemapę z hreflangiem per URL — te same buildery co metadane, więc
nie rozjedzie się z tym, co strony serwują. Handlery są gotowe w
`createContentHelpers`; klient re-eksportuje je jedną linią.
```ts
// src/lib/content.ts
export const { /* ... */, sitemap, robots } = createContentHelpers({
config,
content: contentConfig,
i18n: i18nConfig, // wymagane dla sitemap (hreflang)
baseUrl: process.env.NEXT_PUBLIC_SERVER_URL,
})
```
```ts
// app/sitemap.ts
export { sitemap as default } from '@/lib/content'
// app/robots.ts
export { robots as default } from '@/lib/content'
```
Next generuje z tego `/sitemap.xml` i `/robots.txt`. Nie da się tego schować
całkiem w pluginie — Next tworzy te trasy wyłącznie z plików w `app/`, skanuje
katalog projektu, nie node_modules. Ale re-eksport to maksimum redukcji: cała
logika jest w pluginie.
Co zawiera sitemapa:
- każdą stronę i wpis bloga, URL w domyślnym locale
- `alternates.languages` → Next renderuje `<xhtml:link rel="alternate" hreflang>`
per URL (to czyta Google przy wielu językach; zwykła sitemapa tego nie ma)
- `lastModified` z realnego `updatedAt` dokumentu
- wpisy bloga z prefiksem archiwum (`/pl/artykuly/moj-post`)
Pomija: drafty (`_status !== 'published'`) i dokumenty z `meta.noindex`.
Niskopoziomowo (własna trasa zamiast handlera z fabryki):
```ts
import { buildSitemapEntries, buildRobots } from 'ipal-kit'
const entries = await buildSitemapEntries({
payload, config: i18nConfig, baseUrl, content: contentConfig,
})
```
Przy dziesiątkach tysięcy URL-i Next ma `generateSitemaps` do dzielenia na
części — dodasz, gdyby realnie było ich tyle. Jedna sitemapa wystarcza do ~50k.
+32 -6
View File
@@ -8,12 +8,9 @@ importers:
.: .:
dependencies: dependencies:
'@payloadcms/plugin-form-builder': '@intecion/ipal-kit':
specifier: 3.84.1 specifier: file:intecion-ipal-kit-1.0.0.tgz
version: 3.84.1(@types/[email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected]) version: file:intecion-ipal-kit-1.0.0.tgz(@payloadcms/[email protected](@types/[email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected]))(@payloadcms/[email protected](@types/[email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected]))([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected])
'@payloadcms/plugin-seo':
specifier: 3.84.1
version: 3.84.1(@types/[email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])
lucide-react: lucide-react:
specifier: ^0.400.0 specifier: ^0.400.0
version: 0.400.0([email protected]) version: 0.400.0([email protected])
@@ -42,6 +39,12 @@ importers:
'@payloadcms/next': '@payloadcms/next':
specifier: 3.84.1 specifier: 3.84.1
version: 3.84.1(@types/[email protected])([email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected]) version: 3.84.1(@types/[email protected])([email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])
'@payloadcms/plugin-form-builder':
specifier: 3.84.1
version: 3.84.1(@types/[email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])
'@payloadcms/plugin-seo':
specifier: 3.84.1
version: 3.84.1(@types/[email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])
'@payloadcms/richtext-lexical': '@payloadcms/richtext-lexical':
specifier: 3.84.1 specifier: 3.84.1
version: 3.84.1(@faceless-ui/[email protected]([email protected]([email protected]))([email protected]))(@faceless-ui/[email protected]([email protected]([email protected]))([email protected]))(@payloadcms/[email protected](@types/[email protected])([email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected]))(@types/[email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])([email protected]) version: 3.84.1(@faceless-ui/[email protected]([email protected]([email protected]))([email protected]))(@faceless-ui/[email protected]([email protected]([email protected]))([email protected]))(@payloadcms/[email protected](@types/[email protected])([email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected]))(@types/[email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])([email protected])
@@ -1345,6 +1348,17 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@intecion/ipal-kit@file:intecion-ipal-kit-1.0.0.tgz':
resolution: {integrity: sha512-4nKuQXeg6fZ/5fMmf4145sDx006j/KpVIMywGv7CTqqRLrZV1R6oIg+8XwFcodN5RXP+ksMwAW8bdizLc/Cdqg==, tarball: file:intecion-ipal-kit-1.0.0.tgz}
version: 1.0.0
engines: {node: ^18.20.2 || >=20.9.0, pnpm: ^9 || ^10 || ^11}
peerDependencies:
'@payloadcms/plugin-form-builder': ^3.84.1
'@payloadcms/plugin-seo': ^3.84.1
next: '>=15'
payload: ^3.84.1
react: ^19.0.0
'@jridgewell/[email protected]': '@jridgewell/[email protected]':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@@ -6603,6 +6617,18 @@ snapshots:
'@img/[email protected]': '@img/[email protected]':
optional: true optional: true
'@intecion/ipal-kit@file:intecion-ipal-kit-1.0.0.tgz(@payloadcms/[email protected](@types/[email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected]))(@payloadcms/[email protected](@types/[email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected]))([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected])':
dependencies:
'@payloadcms/plugin-form-builder': 3.84.1(@types/[email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])
'@payloadcms/plugin-seo': 3.84.1(@types/[email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])
lucide-react: 0.400.0([email protected])
next: 16.2.6(@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected])
nodemailer: 8.0.11
payload: 3.84.1([email protected])([email protected])
react: 19.2.6
server-only: 0.0.1
slugify: 1.6.9
'@jridgewell/[email protected]': '@jridgewell/[email protected]':
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
+1 -1
View File
@@ -2,7 +2,7 @@
* Entry point: ipal-kit/next/middleware * Entry point: ipal-kit/next/middleware
* *
* Re-exports the locale middleware factory and default matcher for use in a * Re-exports the locale middleware factory and default matcher for use in a
* client project's next-middleware.ts. Kept as a thin re-export so the Next-facing * client project's middleware.ts. Kept as a thin re-export so the Next-facing
* surface is separate from the main package export. * surface is separate from the main package export.
*/ */
export { createLocaleMiddleware, DEFAULT_MIDDLEWARE_MATCHER } from '../modules/i18n/index.js' export { createLocaleMiddleware, DEFAULT_MIDDLEWARE_MATCHER } from '../modules/i18n/index.js'
+4 -2
View File
@@ -83,13 +83,15 @@ export {
} from './modules/payload/index.js' } from './modules/payload/index.js'
export type { PageMetadata, SeoMeta, SeoOption } from './modules/seo/index.js' export type { PageMetadata, SeoMeta, SeoOption } from './modules/seo/index.js'
export { buildHreflangAlternates, buildMetadata, composeTitle } from './modules/seo/index.js' export { buildHreflangAlternates, buildMetadata, composeTitle } from './modules/seo/index.js'
export type { AutoFillMapping } from './modules/seo/index.js' export type { AutoFillMapping, RobotsRules, SitemapEntry } from './modules/seo/index.js'
export { export {
buildAutoFillMetaHook, buildAutoFillMetaHook,
buildRobots,
buildSitemapEntries,
createMetadataGenerator, createMetadataGenerator,
createPageMetadata, createPageMetadata,
injectAutoFillMeta, injectAutoFillMeta,
} from './modules/seo/index.js' } from './modules/seo/index.js'
export { buildSlugField, toSlug } from './modules/slug/index.js' export { buildSlugField, toSlug } from './modules/slug/index.js'
export { default as ipalKit } from './plugin.js' export { ipalKit } from './plugin.js'
export type { IpalOptions } from './types.js' export type { IpalOptions } from './types.js'
+59 -1
View File
@@ -4,10 +4,19 @@ import { getPayload } from 'payload'
import { cache } from 'react' import { cache } from 'react'
import type { ArchiveEntries, ContentOption, ResolvedRoute } from '../content/index.js' import type { ArchiveEntries, ContentOption, ResolvedRoute } from '../content/index.js'
import type { I18nConfig } from '../i18n/index.js'
import type { RobotsRules, SitemapEntry } from '../seo/index.js'
import { getArchiveEntries, resolveRoute as resolveRouteRaw } from '../content/index.js' import { getArchiveEntries, resolveRoute as resolveRouteRaw } from '../content/index.js'
import { buildRobots, buildSitemapEntries } from '../seo/index.js'
type CreateContentHelpersArgs = { type CreateContentHelpersArgs = {
/**
* Absolute site origin for sitemap/robots URLs. Falls back to
* NEXT_PUBLIC_SERVER_URL, then to a relative origin (which most crawlers
* reject, so set one in production).
*/
baseUrl?: string
/** /**
* The client's payload config promise (the default export of payload.config). * The client's payload config promise (the default export of payload.config).
* Passed in because the plugin never imports the client's config directly. * Passed in because the plugin never imports the client's config directly.
@@ -15,6 +24,11 @@ type CreateContentHelpersArgs = {
config: Promise<SanitizedConfig> | SanitizedConfig config: Promise<SanitizedConfig> | SanitizedConfig
/** Archive-backed collections, same value as the plugin option. */ /** Archive-backed collections, same value as the plugin option. */
content?: ContentOption content?: ContentOption
/**
* i18n config. Required only if you want the ready-made `sitemap` / `robots`
* handlers — they need the locale list to emit hreflang.
*/
i18n?: I18nConfig
/** Pages collection slug. Defaults to 'pages'. */ /** Pages collection slug. Defaults to 'pages'. */
pagesSlug?: string pagesSlug?: string
/** SiteSettings global slug. Defaults to 'site-settings'. */ /** SiteSettings global slug. Defaults to 'site-settings'. */
@@ -48,11 +62,14 @@ type CreateContentHelpersArgs = {
* not the second, and a page component composes them in two obvious lines. * not the second, and a page component composes them in two obvious lines.
*/ */
export function createContentHelpers({ export function createContentHelpers({
baseUrl,
config, config,
content, content,
i18n,
pagesSlug = 'pages', pagesSlug = 'pages',
settingsSlug = 'site-settings', settingsSlug = 'site-settings',
}: CreateContentHelpersArgs) { }: CreateContentHelpersArgs) {
const origin = baseUrl ?? process.env.NEXT_PUBLIC_SERVER_URL ?? ''
const getCachedPayload = cache(async (): Promise<BasePayload> => const getCachedPayload = cache(async (): Promise<BasePayload> =>
getPayload({ config: await config }), getPayload({ config: await config }),
) )
@@ -92,5 +109,46 @@ export function createContentHelpers({
}, },
) )
return { getCachedPayload, getConfiguredLocales, getEntries, getSettings, resolveRoute } /**
* Ready-made handler for Next's `app/sitemap.ts` — every page and entry with
* per-URL hreflang and lastmod. Re-export it directly:
*
* ```ts
* // app/sitemap.ts
* export { sitemap as default } from '@/lib/content'
* ```
*/
const sitemap = cache(async (): Promise<SitemapEntry[]> => {
if (!i18n) {
throw new Error('[ipal] createContentHelpers: pass `i18n` to use the sitemap handler.')
}
return buildSitemapEntries({
baseUrl: origin,
config: i18n,
content,
pagesSlug,
payload: await getCachedPayload(),
settingsSlug,
})
})
/**
* Ready-made handler for Next's `app/robots.ts`. Re-export directly:
*
* ```ts
* // app/robots.ts
* export { robots as default } from '@/lib/content'
* ```
*/
const robots = (): RobotsRules => buildRobots({ baseUrl: origin })
return {
getCachedPayload,
getConfiguredLocales,
getEntries,
getSettings,
resolveRoute,
robots,
sitemap,
}
} }
+49
View File
@@ -0,0 +1,49 @@
type BuildRobotsArgs = {
/** Absolute origin, e.g. 'https://example.com'. */
baseUrl: string
/** Paths to disallow. Defaults to Payload's admin and API. */
disallow?: string[]
/** Whether to advertise the sitemap. Defaults to true. */
sitemap?: boolean
}
/**
* Shape for Next's `app/robots.ts`.
*/
export type RobotsRules = {
host?: string
rules: { allow?: string; disallow?: string[]; userAgent: string }
sitemap?: string
}
/**
* Builds robots rules that allow crawling everything except the admin panel and
* API, and point crawlers at the sitemap.
*
* ```ts
* // app/robots.ts
* export default function robots() {
* return buildRobots({ baseUrl: process.env.NEXT_PUBLIC_SERVER_URL! })
* }
* ```
*
* The admin and API are disallowed by default — no reason for them in the index,
* and it keeps crawl budget on real content.
*/
export function buildRobots({
baseUrl,
disallow = ['/admin', '/api'],
sitemap = true,
}: BuildRobotsArgs): RobotsRules {
const origin = baseUrl.replace(/\/$/, '')
return {
rules: {
allow: '/',
disallow,
userAgent: '*',
},
...(sitemap ? { sitemap: `${origin}/sitemap.xml` } : {}),
host: origin,
}
}
+212
View File
@@ -0,0 +1,212 @@
import type { BasePayload } from 'payload'
import type { ContentOption } from '../content/index.js'
import type { I18nConfig } from '../i18n/index.js'
import { archiveFieldName } from '../content/index.js'
import { buildLocalizedPath, getLocalizedSlugs } from '../i18n/index.js'
import { buildHreflangAlternates } from './hreflang.js'
/**
* One sitemap entry, shaped for Next's `app/sitemap.ts`.
*
* `alternates.languages` is the important part for a multilingual site: Next
* renders it as `<xhtml:link rel="alternate" hreflang=…>` per URL, which is
* exactly what Google uses to connect language versions. A sitemap without it
* is the common, weaker kind.
*/
export type SitemapEntry = {
alternates?: { languages: Record<string, string> }
changeFrequency?: 'always' | 'daily' | 'hourly' | 'monthly' | 'never' | 'weekly' | 'yearly'
lastModified?: Date | string
priority?: number
url: string
}
type CollectionEntry = {
/** Localized segment for entries (archive page slugs), when applicable. */
prefixSlugs?: Record<string, string>
slug: string
}
type BuildSitemapArgs = {
/** Absolute origin, e.g. 'https://example.com'. Required for valid sitemap URLs. */
baseUrl: string
changeFrequency?: SitemapEntry['changeFrequency']
config: I18nConfig
/** Archive-backed collections, same value as the plugin option. */
content?: ContentOption
/**
* Slug of the page that is the site root (collapses to /{locale}).
* Read from System Pages when omitted.
*/
homeSlug?: string
/** Pages collection slug. Defaults to 'pages'. */
pagesSlug?: string
payload: BasePayload
/** SiteSettings global slug. Defaults to 'site-settings'. */
settingsSlug?: string
}
type DocRow = {
_status?: string
id: number | string
meta?: { noindex?: boolean } | null
slug?: unknown
updatedAt?: string
}
/** Skip drafts and anything flagged noindex in the SEO tab. */
function isIndexable(doc: DocRow): boolean {
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: DocRow,
locale: string,
config: I18nConfig,
baseUrl: string,
homeSlug: string | undefined,
prefix: Record<string, string> | undefined,
changeFrequency: SitemapEntry['changeFrequency'],
): null | SitemapEntry {
const slugs =
doc.slug && typeof doc.slug === 'object'
? getLocalizedSlugs({ config, slugField: doc.slug as Record<string, unknown> })
: {}
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',
}: BuildSitemapArgs): Promise<SitemapEntry[]> {
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 as never,
depth: 1,
locale: 'all' as never,
})) as Record<string, unknown>
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: CollectionEntry[] = [{ 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: SitemapEntry[] = []
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 as never,
depth: 0,
limit: 0, // no pagination — sitemap wants everything
locale: 'all' as never,
pagination: false as never,
})
for (const raw of result.docs as DocRow[]) {
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: unknown, locale: string): string | undefined {
if (!rel || typeof rel !== 'object') {return undefined}
const slug = (rel as { slug?: unknown }).slug
if (typeof slug === 'string') {return slug}
if (slug && typeof slug === 'object') {
const v = (slug as Record<string, unknown>)[locale]
return typeof v === 'string' ? v : undefined
}
return undefined
}
/** Builds a locale→slug map from a populated archive relationship. */
function slugMapAllLocales(rel: unknown, locales: string[]): Record<string, string> | undefined {
if (!rel || typeof rel !== 'object') {return undefined}
const slug = (rel as { slug?: unknown }).slug
if (!slug || typeof slug !== 'object') {return undefined}
const map: Record<string, string> = {}
for (const locale of locales) {
const v = (slug as Record<string, unknown>)[locale]
if (typeof v === 'string') {map[locale] = v}
}
return Object.keys(map).length ? map : undefined
}
+4
View File
@@ -2,6 +2,10 @@ export { buildAutoFillMetaHook } from './autoFillMeta.js'
export type { AutoFillMapping } from './autoFillMeta.js' export type { AutoFillMapping } from './autoFillMeta.js'
export { buildMetadata } from './buildMetadata.js' export { buildMetadata } from './buildMetadata.js'
export type { PageMetadata } from './buildMetadata.js' export type { PageMetadata } from './buildMetadata.js'
export { buildRobots } from './buildRobots.js'
export type { RobotsRules } from './buildRobots.js'
export { buildSitemapEntries } from './buildSitemapEntries.js'
export type { SitemapEntry } from './buildSitemapEntries.js'
export { composeTitle } from './composeTitle.js' export { composeTitle } from './composeTitle.js'
export type { TitleOrder } from './composeTitle.js' export type { TitleOrder } from './composeTitle.js'
export { createMetadataGenerator } from './createMetadataGenerator.js' export { createMetadataGenerator } from './createMetadataGenerator.js'