Files
ipal-kit/docs/frontend-setup.md
T
2026-08-12 17:54:40 +02:00

245 lines
8.3 KiB
Markdown

# Frontend — wymagane implementacje
Co projekt klienta musi zrobić na froncie, żeby plugin działał. Zebrane z
realnego wdrożenia (ipal-test). W przyszłości → pełny poradnik "first setup".
## Wymagania środowiska
### Tailwind CSS (WYMÓG)
Komponenty pluginu (CookieBanner, Turnstile widget, i inne) są w czystym
Tailwind. Klient MUSI mieć Tailwind + skanować pakiet pluginu:
```bash
pnpm add tailwindcss @tailwindcss/postcss # v4
```
`postcss.config.mjs` (root):
```js
export default { plugins: { '@tailwindcss/postcss': {} } }
```
W globalnym CSS (np. app/(frontend)/styles.css):
```css
@import "tailwindcss";
@source "../../../node_modules/@intecion/ipal-kit/dist/**/*.js";
```
**@source jest kluczowy** — Tailwind domyślnie NIE skanuje node_modules, więc
bez tego klasy komponentów pluginu się nie wygenerują (komponenty renderują
się bez stylów). Ścieżka relatywna do pliku CSS.
### Przestylowanie pod klienta
Komponenty pluginu (CookieBanner, CookieButton) mają domyślny wygląd i działają
bez konfiguracji. Kolory/zaokrąglenia przez CSS custom properties z fallbackami
— nadpisz w swoim CSS:
```css
:root {
--ipal-primary: #16a34a;
--ipal-radius: 1rem;
}
```
Pełna lista tokenów + opcja classNames (gdy tokeny nie starczą): docs/consent.md.
Bloki są Twoje — plugin ich nie stylizuje, RenderBlocks nie dodaje markupu.
### Wymagane zależności (transitive)
Instalacja z npm zaciąga automatycznie. Przy lokalnym tarballu doinstaluj:
```
@payloadcms/plugin-seo @payloadcms/plugin-form-builder nodemailer
lucide-react slugify server-only
```
### Spójność wersji @payloadcms/*
pnpm.overrides wymuszające jedną wersję (patrz README).
### generate:importmap
Po wpięciu pluginu: `pnpm payload generate:importmap` (dla pól SEO w adminie).
## Struktura tras (lokalizacja)
```
src/app/(frontend)/
layout.tsx # root (<html><body>) — istniejący
[locale]/
layout.tsx # walidacja locale + ConsentProvider
[[...slug]]/
page.tsx # render strony (bloki)
```
**[[...slug]] MUSI być podwójny nawias** (opcjonalny catch-all):
- `[slug]` → string (błąd `slug.join is not a function`)
- `[[...slug]]` → tablica (poprawne), łapie /pl (home) i /pl/o-nas jednym plikiem
## i18n — jedno źródło prawdy
Wydziel config locale do osobnego pliku, importuj wszędzie:
```ts
// src/i18n.config.ts
export const i18nConfig = {
defaultLocale: 'pl',
locales: [
{ code: 'pl', label: 'Polski' },
{ code: 'en', label: 'English' },
],
} as const // as const — inaczej TS: locales nie pasuje do niepustej tuple
```
Importuj w: payload.config (ipalKit({ i18n: i18nConfig })), proxy.ts.
Layout może czytać locale z payload config (config.localization.locales) —
też jedno źródło.
## Proxy (dawniej middleware)
> **Next 16:** konwencja `middleware.ts` jest deprecated na rzecz `proxy.ts`
> (plik `proxy.ts`, funkcja `export function proxy`). Logika pluginu bez zmian —
> `createLocaleMiddleware` działa tak samo, zmienia się tylko nazwa pliku i
> funkcji po stronie projektu. Migracja jedną komendą:
> `npx @next/codemod@canary middleware-to-proxy .`
```ts
// src/proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { createLocaleMiddleware } from '@intecion/ipal-kit/next/middleware'
import { i18nConfig } from '@/i18n.config'
const localeMiddleware = createLocaleMiddleware({ config: i18nConfig })
export function proxy(request: NextRequest) {
const result = localeMiddleware(request)
if (result.type === 'next') return NextResponse.next()
const response = NextResponse.redirect(result.location)
// cookie tylko gdy jest zgoda na kategorię functional — inaczej undefined
if (result.cookie) response.cookies.set(result.cookie.name, result.cookie.value)
return response
}
// matcher MUSI być inline (Next analizuje statycznie, nie wykonuje importów —
// import DEFAULT_MIDDLEWARE_MATCHER byłby zignorowany → proxy łapie
// /admin /_next /api → 500)
export const config = {
matcher: ['/((?!api|admin|_next|.*\\..*).*)'],
}
```
## Rozwiązywanie strony (page.tsx)
- brak slug (/pl) → home przez System Pages (settings.homepage), NIE hardkod slug
- slug (/pl/o-nas) → payload.find by slug w danym locale
- depth: 2 → żeby relacje w blokach (form) się populowały
## Consent (layout [locale])
Layout (server) czyta getConsentTexts, przekazuje jako prop do ConsentProvider
(client). Banner + button renderują się same:
```ts
import { getConsentTexts } from '@intecion/ipal-kit'
import { ConsentProvider, CookieBanner, CookieButton } from '@intecion/ipal-kit/client'
const texts = await getConsentTexts({ config, locale, payload, privacyPolicy })
// <ConsentProvider texts={texts}>{children}<CookieBanner/><CookieButton/></ConsentProvider>
```
## Bloki (RenderBlocks)
Klient definiuje bloki (config + komponent) — plugin jest block-agnostic.
- `blocks/<Nazwa>/config.ts` — schemat Payload (Block)
- `blocks/<Nazwa>/Component.tsx` — komponent (dane bloku jako propsy)
- `blocks/registry.ts` — mapa blockType → komponent
- Pages: pole `layout` typu blocks z listą bloków
- page.tsx: `<RenderBlocks blocks={doc.layout} components={registry} />`
**Puste blocks: [] crashuje** (traverseFields) — zawsze z co najmniej jednym
blokiem.
enhanceProps — wstrzykiwanie server-side wartości do bloku bez wiedzy pluginu
(np. turnstileSiteKey, email do FormBlock):
```ts
const enhanceProps = ({ block }) => {
if (block.blockType === 'formBlock') return { turnstileSiteKey, notificationTo }
return {}
}
```
## Formularz (FormBlock)
- FormRenderer (client) — renderuje pola form-buildera (text/email/select/
country/checkbox/textarea/number/state/message)
- Turnstile widget (@intecion/ipal-kit/client) — siteKey jako prop, wstrzykiwany przez
enhanceProps (z SiteIntegrations, publiczny — bezpieczny na kliencie)
- server action → submitForm (@intecion/ipal-kit/server) — weryfikuje Turnstile i zapisuje
Maili NIE składa się w kodzie. Po zapisie submission form-builder sam wysyła
wiadomości skonfigurowane przez edytora (Forms → dany formularz → Emails:
Email To / CC / BCC / Subject / Message z placeholderami {{pole}}, {{*}},
{{*:table}}). Idą przez payload.sendEmail → panelSmtpAdapter → SMTP z panelu.
Wymaga w payload.config:
```ts
import { ipalKit, panelSmtpAdapter } from '@intecion/ipal-kit'
export default buildConfig({
email: panelSmtpAdapter(),
plugins: [ipalKit({ ... })],
})
```
Wymaga w SiteIntegrations: SMTP (host, port, user, password, from) + Turnstile.
Turnstile testowe klucze Cloudflare (zawsze pass):
site 1x00000000000000000000AA, secret 1x0000000000000000000000000000000AA.
Walidacja server-side (limit długości pól) w actions.ts — browserowy `required`
da się obejść wołając akcję bezpośrednio.
## Metadata (SEO na froncie)
createMetadataGenerator zwraca funkcję `({ payload, params, locale })` — Next
woła `generateMetadata({ params })` bez payload/locale, a plugin nigdy nie
wywołuje getPayload sam, więc klient opakowuje:
```ts
const generate = createMetadataGenerator({
config: i18nConfig,
baseUrl: process.env.NEXT_PUBLIC_SERVER_URL,
homeSlug: 'homepage', // home zwija się do /pl, nie /pl/homepage
resolveDocument: async ({ params, locale }) => { ... }, // locale: 'all'!
resolveSiteName: async ({ locale }) => { ... },
resolveImageUrl: async ({ doc }) => { ... },
})
export async function generateMetadata({ params }) {
const { locale, slug } = await params
const payload = await getPayload({ config: await config })
return generate({ payload, params: { slug: slug ?? [] }, locale })
}
```
resolveDocument MUSI pobrać dokument z `locale: 'all'` — wtedy `slug` jest mapą
locale→wartość, z której budowane są hreflang alternates. Zwykły fetch (jeden
locale) da tylko string i hreflang nie powstanie.
Next uruchamia generateMetadata i komponent strony niezależnie — bez React
cache() każde żądanie odpytuje bazę dwa razy o ten sam dokument.
## Analytics
W layoucie [locale], WEWNĄTRZ ConsentProvider (Analytics ustawia Consent Mode
ze stored choice, zanim załaduje tag):
```ts
const analytics = await getAnalyticsConfig(payload) // tylko publiczne GA4/GTM ID
// <ConsentProvider texts={texts}> ... <Analytics {...analytics} /> </ConsentProvider>
```
ID z SiteIntegrations. GTM ma priorytet nad GA4, gdy oba ustawione.