fix createContentHelpers.ts
This commit is contained in:
+80
-70
@@ -141,79 +141,89 @@ import { buildRobots, buildSitemapEntries } from '../seo/index.js';
|
|||||||
* // app/(frontend)/[[...slug]]/page.tsx (or [locale]/[[...slug]])
|
* // app/(frontend)/[[...slug]]/page.tsx (or [locale]/[[...slug]])
|
||||||
* export { generateStaticParams } from '@/lib/content'
|
* export { generateStaticParams } from '@/lib/content'
|
||||||
*/ const generateStaticParams = async ()=>{
|
*/ const generateStaticParams = async ()=>{
|
||||||
const payload = await getCachedPayload();
|
try {
|
||||||
const locales = i18n ? i18n.locales.map((l)=>l.code) : [
|
const payload = await getCachedPayload();
|
||||||
undefined
|
const locales = i18n ? i18n.locales.map((l)=>l.code) : [
|
||||||
];
|
undefined
|
||||||
const singleLocale = !i18n || i18n.locales.length === 1;
|
];
|
||||||
// Home slug per locale, to exclude the homepage (it's the root, slug []).
|
const singleLocale = !i18n || i18n.locales.length === 1;
|
||||||
const settings = await payload.findGlobal({
|
// Home slug per locale, to exclude the homepage (it's the root, slug []).
|
||||||
slug: settingsSlug,
|
const settings = await payload.findGlobal({
|
||||||
depth: 1,
|
slug: settingsSlug,
|
||||||
locale: 'all'
|
depth: 1,
|
||||||
}).catch(()=>null);
|
locale: 'all'
|
||||||
const homeId = settings?.homepage?.id;
|
}).catch(()=>null);
|
||||||
const EXCLUDED = new Set([
|
const homeId = settings?.homepage?.id;
|
||||||
'404',
|
const EXCLUDED = new Set([
|
||||||
'500',
|
'404',
|
||||||
'error',
|
'500',
|
||||||
'not-found'
|
'error',
|
||||||
]);
|
'not-found'
|
||||||
const params = [];
|
]);
|
||||||
for (const locale of locales){
|
const params = [];
|
||||||
// NO where:{_status} filter — collections without drafts enabled don't
|
for (const locale of locales){
|
||||||
// register the _status field, and querying it throws
|
// NO where:{_status} filter — collections without drafts enabled don't
|
||||||
// "path cannot be queried: _status". We filter drafts in memory below,
|
// register the _status field, and querying it throws
|
||||||
// which is safe for every collection (with or without drafts).
|
// "path cannot be queried: _status". We filter drafts in memory below,
|
||||||
const result = await payload.find({
|
// which is safe for every collection (with or without drafts).
|
||||||
collection: pagesSlug,
|
const result = await payload.find({
|
||||||
depth: 0,
|
collection: pagesSlug,
|
||||||
limit: 1000,
|
depth: 0,
|
||||||
locale: locale ?? 'all'
|
limit: 1000,
|
||||||
});
|
locale: locale ?? 'all'
|
||||||
for (const raw of result.docs){
|
|
||||||
// Draft filter in memory (safe whether or not the collection has drafts).
|
|
||||||
if (raw._status && raw._status !== 'published') {
|
|
||||||
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) {
|
|
||||||
// Homepage → root. Emit an empty-slug param so '/' (or '/pl') builds.
|
|
||||||
const empty = singleLocale ? {
|
|
||||||
slug: []
|
|
||||||
} : {
|
|
||||||
slug: [],
|
|
||||||
locale: locale
|
|
||||||
};
|
|
||||||
if (!params.some((p)=>JSON.stringify(p) === JSON.stringify(empty))) {
|
|
||||||
params.push(empty);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// 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)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Multi-level slugs ('atrakcje/telefon') → array segments.
|
|
||||||
const segments = String(slug).split('/').filter(Boolean);
|
|
||||||
params.push(singleLocale ? {
|
|
||||||
slug: segments
|
|
||||||
} : {
|
|
||||||
slug: segments,
|
|
||||||
locale: locale
|
|
||||||
});
|
});
|
||||||
|
for (const raw of result.docs){
|
||||||
|
// Draft filter in memory (safe whether or not the collection has drafts).
|
||||||
|
if (raw._status && raw._status !== 'published') {
|
||||||
|
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) {
|
||||||
|
// Homepage → root. Emit an empty-slug param so '/' (or '/pl') builds.
|
||||||
|
const empty = singleLocale ? {
|
||||||
|
slug: []
|
||||||
|
} : {
|
||||||
|
slug: [],
|
||||||
|
locale: locale
|
||||||
|
};
|
||||||
|
if (!params.some((p)=>JSON.stringify(p) === JSON.stringify(empty))) {
|
||||||
|
params.push(empty);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 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)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Multi-level slugs ('atrakcje/telefon') → array segments.
|
||||||
|
const segments = String(slug).split('/').filter(Boolean);
|
||||||
|
params.push(singleLocale ? {
|
||||||
|
slug: segments
|
||||||
|
} : {
|
||||||
|
slug: segments,
|
||||||
|
locale: locale
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return params;
|
||||||
|
} catch (err) {
|
||||||
|
// DB unreachable — typically a container build (Docker/Coolify/CI) with no
|
||||||
|
// database network. Return [] so the build doesn't crash: Next falls back
|
||||||
|
// to on-demand rendering for the routes, which fill in once the DB is
|
||||||
|
// reachable at runtime. Without this every project would need its own
|
||||||
|
// try/catch here. (Same graceful-degradation as the sitemap handler.)
|
||||||
|
console.warn('[ipal] generateStaticParams: database not reachable during build ' + '(Docker/CI) — returning empty params; routes render on-demand at runtime:', err);
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
return params;
|
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
generateStaticParams,
|
generateStaticParams,
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -145,6 +145,29 @@ sitemap/robots → force-dynamic (bo generują przy żądaniu). Nie mieszaj na j
|
|||||||
trasie. Treść z panelu: ISR = redaktor czeka do rewalidacji; rozważ on-demand
|
trasie. Treść z panelu: ISR = redaktor czeka do rewalidacji; rozważ on-demand
|
||||||
revalidation (hook afterChange → revalidatePath). Patrz seo.md, HOOKS.md.
|
revalidation (hook afterChange → revalidatePath). Patrz seo.md, HOOKS.md.
|
||||||
|
|
||||||
|
## 3a3. SSG a dostęp do bazy przy buildzie (WAŻNE dla SEO)
|
||||||
|
|
||||||
|
`generateStaticParams` (z lib/content) prerenderuje strony jako SSG — head
|
||||||
|
synchroniczny, SEO 100/100. ALE żeby prerenderować, **build musi mieć dostęp do
|
||||||
|
bazy** (generateStaticParams czyta strony z bazy w czasie buildu).
|
||||||
|
|
||||||
|
- **Build MA dostęp do bazy** (baza w tej samej sieci Docker, dostępna w build
|
||||||
|
stage) → strony prerenderowane jako SSG (`●`), head synchroniczny → SEO OK ✓
|
||||||
|
- **Build NIE MA dostępu** (izolowany build stage) → generateStaticParams zwraca
|
||||||
|
`[]` (plugin łapie błąd, build nie pada), ale strony renderują się on-demand
|
||||||
|
(dynamicznie) → head może streamować do body → problem SEO wraca ✗
|
||||||
|
|
||||||
|
Plugin zabezpiecza build przed CRASHEM (try/catch → `[]`), ale to NIE zastępuje
|
||||||
|
dostępu do bazy. **Dla pełnego SSG/SEO zapewnij, że build kontenerowy widzi bazę.**
|
||||||
|
|
||||||
|
W Coolify/Docker: baza (Mongo/Postgres) powinna być dostępna podczas `pnpm build`,
|
||||||
|
nie tylko w runtime. Jeśli build jest w izolowanej sieci — rozważ:
|
||||||
|
- uruchom bazę w tej samej sieci Docker co build stage, albo
|
||||||
|
- build z DATABASE_URI wskazującym na dostępną bazę (nie wewnętrzny host niedostępny w buildzie).
|
||||||
|
|
||||||
|
Weryfikacja: po buildzie `pnpm build` pokazuje trasy jako `●` (SSG), nie `ƒ`
|
||||||
|
(Dynamic). Jeśli `ƒ` mimo generateStaticParams → build nie miał dostępu do bazy.
|
||||||
|
|
||||||
## 3b. Pułapka: prerender tras zależnych od bazy (KONIECZNE)
|
## 3b. Pułapka: prerender tras zależnych od bazy (KONIECZNE)
|
||||||
|
|
||||||
Next domyślnie **prerenderuje** trasy typu `sitemap.ts` w czasie `next build` —
|
Next domyślnie **prerenderuje** trasy typu `sitemap.ts` w czasie `next build` —
|
||||||
|
|||||||
@@ -194,77 +194,91 @@ export function createContentHelpers({
|
|||||||
const generateStaticParams = async (): Promise<
|
const generateStaticParams = async (): Promise<
|
||||||
Array<{ locale: string; slug: string[] } | { slug: string[] }>
|
Array<{ locale: string; slug: string[] } | { slug: string[] }>
|
||||||
> => {
|
> => {
|
||||||
const payload = await getCachedPayload()
|
try {
|
||||||
const locales = i18n ? i18n.locales.map((l) => l.code) : [undefined]
|
const payload = await getCachedPayload()
|
||||||
const singleLocale = !i18n || i18n.locales.length === 1
|
const locales = i18n ? i18n.locales.map((l) => l.code) : [undefined]
|
||||||
|
const singleLocale = !i18n || i18n.locales.length === 1
|
||||||
|
|
||||||
// Home slug per locale, to exclude the homepage (it's the root, slug []).
|
// Home slug per locale, to exclude the homepage (it's the root, slug []).
|
||||||
const settings = (await payload
|
const settings = (await payload
|
||||||
.findGlobal({ slug: settingsSlug as never, depth: 1, locale: 'all' as never })
|
.findGlobal({ slug: settingsSlug as never, depth: 1, locale: 'all' as never })
|
||||||
.catch(() => null)) as { homepage?: { id?: number | string; slug?: unknown } } | null
|
.catch(() => null)) as { homepage?: { id?: number | string; slug?: unknown } } | null
|
||||||
const homeId = settings?.homepage?.id
|
const homeId = settings?.homepage?.id
|
||||||
|
|
||||||
const EXCLUDED = new Set(['404', '500', 'error', 'not-found'])
|
const EXCLUDED = new Set(['404', '500', 'error', 'not-found'])
|
||||||
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
|
// NO where:{_status} filter — collections without drafts enabled don't
|
||||||
// register the _status field, and querying it throws
|
// register the _status field, and querying it throws
|
||||||
// "path cannot be queried: _status". We filter drafts in memory below,
|
// "path cannot be queried: _status". We filter drafts in memory below,
|
||||||
// which is safe for every collection (with or without drafts).
|
// 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,
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const raw of result.docs as Array<{
|
for (const raw of result.docs as Array<{
|
||||||
_status?: string
|
_status?: string
|
||||||
id: number | string
|
id: number | string
|
||||||
meta?: { noindex?: boolean } | null
|
meta?: { noindex?: boolean } | null
|
||||||
slug?: unknown
|
slug?: unknown
|
||||||
}>) {
|
}>) {
|
||||||
// Draft filter in memory (safe whether or not the collection has drafts).
|
// 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}
|
||||||
|
|
||||||
// NOTE: unlike the sitemap, we do NOT skip meta.noindex here. A noindex
|
// NOTE: unlike the sitemap, we do NOT skip meta.noindex here. A noindex
|
||||||
// page (privacy, cookies, terms) still needs to render — users reach it
|
// page (privacy, cookies, terms) still needs to render — users reach it
|
||||||
// from the footer and crawlers read its <meta robots=noindex>. Pre-render
|
// 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
|
// 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
|
// INDEXING, not whether the page exists. Skipping it would force dynamic
|
||||||
// rendering (the very streaming problem we're avoiding).
|
// 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 }
|
||||||
if (!params.some((p) => JSON.stringify(p) === JSON.stringify(empty))) {params.push(empty)}
|
if (!params.some((p) => JSON.stringify(p) === JSON.stringify(empty))) {params.push(empty)}
|
||||||
continue
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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}
|
||||||
|
|
||||||
|
// Multi-level slugs ('atrakcje/telefon') → array segments.
|
||||||
|
const segments = String(slug).split('/').filter(Boolean)
|
||||||
|
params.push(
|
||||||
|
singleLocale ? { slug: segments } : { slug: segments, locale: locale as string },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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}
|
|
||||||
|
|
||||||
// Multi-level slugs ('atrakcje/telefon') → array segments.
|
|
||||||
const segments = String(slug).split('/').filter(Boolean)
|
|
||||||
params.push(
|
|
||||||
singleLocale ? { slug: segments } : { slug: segments, locale: locale as string },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return params
|
return params
|
||||||
|
} catch (err) {
|
||||||
|
// DB unreachable — typically a container build (Docker/Coolify/CI) with no
|
||||||
|
// database network. Return [] so the build doesn't crash: Next falls back
|
||||||
|
// to on-demand rendering for the routes, which fill in once the DB is
|
||||||
|
// reachable at runtime. Without this every project would need its own
|
||||||
|
// try/catch here. (Same graceful-degradation as the sitemap handler.)
|
||||||
|
console.warn(
|
||||||
|
'[ipal] generateStaticParams: database not reachable during build ' +
|
||||||
|
'(Docker/CI) — returning empty params; routes render on-demand at runtime:',
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user