# content Kolekcje, których wpisy żyją pod stroną-archiwum — blog, realizacje, aktualności, cokolwiek z listingiem. Wpisy trafiają pod adres w rodzaju `/pl/artykuly/moj-post`, a segment `artykuly` nie jest osobną konfiguracją — to slug strony, którą edytor wskazał jako archiwum tej kolekcji. ## Model — kto czym jest ``` kolekcja posts ──(config)──► „to kolekcja archiwalna” strona „Artykuły” ──(System Pages)──► „jestem archiwum kolekcji posts” wpis ──► należy do posts, i tyle ``` Wpis niczego nie wie o swoim adresie. O adresie decyduje przypisanie strony — jedno dla całej kolekcji. Zmiana tytułu strony „Artykuły" na „Wpisy" przenosi całą sekcję na `/pl/wpisy`, per locale, bez deploya. To jak folder: pliki nie tagują się nazwą folderu, folder ma nazwę. **Kolekcji nie da się dodać z panelu** — Payload trzyma schemat w kodzie. Nowy typ treści to zawsze zmiana w kodzie (nowa kolekcja + wpis w configu). Edytor zarządza tylko przypisaniem archiwum i jego adresem. ## Config ```ts // content.config.ts — współdzielony przez payload.config i front import type { ContentOption } from 'ipal-kit' export const contentConfig: ContentOption = { collections: [ { slug: 'posts', label: 'Artykuły', perPage: 10 }, { slug: 'projects', label: 'Realizacje', perPage: 6 }, ], } ``` ```ts // payload.config.ts ipalKit({ pages: { slug: 'pages' }, // wymagane — archiwum jest stroną content: contentConfig, seo: { collections: ['pages', 'posts', 'projects'] }, // wpisy też chcą meta }) ``` Każda pozycja dodaje w **Site Settings → System Pages** pole relacji „{label} — archive page". `slug` to kolekcja klienta, `perPage` steruje paginacją listingu. Osobny plik `content.config.ts` (jak `i18n.config.ts`) jest potrzebny, bo tę samą deklarację czytają dwa miejsca: payload.config (żeby dodać pola archiwum) i front (router). Jedno źródło prawdy. ## Rozstrzyganie tras — resolveRoute Serce modułu. Catch-all `[[...slug]]` łapie wszystko, a `resolveRoute` mówi, czym dana ścieżka jest: ```ts import { resolveRoute } from 'ipal-kit' const route = await resolveRoute({ payload, locale: 'pl', segments: ['artykuly', 'moj-post'], page: 1, // z ?page= content: contentConfig, withEntries: false, // true → dociąga wpisy do archiwum (patrz niżej) }) // route.type: 'home' | 'page' | 'archive' | 'entry' | (null gdy 404) ``` Logika: pierwszy segment dopasowywany jest do slugów stron-archiwów z System Pages. Trafienie → wiadomo, której kolekcji dotyczy. `['artykuly']` → listing `posts`; `['artykuly','moj-post']` → wpis w `posts`; brak trafienia → zwykła strona. **Archiwum ma pierwszeństwo przed stroną o tym samym slugu** — inaczej strona „artykuly" przesłaniałaby własne wpisy. **Głębokość tylko `{archiwum}/{wpis}`** — kategorie w ścieżce robiłyby canonical niejednoznacznym (ten sam wpis pod wieloma URL-ami), więc `/a/b/c` → null. **Kolizja slugów:** dwie kolekcje z archiwum o tym samym slugu → wygrywa pierwsza z listy `collections`. Błąd konfiguracji, router nie ostrzega. ## Helpery frontu — createContentHelpers Zamiast pisać cache'owane wrappery w każdym projekcie: ```ts // src/lib/content.ts import { createContentHelpers } from 'ipal-kit' import config from '@/payload.config' import { contentConfig } from '@/content.config' export const { getCachedPayload, getConfiguredLocales, getSettings, resolveRoute } = createContentHelpers({ config, content: contentConfig }) ``` Wszystko owinięte w React `cache()`, a instancja Payloada powstaje **raz** w fabryce i jest współdzielona — dlatego to fabryka, nie luźne funkcje. Bez tego `cache()` nie dedupikowałby między helperami, a Next woła generateMetadata i komponent strony osobno. `resolveRoute` z fabryki przyjmuje `(locale, segments, page, withEntries?)`. ## withEntries — wpisy tylko gdy trzeba Archiwum potrzebuje listy wpisów; metadane nie. `withEntries` rozstrzyga: ```ts // generateMetadata — bez wpisów (nie płaci za zapytanie) const route = await resolveRoute(locale, slug, page) // komponent strony — z wpisami const route = await resolveRoute(locale, slug, page, true) // route.entries: { docs, page, totalPages, hasPrevPage, hasNextPage, ... } ``` Metadane i strona wołają z różnymi argumentami, więc `cache()` traktuje je jako osobne wywołania — i słusznie, bo metadane wpisów nie potrzebują. ## Listing Strona-archiwum to zwykła strona z blokami, więc listing jest **blokiem** (Twoim — plugin nie decyduje o wyglądzie listy). Blok dostaje wpisy przez enhanceProps: ```tsx // w page.tsx const enhanceProps = ({ block }) => { if (block.blockType === 'entriesList' && route.type === 'archive') { return { entries: route.entries, locale, archiveSlug: route.doc.slug } } return {} } ``` Blok nie wie, co listuje — wpisy wstrzykuje trasa na podstawie tego, której kolekcji ta strona jest archiwum. Ten sam blok obsługuje `/pl/artykuly` i `/pl/realizacje`. ## Ścieżki i paginacja ```ts import { buildEntryPath, buildArchivePath, parsePageParam } from 'ipal-kit' buildEntryPath({ locale: 'pl', archiveSlug: 'artykuly', entrySlug: 'moj-post' }) // '/pl/artykuly/moj-post' buildArchivePath({ locale: 'pl', archiveSlug: 'artykuly', page: 2 }) // '/pl/artykuly?page=2' (strona 1 bez query) parsePageParam(searchParams.page) // '2' → 2; śmieci/undefined → 1 ``` Paginacja przez `?page=`, nie `/2`. Segment ścieżki gryzłby się z wpisem o slugu „2", a `/strona/2` dodawałby kolejny zlokalizowany segment do konfiguracji. Google rozumie `?page=` od lat, a `rel=next/prev` zostało wycofane. Canonical strony 2 wskazuje **na siebie** (`?page=2`), nie na stronę 1 — inaczej Google uznałby, że wpisów z dalszych stron nie ma. Hreflangi niosą ten sam numer (`/en/articles?page=2`). To ogarnia `createPageMetadata` automatycznie, gdy przekażesz mu `page` i `content`. ## Metadane wpisów `createPageMetadata` z opcją `content` sam rozpoznaje wpisy i buduje im poprawny canonical/hreflang z prefiksem (patrz seo.md): ```ts const pageMetadata = createPageMetadata({ config: i18nConfig, baseUrl: process.env.NEXT_PUBLIC_SERVER_URL, content: contentConfig, // ← bez tego wpisy dostają zły adres }) ``` ## Przełącznik języka `switchLocalePath` (z i18n) uwzględnia prefiks: polski wpis bez tłumaczenia EN prowadzi do archiwum EN (`/en/articles`), nie na stronę główną — najbliżej tego, czego szuka odwiedzający. ## Eksport ```ts import { resolveRoute, getArchiveEntries, buildArchivePath, buildEntryPath, parsePageParam, createContentHelpers, archiveFieldName, } from 'ipal-kit' import type { ContentOption, ResolvedRoute, ArchiveEntries } from 'ipal-kit' ```