added auto generating for sitemap.
This commit is contained in:
@@ -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ą
|
||||||
|
|||||||
+52
-1
@@ -196,4 +196,55 @@ const slugs = await slugsAcrossLocales({ payload, collection: 'pages', id, confi
|
|||||||
`slugsAcrossLocales` robi osobne zapytanie z `locale: 'all'` — bo ten tryb
|
`slugsAcrossLocales` robi osobne zapytanie z `locale: 'all'` — bo ten tryb
|
||||||
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.
|
||||||
+1
-1
@@ -149,4 +149,4 @@
|
|||||||
"unrs-resolver"
|
"unrs-resolver"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Generated
+32
-6
@@ -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
@@ -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'
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user