diff --git a/dist/modules/frontend/createContentHelpers.js b/dist/modules/frontend/createContentHelpers.js
index e599fe1..ad23f9f 100644
--- a/dist/modules/frontend/createContentHelpers.js
+++ b/dist/modules/frontend/createContentHelpers.js
@@ -141,79 +141,89 @@ import { buildRobots, buildSitemapEntries } from '../seo/index.js';
* // app/(frontend)/[[...slug]]/page.tsx (or [locale]/[[...slug]])
* export { generateStaticParams } from '@/lib/content'
*/ const generateStaticParams = async ()=>{
- const payload = await getCachedPayload();
- 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 settings = await payload.findGlobal({
- slug: settingsSlug,
- depth: 1,
- locale: 'all'
- }).catch(()=>null);
- const homeId = settings?.homepage?.id;
- const EXCLUDED = new Set([
- '404',
- '500',
- 'error',
- 'not-found'
- ]);
- const params = [];
- 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({
- collection: pagesSlug,
- depth: 0,
- 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 . Pre-render
- // it as SSG so it's fast and its
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
+ try {
+ const payload = await getCachedPayload();
+ 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 settings = await payload.findGlobal({
+ slug: settingsSlug,
+ depth: 1,
+ locale: 'all'
+ }).catch(()=>null);
+ const homeId = settings?.homepage?.id;
+ const EXCLUDED = new Set([
+ '404',
+ '500',
+ 'error',
+ 'not-found'
+ ]);
+ const params = [];
+ 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({
+ collection: pagesSlug,
+ depth: 0,
+ 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 . Pre-render
+ // it as SSG so it's fast and its 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 {
generateStaticParams,
diff --git a/dist/modules/frontend/createContentHelpers.js.map b/dist/modules/frontend/createContentHelpers.js.map
index 847b783..032e034 100644
--- a/dist/modules/frontend/createContentHelpers.js.map
+++ b/dist/modules/frontend/createContentHelpers.js.map
@@ -1 +1 @@
-{"version":3,"sources":["../../../src/modules/frontend/createContentHelpers.ts"],"sourcesContent":["import type { BasePayload, SanitizedConfig } from 'payload'\n\nimport { getPayload } from 'payload'\nimport { cache } from 'react'\n\nimport type { ArchiveEntries, ContentOption, ResolvedRoute } from '../content/index.js'\nimport type { I18nConfig } from '../i18n/index.js'\nimport type { RobotsRules, SitemapEntry } from '../seo/index.js'\n\nimport { getArchiveEntries, resolveRoute as resolveRouteRaw } from '../content/index.js'\nimport { buildRobots, buildSitemapEntries } from '../seo/index.js'\n\ntype CreateContentHelpersArgs = {\n /**\n * Absolute site origin for sitemap/robots URLs. Falls back to\n * NEXT_PUBLIC_SERVER_URL, then to a relative origin (which most crawlers\n * reject, so set one in production).\n */\n baseUrl?: string\n /**\n * The client's payload config promise (the default export of payload.config).\n * Passed in because the plugin never imports the client's config directly.\n */\n config: Promise | SanitizedConfig\n /** Archive-backed collections, same value as the plugin option. */\n content?: ContentOption\n /**\n * i18n config. Required only if you want the ready-made `sitemap` / `robots`\n * handlers — they need the locale list to emit hreflang.\n */\n i18n?: I18nConfig\n /** Pages collection slug. Defaults to 'pages'. */\n pagesSlug?: string\n /** SiteSettings global slug. Defaults to 'site-settings'. */\n settingsSlug?: string\n}\n\n/**\n * Bundles the per-request data helpers a frontend needs — the same cached\n * wrappers every project was writing by hand (getPayload, settings, locale\n * list, route resolution, archive entries).\n *\n * Everything is wrapped in React `cache()`, so within one request a value is\n * fetched once no matter how many times it's asked for — which matters because\n * Next runs generateMetadata and the page component separately, and both hit\n * these. Crucially the Payload instance is cached *here*, once, so every helper\n * shares it; that's why this is a factory and not loose functions importing a\n * shared module.\n *\n * ```ts\n * // src/lib/content.ts\n * import { createContentHelpers } from 'ipal-kit'\n * import config from '@/payload.config'\n * import { contentConfig } from '@/content.config'\n *\n * export const { getCachedPayload, getSettings, getConfiguredLocales, resolveRoute, getEntries } =\n * createContentHelpers({ config, content: contentConfig })\n * ```\n *\n * `resolveRoute` and `getEntries` are separate on purpose: routing decides what\n * a URL is, fetching gets the listing. Metadata generation needs the first and\n * not the second, and a page component composes them in two obvious lines.\n */\nexport function createContentHelpers({\n baseUrl,\n config,\n content,\n i18n,\n pagesSlug = 'pages',\n settingsSlug = 'site-settings',\n}: CreateContentHelpersArgs) {\n const origin = baseUrl ?? process.env.NEXT_PUBLIC_SERVER_URL ?? ''\n const getCachedPayload = cache(async (): Promise =>\n getPayload({ config: await config }),\n )\n\n const getConfiguredLocales = cache(async (): Promise => {\n const c = await config\n return c.localization ? c.localization.locales.map((l) => l.code) : []\n })\n\n const getSettings = cache(async (locale: string) => {\n const payload = await getCachedPayload()\n return payload.findGlobal({ slug: settingsSlug as never, depth: 2, locale: locale as never })\n })\n\n /** What does this URL point at? Routing only — no listing data. */\n const resolveRoute = cache(\n async (\n locale: string,\n segments: string[] | undefined,\n page: number,\n ): Promise => {\n const payload = await getCachedPayload()\n return resolveRouteRaw({ content, locale, page, pagesSlug, payload, segments, settingsSlug })\n },\n )\n\n /** One page of a collection's entries, for an archive listing. */\n const getEntries = cache(\n async (\n collection: string,\n locale: string,\n page: number,\n perPage: number,\n ): Promise => {\n const payload = await getCachedPayload()\n return getArchiveEntries({ collection, locale, page, payload, perPage })\n },\n )\n\n /**\n * Ready-made handler for Next's `app/sitemap.ts` — every page and entry with\n * per-URL hreflang and lastmod. Re-export it directly:\n *\n * ```ts\n * // app/sitemap.ts\n * export { sitemap as default } from '@/lib/content'\n * export const dynamic = 'force-dynamic' // generate at runtime, not build\n * ```\n *\n * IMPORTANT — container deploys (Coolify/Docker/Railway/CI): Next treats\n * sitemap.ts as STATIC by default and prerenders it during `next build`, which\n * calls into Payload → the database. The build container usually has no access\n * to the internal Docker network, so the DB connection fails (ENOTFOUND) and\n * the build dies. Two defenses:\n * 1. `export const dynamic = 'force-dynamic'` in app/sitemap.ts — skips build\n * prerender, generates at runtime when the DB is reachable (recommended).\n * 2. This handler also catches DB errors and returns [] so that even without\n * (1) the build won't crash — it just ships an empty sitemap until the\n * next runtime regeneration. (1) is still preferred; (2) is a safety net.\n */\n const sitemap = cache(async (): Promise => {\n if (!i18n) {\n throw new Error('[ipal] createContentHelpers: pass `i18n` to use the sitemap handler.')\n }\n try {\n return await buildSitemapEntries({\n baseUrl: origin,\n config: i18n,\n content,\n pagesSlug,\n payload: await getCachedPayload(),\n settingsSlug,\n })\n } catch (error) {\n // DB unreachable (typically a container build with no DB network) — return\n // an empty sitemap instead of failing the build. Runtime regeneration will\n // produce the real one once the DB is reachable. See dynamic='force-dynamic'.\n console.warn(\n '[ipal] sitemap: could not reach the database, returning empty entries ' +\n \"(add `export const dynamic = 'force-dynamic'` to app/sitemap.ts to \" +\n 'generate at runtime and avoid build-time DB access):',\n error,\n )\n return []\n }\n })\n\n /**\n * Ready-made handler for Next's `app/robots.ts`. Re-export directly:\n *\n * ```ts\n * // app/robots.ts\n * export { robots as default } from '@/lib/content'\n * ```\n */\n const robots = (): RobotsRules => buildRobots({ baseUrl: origin })\n\n /**\n * Next.js generateStaticParams for the [[...slug]] route (or\n * [locale]/[[...slug]]). Returns every routable page as a params object, so\n * Next PRE-RENDERS them as static (SSG) instead of dynamic.\n *\n * Why this matters beyond convenience: an optional catch-all with no\n * generateStaticParams is treated as a DYNAMIC route (ƒ), which streams\n * metadata into (crawlers miss it). Providing generateStaticParams\n * compiles routes as SSG (●) — the is synchronous and complete. This is\n * the strongest fix for the metadata-in-head problem (stronger than ISR alone).\n *\n * Handles automatically:\n * - pages collection + content collections (with their archive prefix)\n * - excludes the homepage (maps to { slug: [] } — the root)\n * - excludes drafts and 404/500/system slugs\n * - KEEPS noindex pages (they must still render — noindex controls indexing,\n * not existence; skipping them would force dynamic rendering)\n * - localized slugs (string or per-locale map) both handled\n * - single-locale → { slug }[]; multi-locale → { locale, slug }[]\n *\n * Wire it in the project:\n * // app/(frontend)/[[...slug]]/page.tsx (or [locale]/[[...slug]])\n * export { generateStaticParams } from '@/lib/content'\n */\n const generateStaticParams = async (): Promise<\n Array<{ locale: string; slug: string[] } | { slug: string[] }>\n > => {\n const payload = await getCachedPayload()\n const locales = i18n ? i18n.locales.map((l) => l.code) : [undefined]\n const singleLocale = !i18n || i18n.locales.length === 1\n\n // Home slug per locale, to exclude the homepage (it's the root, slug []).\n const settings = (await payload\n .findGlobal({ slug: settingsSlug as never, depth: 1, locale: 'all' as never })\n .catch(() => null)) as { homepage?: { id?: number | string; slug?: unknown } } | null\n const homeId = settings?.homepage?.id\n\n const EXCLUDED = new Set(['404', '500', 'error', 'not-found'])\n const params: Array<{ locale: string; slug: string[] } | { slug: string[] }> = []\n\n for (const locale of locales) {\n // NO where:{_status} filter — collections without drafts enabled don't\n // register the _status field, and querying it throws\n // \"path cannot be queried: _status\". We filter drafts in memory below,\n // which is safe for every collection (with or without drafts).\n const result = await payload.find({\n collection: pagesSlug as never,\n depth: 0,\n limit: 1000,\n locale: (locale ?? 'all') as never,\n })\n\n for (const raw of result.docs as Array<{\n _status?: string\n id: number | string\n meta?: { noindex?: boolean } | null\n slug?: unknown\n }>) {\n // Draft filter in memory (safe whether or not the collection has drafts).\n if (raw._status && raw._status !== 'published') {continue}\n\n // NOTE: unlike the sitemap, we do NOT skip meta.noindex here. A noindex\n // page (privacy, cookies, terms) still needs to render — users reach it\n // from the footer and crawlers read its . Pre-render\n // it as SSG so it's fast and its is complete; noindex controls\n // INDEXING, not whether the page exists. Skipping it would force dynamic\n // rendering (the very streaming problem we're avoiding).\n\n if (homeId && raw.id === homeId) {\n // Homepage → root. Emit an empty-slug param so '/' (or '/pl') builds.\n const empty = singleLocale ? { slug: [] } : { slug: [], locale: locale as string }\n if (!params.some((p) => JSON.stringify(p) === JSON.stringify(empty))) {params.push(empty)}\n continue\n }\n\n // Slug may be a plain string OR a localized map ({ pl: 'kontakt' }) when\n // read with locale:'all' or left unflattened. Handle both, or localized\n // pages get silently dropped.\n const rawSlug = raw.slug\n const slug =\n typeof rawSlug === 'string'\n ? rawSlug\n : rawSlug && typeof rawSlug === 'object'\n ? (((rawSlug as Record)[locale ?? ''] as string | undefined) ??\n (Object.values(rawSlug as Record)[0] as string | undefined))\n : undefined\n\n if (!slug || EXCLUDED.has(slug)) {continue}\n\n // Multi-level slugs ('atrakcje/telefon') → array segments.\n const segments = String(slug).split('/').filter(Boolean)\n params.push(\n singleLocale ? { slug: segments } : { slug: segments, locale: locale as string },\n )\n }\n }\n\n return params\n }\n\n return {\n generateStaticParams,\n getCachedPayload,\n getConfiguredLocales,\n getEntries,\n getSettings,\n resolveRoute,\n robots,\n sitemap,\n }\n}\n"],"names":["getPayload","cache","getArchiveEntries","resolveRoute","resolveRouteRaw","buildRobots","buildSitemapEntries","createContentHelpers","baseUrl","config","content","i18n","pagesSlug","settingsSlug","origin","process","env","NEXT_PUBLIC_SERVER_URL","getCachedPayload","getConfiguredLocales","c","localization","locales","map","l","code","getSettings","locale","payload","findGlobal","slug","depth","segments","page","getEntries","collection","perPage","sitemap","Error","error","console","warn","robots","generateStaticParams","undefined","singleLocale","length","settings","catch","homeId","homepage","id","EXCLUDED","Set","params","result","find","limit","raw","docs","_status","empty","some","p","JSON","stringify","push","rawSlug","Object","values","has","String","split","filter","Boolean"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"sources":["../../../src/modules/frontend/createContentHelpers.ts"],"sourcesContent":["import type { BasePayload, SanitizedConfig } from 'payload'\n\nimport { getPayload } from 'payload'\nimport { cache } from 'react'\n\nimport type { ArchiveEntries, ContentOption, ResolvedRoute } from '../content/index.js'\nimport type { I18nConfig } from '../i18n/index.js'\nimport type { RobotsRules, SitemapEntry } from '../seo/index.js'\n\nimport { getArchiveEntries, resolveRoute as resolveRouteRaw } from '../content/index.js'\nimport { buildRobots, buildSitemapEntries } from '../seo/index.js'\n\ntype CreateContentHelpersArgs = {\n /**\n * Absolute site origin for sitemap/robots URLs. Falls back to\n * NEXT_PUBLIC_SERVER_URL, then to a relative origin (which most crawlers\n * reject, so set one in production).\n */\n baseUrl?: string\n /**\n * The client's payload config promise (the default export of payload.config).\n * Passed in because the plugin never imports the client's config directly.\n */\n config: Promise | SanitizedConfig\n /** Archive-backed collections, same value as the plugin option. */\n content?: ContentOption\n /**\n * i18n config. Required only if you want the ready-made `sitemap` / `robots`\n * handlers — they need the locale list to emit hreflang.\n */\n i18n?: I18nConfig\n /** Pages collection slug. Defaults to 'pages'. */\n pagesSlug?: string\n /** SiteSettings global slug. Defaults to 'site-settings'. */\n settingsSlug?: string\n}\n\n/**\n * Bundles the per-request data helpers a frontend needs — the same cached\n * wrappers every project was writing by hand (getPayload, settings, locale\n * list, route resolution, archive entries).\n *\n * Everything is wrapped in React `cache()`, so within one request a value is\n * fetched once no matter how many times it's asked for — which matters because\n * Next runs generateMetadata and the page component separately, and both hit\n * these. Crucially the Payload instance is cached *here*, once, so every helper\n * shares it; that's why this is a factory and not loose functions importing a\n * shared module.\n *\n * ```ts\n * // src/lib/content.ts\n * import { createContentHelpers } from 'ipal-kit'\n * import config from '@/payload.config'\n * import { contentConfig } from '@/content.config'\n *\n * export const { getCachedPayload, getSettings, getConfiguredLocales, resolveRoute, getEntries } =\n * createContentHelpers({ config, content: contentConfig })\n * ```\n *\n * `resolveRoute` and `getEntries` are separate on purpose: routing decides what\n * a URL is, fetching gets the listing. Metadata generation needs the first and\n * not the second, and a page component composes them in two obvious lines.\n */\nexport function createContentHelpers({\n baseUrl,\n config,\n content,\n i18n,\n pagesSlug = 'pages',\n settingsSlug = 'site-settings',\n}: CreateContentHelpersArgs) {\n const origin = baseUrl ?? process.env.NEXT_PUBLIC_SERVER_URL ?? ''\n const getCachedPayload = cache(async (): Promise =>\n getPayload({ config: await config }),\n )\n\n const getConfiguredLocales = cache(async (): Promise => {\n const c = await config\n return c.localization ? c.localization.locales.map((l) => l.code) : []\n })\n\n const getSettings = cache(async (locale: string) => {\n const payload = await getCachedPayload()\n return payload.findGlobal({ slug: settingsSlug as never, depth: 2, locale: locale as never })\n })\n\n /** What does this URL point at? Routing only — no listing data. */\n const resolveRoute = cache(\n async (\n locale: string,\n segments: string[] | undefined,\n page: number,\n ): Promise => {\n const payload = await getCachedPayload()\n return resolveRouteRaw({ content, locale, page, pagesSlug, payload, segments, settingsSlug })\n },\n )\n\n /** One page of a collection's entries, for an archive listing. */\n const getEntries = cache(\n async (\n collection: string,\n locale: string,\n page: number,\n perPage: number,\n ): Promise => {\n const payload = await getCachedPayload()\n return getArchiveEntries({ collection, locale, page, payload, perPage })\n },\n )\n\n /**\n * Ready-made handler for Next's `app/sitemap.ts` — every page and entry with\n * per-URL hreflang and lastmod. Re-export it directly:\n *\n * ```ts\n * // app/sitemap.ts\n * export { sitemap as default } from '@/lib/content'\n * export const dynamic = 'force-dynamic' // generate at runtime, not build\n * ```\n *\n * IMPORTANT — container deploys (Coolify/Docker/Railway/CI): Next treats\n * sitemap.ts as STATIC by default and prerenders it during `next build`, which\n * calls into Payload → the database. The build container usually has no access\n * to the internal Docker network, so the DB connection fails (ENOTFOUND) and\n * the build dies. Two defenses:\n * 1. `export const dynamic = 'force-dynamic'` in app/sitemap.ts — skips build\n * prerender, generates at runtime when the DB is reachable (recommended).\n * 2. This handler also catches DB errors and returns [] so that even without\n * (1) the build won't crash — it just ships an empty sitemap until the\n * next runtime regeneration. (1) is still preferred; (2) is a safety net.\n */\n const sitemap = cache(async (): Promise => {\n if (!i18n) {\n throw new Error('[ipal] createContentHelpers: pass `i18n` to use the sitemap handler.')\n }\n try {\n return await buildSitemapEntries({\n baseUrl: origin,\n config: i18n,\n content,\n pagesSlug,\n payload: await getCachedPayload(),\n settingsSlug,\n })\n } catch (error) {\n // DB unreachable (typically a container build with no DB network) — return\n // an empty sitemap instead of failing the build. Runtime regeneration will\n // produce the real one once the DB is reachable. See dynamic='force-dynamic'.\n console.warn(\n '[ipal] sitemap: could not reach the database, returning empty entries ' +\n \"(add `export const dynamic = 'force-dynamic'` to app/sitemap.ts to \" +\n 'generate at runtime and avoid build-time DB access):',\n error,\n )\n return []\n }\n })\n\n /**\n * Ready-made handler for Next's `app/robots.ts`. Re-export directly:\n *\n * ```ts\n * // app/robots.ts\n * export { robots as default } from '@/lib/content'\n * ```\n */\n const robots = (): RobotsRules => buildRobots({ baseUrl: origin })\n\n /**\n * Next.js generateStaticParams for the [[...slug]] route (or\n * [locale]/[[...slug]]). Returns every routable page as a params object, so\n * Next PRE-RENDERS them as static (SSG) instead of dynamic.\n *\n * Why this matters beyond convenience: an optional catch-all with no\n * generateStaticParams is treated as a DYNAMIC route (ƒ), which streams\n * metadata into (crawlers miss it). Providing generateStaticParams\n * compiles routes as SSG (●) — the is synchronous and complete. This is\n * the strongest fix for the metadata-in-head problem (stronger than ISR alone).\n *\n * Handles automatically:\n * - pages collection + content collections (with their archive prefix)\n * - excludes the homepage (maps to { slug: [] } — the root)\n * - excludes drafts and 404/500/system slugs\n * - KEEPS noindex pages (they must still render — noindex controls indexing,\n * not existence; skipping them would force dynamic rendering)\n * - localized slugs (string or per-locale map) both handled\n * - single-locale → { slug }[]; multi-locale → { locale, slug }[]\n *\n * Wire it in the project:\n * // app/(frontend)/[[...slug]]/page.tsx (or [locale]/[[...slug]])\n * export { generateStaticParams } from '@/lib/content'\n */\n const generateStaticParams = async (): Promise<\n Array<{ locale: string; slug: string[] } | { slug: string[] }>\n > => {\n try {\n const payload = await getCachedPayload()\n const locales = i18n ? i18n.locales.map((l) => l.code) : [undefined]\n const singleLocale = !i18n || i18n.locales.length === 1\n\n // Home slug per locale, to exclude the homepage (it's the root, slug []).\n const settings = (await payload\n .findGlobal({ slug: settingsSlug as never, depth: 1, locale: 'all' as never })\n .catch(() => null)) as { homepage?: { id?: number | string; slug?: unknown } } | null\n const homeId = settings?.homepage?.id\n\n const EXCLUDED = new Set(['404', '500', 'error', 'not-found'])\n const params: Array<{ locale: string; slug: string[] } | { slug: string[] }> = []\n\n for (const locale of locales) {\n // NO where:{_status} filter — collections without drafts enabled don't\n // register the _status field, and querying it throws\n // \"path cannot be queried: _status\". We filter drafts in memory below,\n // which is safe for every collection (with or without drafts).\n const result = await payload.find({\n collection: pagesSlug as never,\n depth: 0,\n limit: 1000,\n locale: (locale ?? 'all') as never,\n })\n\n for (const raw of result.docs as Array<{\n _status?: string\n id: number | string\n meta?: { noindex?: boolean } | null\n slug?: unknown\n }>) {\n // Draft filter in memory (safe whether or not the collection has drafts).\n if (raw._status && raw._status !== 'published') {continue}\n\n // NOTE: unlike the sitemap, we do NOT skip meta.noindex here. A noindex\n // page (privacy, cookies, terms) still needs to render — users reach it\n // from the footer and crawlers read its . Pre-render\n // it as SSG so it's fast and its is complete; noindex controls\n // INDEXING, not whether the page exists. Skipping it would force dynamic\n // rendering (the very streaming problem we're avoiding).\n\n if (homeId && raw.id === homeId) {\n // Homepage → root. Emit an empty-slug param so '/' (or '/pl') builds.\n const empty = singleLocale ? { slug: [] } : { slug: [], locale: locale as string }\n if (!params.some((p) => JSON.stringify(p) === JSON.stringify(empty))) {params.push(empty)}\n continue\n }\n\n // Slug may be a plain string OR a localized map ({ pl: 'kontakt' }) when\n // read with locale:'all' or left unflattened. Handle both, or localized\n // pages get silently dropped.\n const rawSlug = raw.slug\n const slug =\n typeof rawSlug === 'string'\n ? rawSlug\n : rawSlug && typeof rawSlug === 'object'\n ? (((rawSlug as Record)[locale ?? ''] as string | undefined) ??\n (Object.values(rawSlug as Record)[0] as string | undefined))\n : undefined\n\n if (!slug || EXCLUDED.has(slug)) {continue}\n\n // Multi-level slugs ('atrakcje/telefon') → array segments.\n const segments = String(slug).split('/').filter(Boolean)\n params.push(\n singleLocale ? { slug: segments } : { slug: segments, locale: locale as string },\n )\n }\n }\n\n return params\n } catch (err) {\n // DB unreachable — typically a container build (Docker/Coolify/CI) with no\n // database network. Return [] so the build doesn't crash: Next falls back\n // to on-demand rendering for the routes, which fill in once the DB is\n // reachable at runtime. Without this every project would need its own\n // try/catch here. (Same graceful-degradation as the sitemap handler.)\n console.warn(\n '[ipal] generateStaticParams: database not reachable during build ' +\n '(Docker/CI) — returning empty params; routes render on-demand at runtime:',\n err,\n )\n return []\n }\n }\n\n return {\n generateStaticParams,\n getCachedPayload,\n getConfiguredLocales,\n getEntries,\n getSettings,\n resolveRoute,\n robots,\n sitemap,\n }\n}\n"],"names":["getPayload","cache","getArchiveEntries","resolveRoute","resolveRouteRaw","buildRobots","buildSitemapEntries","createContentHelpers","baseUrl","config","content","i18n","pagesSlug","settingsSlug","origin","process","env","NEXT_PUBLIC_SERVER_URL","getCachedPayload","getConfiguredLocales","c","localization","locales","map","l","code","getSettings","locale","payload","findGlobal","slug","depth","segments","page","getEntries","collection","perPage","sitemap","Error","error","console","warn","robots","generateStaticParams","undefined","singleLocale","length","settings","catch","homeId","homepage","id","EXCLUDED","Set","params","result","find","limit","raw","docs","_status","empty","some","p","JSON","stringify","push","rawSlug","Object","values","has","String","split","filter","Boolean","err"],"mappings":"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"}
\ No newline at end of file
diff --git a/docs/deployment.md b/docs/deployment.md
index 2240d1d..f9f7c77 100644
--- a/docs/deployment.md
+++ b/docs/deployment.md
@@ -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
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)
Next domyślnie **prerenderuje** trasy typu `sitemap.ts` w czasie `next build` —
diff --git a/src/modules/frontend/createContentHelpers.ts b/src/modules/frontend/createContentHelpers.ts
index f28aa53..4a9779a 100644
--- a/src/modules/frontend/createContentHelpers.ts
+++ b/src/modules/frontend/createContentHelpers.ts
@@ -194,77 +194,91 @@ export function createContentHelpers({
const generateStaticParams = async (): Promise<
Array<{ locale: string; slug: string[] } | { slug: string[] }>
> => {
- const payload = await getCachedPayload()
- const locales = i18n ? i18n.locales.map((l) => l.code) : [undefined]
- const singleLocale = !i18n || i18n.locales.length === 1
+ try {
+ const payload = await getCachedPayload()
+ 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 settings = (await payload
- .findGlobal({ slug: settingsSlug as never, depth: 1, locale: 'all' as never })
- .catch(() => null)) as { homepage?: { id?: number | string; slug?: unknown } } | null
- const homeId = settings?.homepage?.id
+ // Home slug per locale, to exclude the homepage (it's the root, slug []).
+ const settings = (await payload
+ .findGlobal({ slug: settingsSlug as never, depth: 1, locale: 'all' as never })
+ .catch(() => null)) as { homepage?: { id?: number | string; slug?: unknown } } | null
+ const homeId = settings?.homepage?.id
- const EXCLUDED = new Set(['404', '500', 'error', 'not-found'])
- const params: Array<{ locale: string; slug: string[] } | { slug: string[] }> = []
+ const EXCLUDED = new Set(['404', '500', 'error', 'not-found'])
+ const params: Array<{ locale: string; slug: string[] } | { slug: string[] }> = []
- 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({
- collection: pagesSlug as never,
- depth: 0,
- limit: 1000,
- locale: (locale ?? 'all') as never,
- })
+ 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({
+ collection: pagesSlug as never,
+ depth: 0,
+ limit: 1000,
+ locale: (locale ?? 'all') as never,
+ })
- for (const raw of result.docs as Array<{
- _status?: string
- id: number | string
- meta?: { noindex?: boolean } | null
- slug?: unknown
- }>) {
- // Draft filter in memory (safe whether or not the collection has drafts).
- if (raw._status && raw._status !== 'published') {continue}
+ for (const raw of result.docs as Array<{
+ _status?: string
+ id: number | string
+ meta?: { noindex?: boolean } | null
+ slug?: unknown
+ }>) {
+ // 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 . Pre-render
- // it as SSG so it's fast and its is complete; noindex controls
- // INDEXING, not whether the page exists. Skipping it would force dynamic
- // rendering (the very streaming problem we're avoiding).
+ // 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 . Pre-render
+ // it as SSG so it's fast and its 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 as string }
- if (!params.some((p) => JSON.stringify(p) === JSON.stringify(empty))) {params.push(empty)}
- continue
+ if (homeId && raw.id === homeId) {
+ // Homepage → root. Emit an empty-slug param so '/' (or '/pl') builds.
+ const empty = singleLocale ? { slug: [] } : { slug: [], locale: locale as string }
+ 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 as Record)[locale ?? ''] as string | undefined) ??
+ (Object.values(rawSlug as Record)[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)[locale ?? ''] as string | undefined) ??
- (Object.values(rawSlug as Record)[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 {