206 lines
7.2 KiB
Markdown
206 lines
7.2 KiB
Markdown
# 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 '@intecion/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 '@intecion/ipal-kit'
|
|
|
|
const route = await resolveRoute({
|
|
payload,
|
|
locale: 'pl',
|
|
segments: ['artykuly', 'moj-post'],
|
|
page: 1, // z ?page=
|
|
content: contentConfig,
|
|
})
|
|
// 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 '@intecion/ipal-kit'
|
|
import config from '@/payload.config'
|
|
import { contentConfig } from '@/content.config'
|
|
|
|
export const { getCachedPayload, getConfiguredLocales, getSettings, resolveRoute, getEntries } =
|
|
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)` — rozstrzyga
|
|
trasę. Pobranie wpisów to osobna funkcja `getEntries` (patrz niżej), bo
|
|
routing i pobieranie danych to dwie różne odpowiedzialności.
|
|
|
|
## Routing i pobieranie — osobno
|
|
|
|
Archiwum potrzebuje listy wpisów; metadane nie. Rozstrzyganie trasy i pobieranie
|
|
wpisów to dwie funkcje, składane jawnie tam, gdzie trzeba obu:
|
|
|
|
```ts
|
|
// generateMetadata — sama trasa (nie płaci za zapytanie o wpisy)
|
|
const route = await resolveRoute(locale, slug, page)
|
|
|
|
// komponent strony — trasa, a potem wpisy, jeśli to archiwum
|
|
const route = await resolveRoute(locale, slug, page)
|
|
const entries = route?.type === 'archive'
|
|
? await getEntries(route.collection, locale, route.page, route.perPage)
|
|
: null
|
|
// entries: { docs, page, totalPages, hasPrevPage, hasNextPage, ... }
|
|
```
|
|
|
|
`resolveRoute` zwraca `collection` i `perPage` — czyli CO i ILE pobrać — ale
|
|
pobierania nie robi. Dzięki temu zmiana sortowania czy filtrowania listingu nie
|
|
dotyka reguł routingu, a metadane nie pobierają wpisów, których i tak nie użyją.
|
|
|
|
## 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 '@intecion/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 '@intecion/ipal-kit'
|
|
import type { ContentOption, ResolvedRoute, ArchiveEntries } from '@intecion/ipal-kit'
|
|
``` |