init commit for iPAL-kit plugin
This commit is contained in:
+200
@@ -0,0 +1,200 @@
|
||||
# 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'
|
||||
```
|
||||
Reference in New Issue
Block a user