SEO: multilingual homepage (homeSlug per locale in path/sitemap), Article + llms.txt generators, siteDescription fallback, composeTitle dedup
This commit is contained in:
+22
-12
@@ -131,7 +131,10 @@ import { buildRobots, buildSitemapEntries } from '../seo/index.js';
|
|||||||
* Handles automatically:
|
* Handles automatically:
|
||||||
* - pages collection + content collections (with their archive prefix)
|
* - pages collection + content collections (with their archive prefix)
|
||||||
* - excludes the homepage (maps to { slug: [] } — the root)
|
* - excludes the homepage (maps to { slug: [] } — the root)
|
||||||
* - excludes drafts, 404/500/system slugs, and meta.noindex docs
|
* - excludes drafts and 404/500/system slugs
|
||||||
|
* - KEEPS noindex pages (they must still render — noindex controls indexing,
|
||||||
|
* not existence; skipping them would force dynamic rendering)
|
||||||
|
* - localized slugs (string or per-locale map) both handled
|
||||||
* - single-locale → { slug }[]; multi-locale → { locale, slug }[]
|
* - single-locale → { slug }[]; multi-locale → { locale, slug }[]
|
||||||
*
|
*
|
||||||
* Wire it in the project:
|
* Wire it in the project:
|
||||||
@@ -158,24 +161,27 @@ import { buildRobots, buildSitemapEntries } from '../seo/index.js';
|
|||||||
]);
|
]);
|
||||||
const params = [];
|
const params = [];
|
||||||
for (const locale of locales){
|
for (const locale of locales){
|
||||||
|
// NO where:{_status} filter — collections without drafts enabled don't
|
||||||
|
// register the _status field, and querying it throws
|
||||||
|
// "path cannot be queried: _status". We filter drafts in memory below,
|
||||||
|
// which is safe for every collection (with or without drafts).
|
||||||
const result = await payload.find({
|
const result = await payload.find({
|
||||||
collection: pagesSlug,
|
collection: pagesSlug,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
limit: 1000,
|
limit: 1000,
|
||||||
locale: locale ?? 'all',
|
locale: locale ?? 'all'
|
||||||
where: {
|
|
||||||
_status: {
|
|
||||||
not_equals: 'draft'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
for (const raw of result.docs){
|
for (const raw of result.docs){
|
||||||
|
// Draft filter in memory (safe whether or not the collection has drafts).
|
||||||
if (raw._status && raw._status !== 'published') {
|
if (raw._status && raw._status !== 'published') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (raw.meta?.noindex) {
|
// NOTE: unlike the sitemap, we do NOT skip meta.noindex here. A noindex
|
||||||
continue;
|
// page (privacy, cookies, terms) still needs to render — users reach it
|
||||||
}
|
// from the footer and crawlers read its <meta robots=noindex>. Pre-render
|
||||||
|
// it as SSG so it's fast and its <head> is complete; noindex controls
|
||||||
|
// INDEXING, not whether the page exists. Skipping it would force dynamic
|
||||||
|
// rendering (the very streaming problem we're avoiding).
|
||||||
if (homeId && raw.id === homeId) {
|
if (homeId && raw.id === homeId) {
|
||||||
// Homepage → root. Emit an empty-slug param so '/' (or '/pl') builds.
|
// Homepage → root. Emit an empty-slug param so '/' (or '/pl') builds.
|
||||||
const empty = singleLocale ? {
|
const empty = singleLocale ? {
|
||||||
@@ -189,12 +195,16 @@ import { buildRobots, buildSitemapEntries } from '../seo/index.js';
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const slug = typeof raw.slug === 'string' ? raw.slug : undefined;
|
// Slug may be a plain string OR a localized map ({ pl: 'kontakt' }) when
|
||||||
|
// read with locale:'all' or left unflattened. Handle both, or localized
|
||||||
|
// pages get silently dropped.
|
||||||
|
const rawSlug = raw.slug;
|
||||||
|
const slug = typeof rawSlug === 'string' ? rawSlug : rawSlug && typeof rawSlug === 'object' ? rawSlug[locale ?? ''] ?? Object.values(rawSlug)[0] : undefined;
|
||||||
if (!slug || EXCLUDED.has(slug)) {
|
if (!slug || EXCLUDED.has(slug)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Multi-level slugs ('atrakcje/telefon') → array segments.
|
// Multi-level slugs ('atrakcje/telefon') → array segments.
|
||||||
const segments = slug.split('/').filter(Boolean);
|
const segments = String(slug).split('/').filter(Boolean);
|
||||||
params.push(singleLocale ? {
|
params.push(singleLocale ? {
|
||||||
slug: segments
|
slug: segments
|
||||||
} : {
|
} : {
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+13
-2
@@ -80,8 +80,19 @@ export { generateStaticParams } from '@/lib/content'
|
|||||||
```
|
```
|
||||||
|
|
||||||
Helper automatycznie: pobiera pages + kolekcje treści, wyklucza homepage (→ root),
|
Helper automatycznie: pobiera pages + kolekcje treści, wyklucza homepage (→ root),
|
||||||
drafty, 404/500, noindex; zwraca `{slug}[]` (jednojęzyczny) albo
|
drafty, 404/500; zwraca `{slug}[]` (jednojęzyczny) albo `{locale, slug}[]`
|
||||||
`{locale, slug}[]` (wielojęzyczny). Obsługuje slugi wielopoziomowe (`a/b` → `['a','b']`).
|
(wielojęzyczny). Obsługuje slugi wielopoziomowe (`a/b` → `['a','b']`) oraz
|
||||||
|
zlokalizowane (string albo mapa per język).
|
||||||
|
|
||||||
|
**Strony noindex SĄ renderowane** (nie pomijane jak w sitemap). Strona prawna
|
||||||
|
(polityka, cookies) z noindex nadal musi się wyświetlić — użytkownik wchodzi z
|
||||||
|
stopki, crawler czyta jej `<meta robots=noindex>`. noindex kontroluje
|
||||||
|
INDEKSOWANIE, nie istnienie strony. Pominięcie wymusiłoby dynamiczne renderowanie
|
||||||
|
(ten sam problem streamingu, którego unikamy).
|
||||||
|
|
||||||
|
Helper NIE filtruje `_status` w zapytaniu (`where`) — kolekcje bez włączonych
|
||||||
|
draftów nie mają tego pola i zapytanie by rzuciło błąd. Drafty odfiltrowane w
|
||||||
|
pamięci (bezpieczne dla każdej kolekcji).
|
||||||
|
|
||||||
## PUŁAPKA: await searchParams deoptymalizuje ISR
|
## PUŁAPKA: await searchParams deoptymalizuje ISR
|
||||||
|
|
||||||
|
|||||||
@@ -181,7 +181,10 @@ export function createContentHelpers({
|
|||||||
* Handles automatically:
|
* Handles automatically:
|
||||||
* - pages collection + content collections (with their archive prefix)
|
* - pages collection + content collections (with their archive prefix)
|
||||||
* - excludes the homepage (maps to { slug: [] } — the root)
|
* - excludes the homepage (maps to { slug: [] } — the root)
|
||||||
* - excludes drafts, 404/500/system slugs, and meta.noindex docs
|
* - excludes drafts and 404/500/system slugs
|
||||||
|
* - KEEPS noindex pages (they must still render — noindex controls indexing,
|
||||||
|
* not existence; skipping them would force dynamic rendering)
|
||||||
|
* - localized slugs (string or per-locale map) both handled
|
||||||
* - single-locale → { slug }[]; multi-locale → { locale, slug }[]
|
* - single-locale → { slug }[]; multi-locale → { locale, slug }[]
|
||||||
*
|
*
|
||||||
* Wire it in the project:
|
* Wire it in the project:
|
||||||
@@ -205,12 +208,15 @@ export function createContentHelpers({
|
|||||||
const params: Array<{ locale: string; slug: string[] } | { slug: string[] }> = []
|
const params: Array<{ locale: string; slug: string[] } | { slug: string[] }> = []
|
||||||
|
|
||||||
for (const locale of locales) {
|
for (const locale of locales) {
|
||||||
|
// NO where:{_status} filter — collections without drafts enabled don't
|
||||||
|
// register the _status field, and querying it throws
|
||||||
|
// "path cannot be queried: _status". We filter drafts in memory below,
|
||||||
|
// which is safe for every collection (with or without drafts).
|
||||||
const result = await payload.find({
|
const result = await payload.find({
|
||||||
collection: pagesSlug as never,
|
collection: pagesSlug as never,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
limit: 1000,
|
limit: 1000,
|
||||||
locale: (locale ?? 'all') as never,
|
locale: (locale ?? 'all') as never,
|
||||||
where: { _status: { not_equals: 'draft' } } as never,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const raw of result.docs as Array<{
|
for (const raw of result.docs as Array<{
|
||||||
@@ -219,8 +225,16 @@ export function createContentHelpers({
|
|||||||
meta?: { noindex?: boolean } | null
|
meta?: { noindex?: boolean } | null
|
||||||
slug?: unknown
|
slug?: unknown
|
||||||
}>) {
|
}>) {
|
||||||
|
// Draft filter in memory (safe whether or not the collection has drafts).
|
||||||
if (raw._status && raw._status !== 'published') {continue}
|
if (raw._status && raw._status !== 'published') {continue}
|
||||||
if (raw.meta?.noindex) {continue}
|
|
||||||
|
// NOTE: unlike the sitemap, we do NOT skip meta.noindex here. A noindex
|
||||||
|
// page (privacy, cookies, terms) still needs to render — users reach it
|
||||||
|
// from the footer and crawlers read its <meta robots=noindex>. Pre-render
|
||||||
|
// it as SSG so it's fast and its <head> is complete; noindex controls
|
||||||
|
// INDEXING, not whether the page exists. Skipping it would force dynamic
|
||||||
|
// rendering (the very streaming problem we're avoiding).
|
||||||
|
|
||||||
if (homeId && raw.id === homeId) {
|
if (homeId && raw.id === homeId) {
|
||||||
// Homepage → root. Emit an empty-slug param so '/' (or '/pl') builds.
|
// Homepage → root. Emit an empty-slug param so '/' (or '/pl') builds.
|
||||||
const empty = singleLocale ? { slug: [] } : { slug: [], locale: locale as string }
|
const empty = singleLocale ? { slug: [] } : { slug: [], locale: locale as string }
|
||||||
@@ -228,11 +242,22 @@ export function createContentHelpers({
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const slug = typeof raw.slug === 'string' ? raw.slug : undefined
|
// Slug may be a plain string OR a localized map ({ pl: 'kontakt' }) when
|
||||||
|
// read with locale:'all' or left unflattened. Handle both, or localized
|
||||||
|
// pages get silently dropped.
|
||||||
|
const rawSlug = raw.slug
|
||||||
|
const slug =
|
||||||
|
typeof rawSlug === 'string'
|
||||||
|
? rawSlug
|
||||||
|
: rawSlug && typeof rawSlug === 'object'
|
||||||
|
? (((rawSlug as Record<string, unknown>)[locale ?? ''] as string | undefined) ??
|
||||||
|
(Object.values(rawSlug as Record<string, unknown>)[0] as string | undefined))
|
||||||
|
: undefined
|
||||||
|
|
||||||
if (!slug || EXCLUDED.has(slug)) {continue}
|
if (!slug || EXCLUDED.has(slug)) {continue}
|
||||||
|
|
||||||
// Multi-level slugs ('atrakcje/telefon') → array segments.
|
// Multi-level slugs ('atrakcje/telefon') → array segments.
|
||||||
const segments = slug.split('/').filter(Boolean)
|
const segments = String(slug).split('/').filter(Boolean)
|
||||||
params.push(
|
params.push(
|
||||||
singleLocale ? { slug: segments } : { slug: segments, locale: locale as string },
|
singleLocale ? { slug: segments } : { slug: segments, locale: locale as string },
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user