Compare commits
2
Commits
55a1e5bf8f
...
v1.2.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
671090b1b1 | ||
|
|
0f03bb7446 |
+22
-16
@@ -1,8 +1,14 @@
|
|||||||
import type { BasePayload, SanitizedConfig } from 'payload';
|
import type { BasePayload, SanitizedConfig } from 'payload';
|
||||||
import type { ContentOption, ResolvedRoute, ArchiveEntries } from '../content/index.js';
|
import type { ArchiveEntries, ContentOption, ResolvedRoute } from '../content/index.js';
|
||||||
import type { I18nConfig } from '../i18n/index.js';
|
import type { I18nConfig } from '../i18n/index.js';
|
||||||
import type { SitemapEntry, RobotsRules } from '../seo/index.js';
|
import type { RobotsRules, SitemapEntry } 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.
|
||||||
@@ -10,21 +16,15 @@ 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;
|
||||||
/** SiteSettings global slug. Defaults to 'site-settings'. */
|
|
||||||
settingsSlug?: string;
|
|
||||||
/** Pages collection slug. Defaults to 'pages'. */
|
|
||||||
pagesSlug?: string;
|
|
||||||
/**
|
/**
|
||||||
* i18n config. Required only if you want the ready-made `sitemap` / `robots`
|
* i18n config. Required only if you want the ready-made `sitemap` / `robots`
|
||||||
* handlers — they need the locale list to emit hreflang.
|
* handlers — they need the locale list to emit hreflang.
|
||||||
*/
|
*/
|
||||||
i18n?: I18nConfig;
|
i18n?: I18nConfig;
|
||||||
/**
|
/** Pages collection slug. Defaults to 'pages'. */
|
||||||
* Absolute site origin for sitemap/robots URLs. Falls back to
|
pagesSlug?: string;
|
||||||
* NEXT_PUBLIC_SERVER_URL, then to a relative origin (which most crawlers
|
/** SiteSettings global slug. Defaults to 'site-settings'. */
|
||||||
* reject, so set one in production).
|
settingsSlug?: string;
|
||||||
*/
|
|
||||||
baseUrl?: string;
|
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Bundles the per-request data helpers a frontend needs — the same cached
|
* Bundles the per-request data helpers a frontend needs — the same cached
|
||||||
@@ -52,13 +52,19 @@ type CreateContentHelpersArgs = {
|
|||||||
* a URL is, fetching gets the listing. Metadata generation needs the first and
|
* a URL is, fetching gets the listing. Metadata generation needs the first and
|
||||||
* 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 declare function createContentHelpers({ config, content, settingsSlug, pagesSlug, i18n, baseUrl, }: CreateContentHelpersArgs): {
|
export declare function createContentHelpers({ baseUrl, config, content, i18n, pagesSlug, settingsSlug, }: CreateContentHelpersArgs): {
|
||||||
|
generateStaticParams: () => Promise<Array<{
|
||||||
|
locale: string;
|
||||||
|
slug: string[];
|
||||||
|
} | {
|
||||||
|
slug: string[];
|
||||||
|
}>>;
|
||||||
getCachedPayload: () => Promise<BasePayload>;
|
getCachedPayload: () => Promise<BasePayload>;
|
||||||
getConfiguredLocales: () => Promise<string[]>;
|
getConfiguredLocales: () => Promise<string[]>;
|
||||||
getSettings: (locale: string) => Promise<import("payload").JsonObject>;
|
|
||||||
resolveRoute: (locale: string, segments: string[] | undefined, page: number) => Promise<ResolvedRoute | null>;
|
|
||||||
getEntries: (collection: string, locale: string, page: number, perPage: number) => Promise<ArchiveEntries>;
|
getEntries: (collection: string, locale: string, page: number, perPage: number) => Promise<ArchiveEntries>;
|
||||||
sitemap: () => Promise<SitemapEntry[]>;
|
getSettings: (locale: string) => Promise<import("payload").JsonObject>;
|
||||||
|
resolveRoute: (locale: string, segments: string[] | undefined, page: number) => Promise<null | ResolvedRoute>;
|
||||||
robots: () => RobotsRules;
|
robots: () => RobotsRules;
|
||||||
|
sitemap: () => Promise<SitemapEntry[]>;
|
||||||
};
|
};
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
+105
-16
@@ -1,7 +1,7 @@
|
|||||||
import { cache } from 'react';
|
|
||||||
import { getPayload } from 'payload';
|
import { getPayload } from 'payload';
|
||||||
import { resolveRoute as resolveRouteRaw, getArchiveEntries } from '../content/index.js';
|
import { cache } from 'react';
|
||||||
import { buildSitemapEntries, buildRobots } from '../seo/index.js';
|
import { getArchiveEntries, resolveRoute as resolveRouteRaw } from '../content/index.js';
|
||||||
|
import { buildRobots, buildSitemapEntries } from '../seo/index.js';
|
||||||
/**
|
/**
|
||||||
* Bundles the per-request data helpers a frontend needs — the same cached
|
* Bundles the per-request data helpers a frontend needs — the same cached
|
||||||
* wrappers every project was writing by hand (getPayload, settings, locale
|
* wrappers every project was writing by hand (getPayload, settings, locale
|
||||||
@@ -27,7 +27,7 @@ import { buildSitemapEntries, buildRobots } from '../seo/index.js';
|
|||||||
* `resolveRoute` and `getEntries` are separate on purpose: routing decides what
|
* `resolveRoute` and `getEntries` are separate on purpose: routing decides what
|
||||||
* a URL is, fetching gets the listing. Metadata generation needs the first and
|
* a URL is, fetching gets the listing. Metadata generation needs the first and
|
||||||
* 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({ config, content, settingsSlug = 'site-settings', pagesSlug = 'pages', i18n, baseUrl }) {
|
*/ export function createContentHelpers({ baseUrl, config, content, i18n, pagesSlug = 'pages', settingsSlug = 'site-settings' }) {
|
||||||
const origin = baseUrl ?? process.env.NEXT_PUBLIC_SERVER_URL ?? '';
|
const origin = baseUrl ?? process.env.NEXT_PUBLIC_SERVER_URL ?? '';
|
||||||
const getCachedPayload = cache(async ()=>getPayload({
|
const getCachedPayload = cache(async ()=>getPayload({
|
||||||
config: await config
|
config: await config
|
||||||
@@ -40,29 +40,29 @@ import { buildSitemapEntries, buildRobots } from '../seo/index.js';
|
|||||||
const payload = await getCachedPayload();
|
const payload = await getCachedPayload();
|
||||||
return payload.findGlobal({
|
return payload.findGlobal({
|
||||||
slug: settingsSlug,
|
slug: settingsSlug,
|
||||||
locale: locale,
|
depth: 2,
|
||||||
depth: 2
|
locale: locale
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
/** What does this URL point at? Routing only — no listing data. */ const resolveRoute = cache(async (locale, segments, page)=>{
|
/** What does this URL point at? Routing only — no listing data. */ const resolveRoute = cache(async (locale, segments, page)=>{
|
||||||
const payload = await getCachedPayload();
|
const payload = await getCachedPayload();
|
||||||
return resolveRouteRaw({
|
return resolveRouteRaw({
|
||||||
payload,
|
|
||||||
locale,
|
|
||||||
segments,
|
|
||||||
page,
|
|
||||||
content,
|
content,
|
||||||
|
locale,
|
||||||
|
page,
|
||||||
pagesSlug,
|
pagesSlug,
|
||||||
|
payload,
|
||||||
|
segments,
|
||||||
settingsSlug
|
settingsSlug
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
/** One page of a collection's entries, for an archive listing. */ const getEntries = cache(async (collection, locale, page, perPage)=>{
|
/** One page of a collection's entries, for an archive listing. */ const getEntries = cache(async (collection, locale, page, perPage)=>{
|
||||||
const payload = await getCachedPayload();
|
const payload = await getCachedPayload();
|
||||||
return getArchiveEntries({
|
return getArchiveEntries({
|
||||||
payload,
|
|
||||||
collection,
|
collection,
|
||||||
locale,
|
locale,
|
||||||
page,
|
page,
|
||||||
|
payload,
|
||||||
perPage
|
perPage
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -92,11 +92,11 @@ import { buildSitemapEntries, buildRobots } from '../seo/index.js';
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return await buildSitemapEntries({
|
return await buildSitemapEntries({
|
||||||
payload: await getCachedPayload(),
|
|
||||||
config: i18n,
|
|
||||||
baseUrl: origin,
|
baseUrl: origin,
|
||||||
|
config: i18n,
|
||||||
content,
|
content,
|
||||||
pagesSlug,
|
pagesSlug,
|
||||||
|
payload: await getCachedPayload(),
|
||||||
settingsSlug
|
settingsSlug
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -117,14 +117,103 @@ import { buildSitemapEntries, buildRobots } from '../seo/index.js';
|
|||||||
*/ const robots = ()=>buildRobots({
|
*/ const robots = ()=>buildRobots({
|
||||||
baseUrl: origin
|
baseUrl: origin
|
||||||
});
|
});
|
||||||
|
/**
|
||||||
|
* Next.js generateStaticParams for the [[...slug]] route (or
|
||||||
|
* [locale]/[[...slug]]). Returns every routable page as a params object, so
|
||||||
|
* Next PRE-RENDERS them as static (SSG) instead of dynamic.
|
||||||
|
*
|
||||||
|
* Why this matters beyond convenience: an optional catch-all with no
|
||||||
|
* generateStaticParams is treated as a DYNAMIC route (ƒ), which streams
|
||||||
|
* metadata into <body> (crawlers miss it). Providing generateStaticParams
|
||||||
|
* compiles routes as SSG (●) — the <head> is synchronous and complete. This is
|
||||||
|
* the strongest fix for the metadata-in-head problem (stronger than ISR alone).
|
||||||
|
*
|
||||||
|
* Handles automatically:
|
||||||
|
* - pages collection + content collections (with their archive prefix)
|
||||||
|
* - excludes the homepage (maps to { slug: [] } — the root)
|
||||||
|
* - excludes drafts, 404/500/system slugs, and meta.noindex docs
|
||||||
|
* - single-locale → { slug }[]; multi-locale → { locale, slug }[]
|
||||||
|
*
|
||||||
|
* Wire it in the project:
|
||||||
|
* // 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){
|
||||||
|
const result = await payload.find({
|
||||||
|
collection: pagesSlug,
|
||||||
|
depth: 0,
|
||||||
|
limit: 1000,
|
||||||
|
locale: locale ?? 'all',
|
||||||
|
where: {
|
||||||
|
_status: {
|
||||||
|
not_equals: 'draft'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (const raw of result.docs){
|
||||||
|
if (raw._status && raw._status !== 'published') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (raw.meta?.noindex) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
const slug = typeof raw.slug === 'string' ? raw.slug : undefined;
|
||||||
|
if (!slug || EXCLUDED.has(slug)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Multi-level slugs ('atrakcje/telefon') → array segments.
|
||||||
|
const segments = slug.split('/').filter(Boolean);
|
||||||
|
params.push(singleLocale ? {
|
||||||
|
slug: segments
|
||||||
|
} : {
|
||||||
|
slug: segments,
|
||||||
|
locale: locale
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
};
|
||||||
return {
|
return {
|
||||||
|
generateStaticParams,
|
||||||
getCachedPayload,
|
getCachedPayload,
|
||||||
getConfiguredLocales,
|
getConfiguredLocales,
|
||||||
|
getEntries,
|
||||||
getSettings,
|
getSettings,
|
||||||
resolveRoute,
|
resolveRoute,
|
||||||
getEntries,
|
robots,
|
||||||
sitemap,
|
sitemap
|
||||||
robots
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -53,3 +53,52 @@ stopce / bannerze cookies bierzesz z `getSystemPagePath({ role: privacyPolicy })
|
|||||||
import { ALL_SYSTEM_PAGE_ROLES } from '@intecion/ipal-kit'
|
import { ALL_SYSTEM_PAGE_ROLES } from '@intecion/ipal-kit'
|
||||||
// ['homepage', 'privacyPolicy', 'cookiePolicy']
|
// ['homepage', 'privacyPolicy', 'cookiePolicy']
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## KRYTYCZNE: generateStaticParams dla [[...slug]] (SEO + head)
|
||||||
|
|
||||||
|
Trasa `[[...slug]]` (opcjonalny catch-all) BEZ `generateStaticParams` jest przez
|
||||||
|
Next traktowana jako **dynamiczna** (`ƒ Dynamic`). W trybie dynamicznym z React 19
|
||||||
|
serwer wysyła pusty `<head>`, a metadata streamuje na końcu `<body>` — crawlery
|
||||||
|
(Lighthouse, Screaming Frog) nie widzą `<meta description>` w head → SEO spada.
|
||||||
|
|
||||||
|
**Z `generateStaticParams` trasa kompiluje się jako SSG (`●`)** → synchroniczny,
|
||||||
|
kompletny `<head>` → SEO 100/100. To najsilniejsze rozwiązanie problemu
|
||||||
|
metadata-w-head (mocniejsze niż samo ISR/htmlLimitedBots).
|
||||||
|
|
||||||
|
Plugin dostarcza gotowy generateStaticParams przez createContentHelpers:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// lib/content.ts — dodaj do destrukturyzacji
|
||||||
|
export const {
|
||||||
|
getCachedPayload, getSettings, resolveRoute,
|
||||||
|
generateStaticParams, // ← z pluginu
|
||||||
|
sitemap, robots,
|
||||||
|
} = createContentHelpers({ config, content: contentConfig, i18n: i18nConfig, baseUrl })
|
||||||
|
|
||||||
|
// app/(frontend)/[[...slug]]/page.tsx (albo [locale]/[[...slug]])
|
||||||
|
export { generateStaticParams } from '@/lib/content'
|
||||||
|
```
|
||||||
|
|
||||||
|
Helper automatycznie: pobiera pages + kolekcje treści, wyklucza homepage (→ root),
|
||||||
|
drafty, 404/500, noindex; zwraca `{slug}[]` (jednojęzyczny) albo
|
||||||
|
`{locale, slug}[]` (wielojęzyczny). Obsługuje slugi wielopoziomowe (`a/b` → `['a','b']`).
|
||||||
|
|
||||||
|
## PUŁAPKA: await searchParams deoptymalizuje ISR
|
||||||
|
|
||||||
|
W Next 15/16 `searchParams` to Promise. Odczyt `const { page } = await searchParams`
|
||||||
|
w komponencie strony **deoptymalizuje ISR** — wymusza dynamiczne renderowanie dla
|
||||||
|
tego requestu (traci cały zysk SSG/ISR + wraca problem metadata w body).
|
||||||
|
|
||||||
|
- **Strona BEZ paginacji** → NIE przekazuj/nie czytaj `searchParams` wcale:
|
||||||
|
```ts
|
||||||
|
export default async function Page({ params }) { // bez searchParams
|
||||||
|
const { locale, slug } = await params
|
||||||
|
const route = await resolveRoute(locale, slug ?? []) // bez page
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Strona Z paginacją** (archiwum) → czytaj searchParams, ale świadomie (ta trasa
|
||||||
|
będzie dynamiczna). Rozważ osobną trasę dla archiwum z paginacją, żeby zwykłe
|
||||||
|
strony zostały SSG.
|
||||||
|
|
||||||
|
Reguła: `searchParams` tylko tam, gdzie NAPRAWDĘ potrzebujesz (paginacja). Wszędzie
|
||||||
|
indziej pomiń — inaczej tracisz SSG i SEO.
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@intecion/ipal-kit",
|
"name": "@intecion/ipal-kit",
|
||||||
"version": "1.2.7",
|
"version": "1.2.8",
|
||||||
"description": "Intecion Payload Advanced Library — a Payload CMS 3 plugin: i18n, SEO, forms, consent, analytics, blog/archives.",
|
"description": "Intecion Payload Advanced Library — a Payload CMS 3 plugin: i18n, SEO, forms, consent, analytics, blog/archives.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
import { cache } from 'react'
|
|
||||||
import type { BasePayload, SanitizedConfig } from 'payload'
|
import type { BasePayload, SanitizedConfig } from 'payload'
|
||||||
import { getPayload } from 'payload'
|
|
||||||
|
|
||||||
import type { ContentOption, ResolvedRoute, ArchiveEntries } from '../content/index.js'
|
import { getPayload } from 'payload'
|
||||||
import { resolveRoute as resolveRouteRaw, getArchiveEntries } from '../content/index.js'
|
import { cache } from 'react'
|
||||||
|
|
||||||
|
import type { ArchiveEntries, ContentOption, ResolvedRoute } from '../content/index.js'
|
||||||
import type { I18nConfig } from '../i18n/index.js'
|
import type { I18nConfig } from '../i18n/index.js'
|
||||||
import type { SitemapEntry, RobotsRules } from '../seo/index.js'
|
import type { RobotsRules, SitemapEntry } from '../seo/index.js'
|
||||||
import { buildSitemapEntries, buildRobots } from '../seo/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.
|
||||||
@@ -16,21 +24,15 @@ 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
|
||||||
/** SiteSettings global slug. Defaults to 'site-settings'. */
|
|
||||||
settingsSlug?: string
|
|
||||||
/** Pages collection slug. Defaults to 'pages'. */
|
|
||||||
pagesSlug?: string
|
|
||||||
/**
|
/**
|
||||||
* i18n config. Required only if you want the ready-made `sitemap` / `robots`
|
* i18n config. Required only if you want the ready-made `sitemap` / `robots`
|
||||||
* handlers — they need the locale list to emit hreflang.
|
* handlers — they need the locale list to emit hreflang.
|
||||||
*/
|
*/
|
||||||
i18n?: I18nConfig
|
i18n?: I18nConfig
|
||||||
/**
|
/** Pages collection slug. Defaults to 'pages'. */
|
||||||
* Absolute site origin for sitemap/robots URLs. Falls back to
|
pagesSlug?: string
|
||||||
* NEXT_PUBLIC_SERVER_URL, then to a relative origin (which most crawlers
|
/** SiteSettings global slug. Defaults to 'site-settings'. */
|
||||||
* reject, so set one in production).
|
settingsSlug?: string
|
||||||
*/
|
|
||||||
baseUrl?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,12 +62,12 @@ 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,
|
||||||
settingsSlug = 'site-settings',
|
|
||||||
pagesSlug = 'pages',
|
|
||||||
i18n,
|
i18n,
|
||||||
baseUrl,
|
pagesSlug = 'pages',
|
||||||
|
settingsSlug = 'site-settings',
|
||||||
}: CreateContentHelpersArgs) {
|
}: CreateContentHelpersArgs) {
|
||||||
const origin = baseUrl ?? process.env.NEXT_PUBLIC_SERVER_URL ?? ''
|
const origin = baseUrl ?? process.env.NEXT_PUBLIC_SERVER_URL ?? ''
|
||||||
const getCachedPayload = cache(async (): Promise<BasePayload> =>
|
const getCachedPayload = cache(async (): Promise<BasePayload> =>
|
||||||
@@ -79,7 +81,7 @@ export function createContentHelpers({
|
|||||||
|
|
||||||
const getSettings = cache(async (locale: string) => {
|
const getSettings = cache(async (locale: string) => {
|
||||||
const payload = await getCachedPayload()
|
const payload = await getCachedPayload()
|
||||||
return payload.findGlobal({ slug: settingsSlug as never, locale: locale as never, depth: 2 })
|
return payload.findGlobal({ slug: settingsSlug as never, depth: 2, locale: locale as never })
|
||||||
})
|
})
|
||||||
|
|
||||||
/** What does this URL point at? Routing only — no listing data. */
|
/** What does this URL point at? Routing only — no listing data. */
|
||||||
@@ -88,9 +90,9 @@ export function createContentHelpers({
|
|||||||
locale: string,
|
locale: string,
|
||||||
segments: string[] | undefined,
|
segments: string[] | undefined,
|
||||||
page: number,
|
page: number,
|
||||||
): Promise<ResolvedRoute | null> => {
|
): Promise<null | ResolvedRoute> => {
|
||||||
const payload = await getCachedPayload()
|
const payload = await getCachedPayload()
|
||||||
return resolveRouteRaw({ payload, locale, segments, page, content, pagesSlug, settingsSlug })
|
return resolveRouteRaw({ content, locale, page, pagesSlug, payload, segments, settingsSlug })
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -103,7 +105,7 @@ export function createContentHelpers({
|
|||||||
perPage: number,
|
perPage: number,
|
||||||
): Promise<ArchiveEntries> => {
|
): Promise<ArchiveEntries> => {
|
||||||
const payload = await getCachedPayload()
|
const payload = await getCachedPayload()
|
||||||
return getArchiveEntries({ payload, collection, locale, page, perPage })
|
return getArchiveEntries({ collection, locale, page, payload, perPage })
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -134,11 +136,11 @@ export function createContentHelpers({
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return await buildSitemapEntries({
|
return await buildSitemapEntries({
|
||||||
payload: await getCachedPayload(),
|
|
||||||
config: i18n,
|
|
||||||
baseUrl: origin,
|
baseUrl: origin,
|
||||||
|
config: i18n,
|
||||||
content,
|
content,
|
||||||
pagesSlug,
|
pagesSlug,
|
||||||
|
payload: await getCachedPayload(),
|
||||||
settingsSlug,
|
settingsSlug,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -165,13 +167,89 @@ export function createContentHelpers({
|
|||||||
*/
|
*/
|
||||||
const robots = (): RobotsRules => buildRobots({ baseUrl: origin })
|
const robots = (): RobotsRules => buildRobots({ baseUrl: origin })
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Next.js generateStaticParams for the [[...slug]] route (or
|
||||||
|
* [locale]/[[...slug]]). Returns every routable page as a params object, so
|
||||||
|
* Next PRE-RENDERS them as static (SSG) instead of dynamic.
|
||||||
|
*
|
||||||
|
* Why this matters beyond convenience: an optional catch-all with no
|
||||||
|
* generateStaticParams is treated as a DYNAMIC route (ƒ), which streams
|
||||||
|
* metadata into <body> (crawlers miss it). Providing generateStaticParams
|
||||||
|
* compiles routes as SSG (●) — the <head> is synchronous and complete. This is
|
||||||
|
* the strongest fix for the metadata-in-head problem (stronger than ISR alone).
|
||||||
|
*
|
||||||
|
* Handles automatically:
|
||||||
|
* - pages collection + content collections (with their archive prefix)
|
||||||
|
* - excludes the homepage (maps to { slug: [] } — the root)
|
||||||
|
* - excludes drafts, 404/500/system slugs, and meta.noindex docs
|
||||||
|
* - single-locale → { slug }[]; multi-locale → { locale, slug }[]
|
||||||
|
*
|
||||||
|
* Wire it in the project:
|
||||||
|
* // app/(frontend)/[[...slug]]/page.tsx (or [locale]/[[...slug]])
|
||||||
|
* export { generateStaticParams } from '@/lib/content'
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
|
||||||
|
// 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[] }> = []
|
||||||
|
|
||||||
|
for (const locale of locales) {
|
||||||
|
const result = await payload.find({
|
||||||
|
collection: pagesSlug as never,
|
||||||
|
depth: 0,
|
||||||
|
limit: 1000,
|
||||||
|
locale: (locale ?? 'all') as never,
|
||||||
|
where: { _status: { not_equals: 'draft' } } as never,
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const raw of result.docs as Array<{
|
||||||
|
_status?: string
|
||||||
|
id: number | string
|
||||||
|
meta?: { noindex?: boolean } | null
|
||||||
|
slug?: unknown
|
||||||
|
}>) {
|
||||||
|
if (raw._status && raw._status !== 'published') {continue}
|
||||||
|
if (raw.meta?.noindex) {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
|
||||||
|
}
|
||||||
|
|
||||||
|
const slug = typeof raw.slug === 'string' ? raw.slug : undefined
|
||||||
|
if (!slug || EXCLUDED.has(slug)) {continue}
|
||||||
|
|
||||||
|
// Multi-level slugs ('atrakcje/telefon') → array segments.
|
||||||
|
const segments = slug.split('/').filter(Boolean)
|
||||||
|
params.push(
|
||||||
|
singleLocale ? { slug: segments } : { slug: segments, locale: locale as string },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
generateStaticParams,
|
||||||
getCachedPayload,
|
getCachedPayload,
|
||||||
getConfiguredLocales,
|
getConfiguredLocales,
|
||||||
|
getEntries,
|
||||||
getSettings,
|
getSettings,
|
||||||
resolveRoute,
|
resolveRoute,
|
||||||
getEntries,
|
|
||||||
sitemap,
|
|
||||||
robots,
|
robots,
|
||||||
|
sitemap,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user