updated docs

This commit is contained in:
2026-08-04 19:37:23 +02:00
parent 9a45b3a50f
commit 90cbc4370f
19 changed files with 290 additions and 272 deletions
+14 -10
View File
@@ -1,8 +1,11 @@
# IPAL — Dokumentacja modułów
**Stawiasz nowy projekt?** → [install.md](./install.md) — instalacja (github/rejestr/tarball) i diagnostyka
**Instalacja pakietu** (token Gitea, rejestr vs repozytorium) → główny
[README](../README.md).
**Nowy projekt krok po kroku** → [getting-started.md](./getting-started.md).
Konfiguracja: [getting-started.md](./getting-started.md)
Ta dokumentacja opisuje **konfigurację i moduły** pluginu — zakłada, że
`@intecion/ipal-kit` jest już zainstalowany.
IPAL (Intecion Payload Advanced Library) to plugin do Payload CMS 3, który
dostarcza logikę i konfigurację; projekt klienta zawiera tylko komponenty
@@ -14,7 +17,7 @@ wizualne i podłączenia do Next.js.
- **Klient (projekt)** = komponenty (wygląd), pliki-podłączenia Next.js
(jednolinijkowe re-eksporty), konfiguracja front.
## Instalacja i wpięcie
## Konfiguracja i wpięcie
### Wymagane zależności
@@ -62,7 +65,7 @@ importMap`).
```ts
// payload.config.ts
import { ipalKit } from 'ipal-kit'
import { ipalKit } from '@intecion/ipal-kit'
export default buildConfig({
// ...
@@ -88,11 +91,11 @@ export default buildConfig({
| Import | Zawiera | Kontekst |
|---|---|---|
| `ipal-kit` | logika server-safe, plugin, helpery | server / config |
| `ipal-kit/server` | runtime server-only (sendEmail, verifyTurnstile, submitForm) | Server Actions / route handlers |
| `ipal-kit/client` | komponenty client (consent, Turnstile, Analytics) | `'use client'` |
| `ipal-kit/rsc` | RenderBlocks (RSC) | server component |
| `ipal-kit/next/middleware` | locale middleware | middleware.ts |
| `@intecion/ipal-kit` | logika server-safe, plugin, helpery | server / config |
| `@intecion/ipal-kit/server` | runtime server-only (sendEmail, verifyTurnstile, submitForm) | Server Actions / route handlers |
| `@intecion/ipal-kit/client` | komponenty client (consent, Turnstile, Analytics) | `'use client'` |
| `@intecion/ipal-kit/rsc` | RenderBlocks (RSC) | server component |
| `@intecion/ipal-kit/next/middleware` | locale middleware | middleware.ts / proxy.ts |
## Moduły
@@ -114,6 +117,7 @@ export default buildConfig({
Nowy projekt krok po kroku: [getting-started.md](./getting-started.md)
Referencja wdrożenia frontu: [frontend-setup.md](./frontend-setup.md)
Wydawanie nowych wersji wtyczki: [publishing.md](./publishing.md)
## Zasady dla wszystkich modułów
@@ -122,6 +126,6 @@ Referencja wdrożenia frontu: [frontend-setup.md](./frontend-setup.md)
2. **Sekrety w panelu** — SMTP, Turnstile secret, R2 w SiteIntegrations
(admin-only). Odczyt server-side przez Local API.
3. **Client/server split** — kod z sekretami ma `server-only`; komponenty
client w `ipal-kit/client`.
client w `@intecion/ipal-kit/client`.
4. **Generyki na typy klienta** — helpery przyjmują `<T>` (np. wygenerowany
`SiteSetting`), bo plugin nie zna typów projektu.
+4 -4
View File
@@ -25,7 +25,7 @@ zmieniać role) do wskazanej kolekcji. Kolekcja należy do Ciebie
## Predykaty (front / hooki / access)
```ts
import { isAdmin, isEditor, hasMinimumRole } from 'ipal-kit'
import { isAdmin, isEditor, hasMinimumRole } from '@intecion/ipal-kit'
isAdmin(user) // admin?
isEditor(user) // editor lub admin (hierarchia)
@@ -39,7 +39,7 @@ Przyjmują luźno typowanego usera (jak `req.user`), czytają `roles` defensywni
### Collection-level (zwracają boolean | Where)
```ts
import { adminOnly, adminOrEditor, adminOrSelf, requireRole, authenticated } from 'ipal-kit'
import { adminOnly, adminOrEditor, adminOrSelf, requireRole, authenticated } from '@intecion/ipal-kit'
export const Articles = {
slug: 'articles',
@@ -62,7 +62,7 @@ access: { update: requireRole('editor') }
### Field-level (zwracają boolean)
```ts
import { adminOnlyField, adminOrEditorField, requireRoleField } from 'ipal-kit'
import { adminOnlyField, adminOrEditorField, requireRoleField } from '@intecion/ipal-kit'
{
name: 'internalNote',
@@ -74,6 +74,6 @@ import { adminOnlyField, adminOrEditorField, requireRoleField } from 'ipal-kit'
## Hierarchia
```ts
import { ROLE_HIERARCHY } from 'ipal-kit'
import { ROLE_HIERARCHY } from '@intecion/ipal-kit'
// ['user', 'editor', 'admin'] — wyższa rola spełnia wymóg niższej
```
+75
View File
@@ -0,0 +1,75 @@
# analytics
Ładuje GA4 albo Google Tag Manager, spięte z modułem consent: Consent Mode
dostaje decyzję odwiedzającego, zanim tag się załaduje, i aktualizację w
momencie kliknięcia w banerze.
## Config
Brak opcji w `payload.config` — plugin czyta ID z globala **Site Integrations**
(Settings → Site Integrations):
- **GA4 Measurement ID** — `G-XXXXXXXXXX`
- **GTM Container ID** — `GTM-XXXXXXX`
Gdy ustawione są oba, wygrywa GTM. Gdy żadne — komponent nie robi nic.
To jedyne pola z Site Integrations, które trafiają na klienta — są publiczne
(widać je w źródle każdej strony z GA). `getAnalyticsConfig` czyta wyłącznie je,
więc sekrety (Turnstile, SMTP) nie mają jak wyciec.
## Front
W layoucie locale, **wewnątrz `ConsentProvider`**:
```tsx
import { getAnalyticsConfig } from '@intecion/ipal-kit'
import { ConsentProvider, CookieBanner, Analytics } from '@intecion/ipal-kit/client'
export default async function LocaleLayout({ children, params }) {
const payload = await getPayload({ config })
const analytics = await getAnalyticsConfig(payload) // server
return (
<ConsentProvider texts={texts}>
{children}
<CookieBanner />
<Analytics {...analytics} />
</ConsentProvider>
)
}
```
`Analytics` nie renderuje nic — wstrzykuje skrypty. Zamontuj raz, wysoko w
drzewie.
## Jak działa Consent Mode
1. Przed załadowaniem tagu: `gtag('consent', 'default', …)` z zapisaną decyzją
odwiedzającego (albo wszystko `denied`, gdy jeszcze nie zdecydował).
2. Tag się ładuje i respektuje ten stan od pierwszego trafienia.
3. Klik w banerze → `gtag('consent', 'update', …)` → tagi reagują natychmiast.
Mapowanie kategorii na sygnały Google:
| Kategoria | Sygnały |
|---|---|
| analytics | `analytics_storage` |
| marketing | `ad_storage`, `ad_user_data`, `ad_personalization` |
| functional | `functionality_storage`, `personalization_storage` |
| necessary | `security_storage` (zawsze `granted`) |
## Testowanie
W konsoli: `window.dataLayer` — powinien zawierać `Arguments(3)` z `consent` /
`default`, a po decyzji `consent` / `update`. Jeśli widzisz `Array` zamiast
`Arguments`, komenda nie zostanie rozpoznana przez Google.
Ciasteczko `_ga` pojawia się dopiero po zgodzie na analytics — to jest sedno
Consent Mode.
**GTM sam nie ustawia ciasteczek** — to pojemnik. Bez opublikowanego (Submit →
Publish, nie sam zapis) tagu GA4 w kontenerze wszystko wygląda dobrze:
`gtm.load` w dataLayer, Tag Assistant widzi kontener — a `_ga` nie ma, bo nic go
nie tworzy. Przy debugowaniu warto tymczasowo wyczyścić GTM Container ID i
zostawić samo GA4, żeby wyeliminować kontener jako zmienną.
+2 -2
View File
@@ -12,10 +12,10 @@ Brak opcji w payload.config — bloki definiujesz w swoich kolekcjach
## Front — RenderBlocks
Import z `ipal-kit/rsc` (to komponent serwerowy):
Import z `@intecion/ipal-kit/rsc` (to komponent serwerowy):
```tsx
import { RenderBlocks } from 'ipal-kit/rsc'
import { RenderBlocks } from '@intecion/ipal-kit/rsc'
// Twój registry: blockType → komponent (komponenty są Twoje)
import { Hero } from '@/blocks/Hero'
+6 -6
View File
@@ -23,12 +23,12 @@ localized. Link do polityki prywatności bierze się z system pages
## Front — Provider + banner
Provider owija aplikację, banner i button renderują się same. Import z
`ipal-kit/client`:
`@intecion/ipal-kit/client`:
```tsx
// app/(frontend)/[locale]/layout.tsx
import { ConsentProvider, CookieBanner, CookieButton } from 'ipal-kit/client'
import { getConsentTexts } from 'ipal-kit'
import { ConsentProvider, CookieBanner, CookieButton } from '@intecion/ipal-kit/client'
import { getConsentTexts } from '@intecion/ipal-kit'
export default async function Layout({ children, params }) {
const { locale } = await params
@@ -66,15 +66,15 @@ Domyślne klasy Tailwind można nadpisać przez `classNames`:
## Gating skryptów wg zgody
```ts
import { updateConsent, setDefaultConsent } from 'ipal-kit'
import { updateConsent, setDefaultConsent } from '@intecion/ipal-kit'
// wysyła sygnały do Google Consent Mode (gtag) na podstawie stanu zgody
```
Logika (kategorie, storage, parsowanie) też jest dostępna server-safe z
`ipal-kit`:
`@intecion/ipal-kit`:
```ts
import { parseConsent, CONSENT_COOKIE, CONSENT_CATEGORIES } from 'ipal-kit'
import { parseConsent, CONSENT_COOKIE, CONSENT_CATEGORIES } from '@intecion/ipal-kit'
// np. gating skryptów server-side na podstawie cookie zgody
```
+6 -6
View File
@@ -26,7 +26,7 @@ zarządza tylko przypisaniem archiwum i jego adresem.
```ts
// content.config.ts — współdzielony przez payload.config i front
import type { ContentOption } from 'ipal-kit'
import type { ContentOption } from '@intecion/ipal-kit'
export const contentConfig: ContentOption = {
collections: [
@@ -59,7 +59,7 @@ Serce modułu. Catch-all `[[...slug]]` łapie wszystko, a `resolveRoute` mówi,
dana ścieżka jest:
```ts
import { resolveRoute } from 'ipal-kit'
import { resolveRoute } from '@intecion/ipal-kit'
const route = await resolveRoute({
payload,
@@ -91,7 +91,7 @@ Zamiast pisać cache'owane wrappery w każdym projekcie:
```ts
// src/lib/content.ts
import { createContentHelpers } from 'ipal-kit'
import { createContentHelpers } from '@intecion/ipal-kit'
import config from '@/payload.config'
import { contentConfig } from '@/content.config'
@@ -151,7 +151,7 @@ kolekcji ta strona jest archiwum. Ten sam blok obsługuje `/pl/artykuly` i
## Ścieżki i paginacja
```ts
import { buildEntryPath, buildArchivePath, parsePageParam } from 'ipal-kit'
import { buildEntryPath, buildArchivePath, parsePageParam } from '@intecion/ipal-kit'
buildEntryPath({ locale: 'pl', archiveSlug: 'artykuly', entrySlug: 'moj-post' })
// '/pl/artykuly/moj-post'
@@ -201,6 +201,6 @@ import {
parsePageParam,
createContentHelpers,
archiveFieldName,
} from 'ipal-kit'
import type { ContentOption, ResolvedRoute, ArchiveEntries } from 'ipal-kit'
} from '@intecion/ipal-kit'
import type { ContentOption, ResolvedRoute, ArchiveEntries } from '@intecion/ipal-kit'
```
+27 -2
View File
@@ -19,7 +19,7 @@ Brak opcji — SMTP (host, port, user, password, from) jest w SiteIntegrations
## Front — sendEmail (server)
```ts
import { sendEmail } from 'ipal-kit/server'
import { sendEmail } from '@intecion/ipal-kit/server'
const result = await sendEmail({
payload,
@@ -48,4 +48,29 @@ if (result.sent) {
Reset hasła / weryfikacja email idą przez wbudowany mechanizm Payload
(`config.email`), którego ten moduł **nie** konfiguruje (celowo — wymagałby
SMTP w env). Jeśli ich potrzebujesz, to osobna konfiguracja adaptera przy
`buildConfig`.
`buildConfig`.
## Adapter — SMTP z panelu dla całego Payloada
`sendEmail` wysyła własnym transportem. Osobno plugin daje adapter, który
podłącza tę samą skrzynkę pod `payload.sendEmail`:
```ts
// payload.config.ts
import { panelSmtpAdapter } from '@intecion/ipal-kit'
export default buildConfig({
email: panelSmtpAdapter(),
// panelSmtpAdapter({ fallbackFromAddress, fallbackFromName }) — używane tylko
// zanim panel zostanie wypełniony (Payload wymaga adresu synchronicznie przy
// starcie, zanim można odczytać globala)
})
```
Po wpięciu wszystko, co w Payloadzie wysyła maile, idzie przez SMTP z Site
Integrations — w tym wbudowane maile form-buildera (Forms → Emails), maile
resetu hasła i weryfikacji konta. Adapter czyta konfigurację przy każdym
wysłaniu, więc zmiana skrzynki w panelu działa bez restartu.
Bez adaptera Payload używa mocka, który tylko loguje do konsoli — maile
form-buildera nie wyjdą.
+2 -2
View File
@@ -31,7 +31,7 @@ Zwykle w Server Action wywoływanej przez formularz. HTML obu maili składasz
na froncie (Twój wygląd):
```ts
import { submitForm } from 'ipal-kit/server'
import { submitForm } from '@intecion/ipal-kit/server'
const result = await submitForm({
payload,
@@ -86,7 +86,7 @@ wiadomości skonfigurowane przez edytora (Forms → formularz → Emails), przez
`payload.sendEmail`. Żeby wyszły, config musi mieć adapter:
```ts
email: panelSmtpAdapter(), // z 'ipal-kit'
email: panelSmtpAdapter(), // z '@intecion/ipal-kit'
```
Wtedy idą przez SMTP z Site Integrations. Edytor ustawia odbiorców, temat i
+7 -7
View File
@@ -22,7 +22,7 @@ export default { plugins: { '@tailwindcss/postcss': {} } }
W globalnym CSS (np. app/(frontend)/styles.css):
```css
@import "tailwindcss";
@source "../../../node_modules/ipal-kit/dist/**/*.js";
@source "../../../node_modules/@intecion/ipal-kit/dist/**/*.js";
```
**@source jest kluczowy** — Tailwind domyślnie NIE skanuje node_modules, więc
@@ -107,7 +107,7 @@ też jedno źródło.
// src/middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { createLocaleMiddleware } from 'ipal-kit/next/middleware'
import { createLocaleMiddleware } from '@intecion/ipal-kit/next/middleware'
import { i18nConfig } from '@/i18n.config'
const localeMiddleware = createLocaleMiddleware({ config: i18nConfig })
@@ -140,8 +140,8 @@ Layout (server) czyta getConsentTexts, przekazuje jako prop do ConsentProvider
(client). Banner + button renderują się same:
```ts
import { getConsentTexts } from 'ipal-kit'
import { ConsentProvider, CookieBanner, CookieButton } from 'ipal-kit/client'
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>
@@ -173,9 +173,9 @@ const enhanceProps = ({ block }) => {
- FormRenderer (client) — renderuje pola form-buildera (text/email/select/
country/checkbox/textarea/number/state/message)
- Turnstile widget (ipal-kit/client) — siteKey jako prop, wstrzykiwany przez
- Turnstile widget (@intecion/ipal-kit/client) — siteKey jako prop, wstrzykiwany przez
enhanceProps (z SiteIntegrations, publiczny — bezpieczny na kliencie)
- server action → submitForm (ipal-kit/server) — weryfikuje Turnstile i zapisuje
- 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:
@@ -185,7 +185,7 @@ Email To / CC / BCC / Subject / Message z placeholderami {{pole}}, {{*}},
Wymaga w payload.config:
```ts
import { ipalKit, panelSmtpAdapter } from 'ipal-kit'
import { ipalKit, panelSmtpAdapter } from '@intecion/ipal-kit'
export default buildConfig({
email: panelSmtpAdapter(),
+19 -16
View File
@@ -1,8 +1,8 @@
# Nowy projekt — krok po kroku
> **Instalacja:** najszybciej `pnpm add github:rasm-its/ipal-kit`. Pełne drogi
> (github / rejestr / tarball) i diagnostyka błędów — install.md.
> **Instalacja pluginu** (token Gitea, rejestr vs git) jest opisana w głównym
> [README](../README.md). Ten przewodnik zakłada, że `@intecion/ipal-kit` jest
> już zainstalowany, i przeprowadza przez **konfigurację** projektu.
Od pustego katalogu do działającej, wielojęzycznej strony z blokami, consentem i
formularzem. Kolejność jest istotna: kilka kroków zależy od poprzednich (schemat
@@ -20,10 +20,13 @@ npx create-payload-app@latest moj-projekt
cd moj-projekt
```
## 2. Instalacja IPAL
## 2. Plugin i zależności
Zainstaluj `@intecion/ipal-kit` zgodnie z [README](../README.md) (rejestr Gitea
albo bezpośrednio z repozytorium — wymaga tokenu). Następnie dodaj zależności
współdzielone z Payloadem, których plugin nie zaciąga sam:
```bash
pnpm add ipal-kit
pnpm add @payloadcms/[email protected] @payloadcms/[email protected] \
nodemailer lucide-react slugify server-only
```
@@ -72,7 +75,7 @@ export const i18nConfig = {
## 4. payload.config.ts
```ts
import { ipalKit, panelSmtpAdapter } from 'ipal-kit'
import { ipalKit, panelSmtpAdapter } from '@intecion/ipal-kit'
import { i18nConfig } from '@/i18n.config'
import { Pages } from '@/collections/Pages'
@@ -101,7 +104,7 @@ export default buildConfig({
```ts
// src/collections/Pages.ts
import type { CollectionConfig } from 'payload'
import { buildSlugField } from 'ipal-kit'
import { buildSlugField } from '@intecion/ipal-kit'
import { ContentBlock } from '@/blocks/Content/config'
export const Pages: CollectionConfig = {
@@ -151,7 +154,7 @@ export function ContentBlockComponent({ heading, body }: { heading?: string; bod
```ts
// src/blocks/registry.ts
import type { BlockComponentMap } from 'ipal-kit/rsc'
import type { BlockComponentMap } from '@intecion/ipal-kit/rsc'
import { ContentBlockComponent } from '@/blocks/Content/Component'
export const blockRegistry: BlockComponentMap = {
@@ -177,7 +180,7 @@ export default { plugins: { '@tailwindcss/postcss': {} } }
```css
/* src/app/(frontend)/styles.css — na górze */
@import "tailwindcss";
@source "../../../node_modules/ipal-kit/dist/**/*.js";
@source "../../../node_modules/@intecion/ipal-kit/dist/**/*.js";
```
`@source` jest **konieczny** — Tailwind nie skanuje `node_modules`, więc bez
@@ -190,7 +193,7 @@ niego klasy komponentów pluginu nie powstaną i banner wyrenderuje się goły.
// src/middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { createLocaleMiddleware } from 'ipal-kit/next/middleware'
import { createLocaleMiddleware } from '@intecion/ipal-kit/next/middleware'
import { i18nConfig } from '@/i18n.config'
const localeMiddleware = createLocaleMiddleware({ config: i18nConfig })
@@ -294,8 +297,8 @@ tablicy (`slug.join is not a function`) i nie łapią samego `/pl`.
```tsx
// src/app/(frontend)/[locale]/layout.tsx
import { notFound } from 'next/navigation'
import { getConsentTexts, getAnalyticsConfig } from 'ipal-kit'
import { ConsentProvider, CookieBanner, CookieButton, Analytics } from 'ipal-kit/client'
import { getConsentTexts, getAnalyticsConfig } from '@intecion/ipal-kit'
import { ConsentProvider, CookieBanner, CookieButton, Analytics } from '@intecion/ipal-kit/client'
import { i18nConfig } from '@/i18n.config'
import { getCachedPayload, getSettings } from '@/lib/payload'
import { getConfiguredLocales } from '@/lib/locales'
@@ -347,8 +350,8 @@ export async function generateStaticParams() {
// src/app/(frontend)/[locale]/[[...slug]]/page.tsx
import { notFound } from 'next/navigation'
import type { Metadata } from 'next'
import { RenderBlocks } from 'ipal-kit/rsc'
import { createPageMetadata } from 'ipal-kit'
import { RenderBlocks } from '@intecion/ipal-kit/rsc'
import { createPageMetadata } from '@intecion/ipal-kit'
import { i18nConfig } from '@/i18n.config'
import { blockRegistry } from '@/blocks/registry'
import { getCachedPayload } from '@/lib/payload'
@@ -435,7 +438,7 @@ Pełny opis: content.md. W skrócie:
2. **content.config.ts** obok i18n.config.ts:
```ts
import type { ContentOption } from 'ipal-kit'
import type { ContentOption } from '@intecion/ipal-kit'
export const contentConfig: ContentOption = {
collections: [{ slug: 'posts', label: 'Artykuły', perPage: 10 }],
}
@@ -497,7 +500,7 @@ Pełna lista tokenów: consent.md.
|---|---|
| Pusty tab SEO / brak kolekcji Forms | rozjazd wersji `@payloadcms/*` — sprawdź `pnpm.overrides` |
| `PayloadComponent not found in importMap` | `pnpm payload generate:importmap` |
| Banner bez stylów | brak `@source` na `node_modules/ipal-kit` albo brak Tailwinda |
| Banner bez stylów | brak `@source` na `node_modules/@intecion/ipal-kit` albo brak Tailwinda |
| `/admin` i `/_next` zwracają 500 | matcher w middleware nie jest inline |
| `slug.join is not a function` | katalog `[slug]` zamiast `[[...slug]]` |
| `/pl` → 404 | Homepage nieustawiony w System Pages |
+4 -4
View File
@@ -34,7 +34,7 @@ import {
getLocaleCodes, getDefaultLocale, isValidLocale, getLocaleDefinition,
negotiateLocale, buildLocalizedPath, switchLocalePath,
getLocalizedSlugs, LOCALE_COOKIE_NAME,
} from 'ipal-kit'
} from '@intecion/ipal-kit'
const config = { defaultLocale: 'pl', locales: [{code:'pl',label:'Polski'},{code:'en',label:'English'}] }
@@ -69,15 +69,15 @@ fallback na `/{locale}` (root), zamiast dead-endu na 404.
## Middleware — patrz osobno
Negocjacja locale + redirect na wejściu (`domena.com` → `/pl`) jest w
`ipal-kit/next/middleware`. Zobacz [middleware w tej sekcji](#middleware)
`@intecion/ipal-kit/next/middleware`. Zobacz [middleware w tej sekcji](#middleware)
niżej.
## Middleware
```ts
// next-middleware.ts (projekt klienta) — jedyna logika to podłączenie
// middleware.ts (projekt klienta) — jedyna logika to podłączenie
import { NextResponse } from 'next/server'
import { createLocaleMiddleware, DEFAULT_MIDDLEWARE_MATCHER } from 'ipal-kit/next/middleware'
import { createLocaleMiddleware, DEFAULT_MIDDLEWARE_MATCHER } from '@intecion/ipal-kit/next/middleware'
const i18nConfig = {
defaultLocale: 'pl',
-118
View File
@@ -1,118 +0,0 @@
# Instalacja ipal-kit
Trzy drogi. Wybierz jedną i trzymaj się jej — mieszanie (raz git, raz rejestr,
raz tarball) to najczęstsze źródło błędów instalacji.
## Droga A — z GitHuba (zalecana dla zespołu)
Wymaga, żeby `dist/` był zacommitowany w repo (nie budowany u instalującego).
```bash
pnpm add github:rasm-its/ipal-kit
# konkretny tag (stabilniej):
pnpm add github:rasm-its/ipal-kit#v1.0.0
```
Wymagania po stronie projektu:
1. **`onlyBuiltDependencies`** — pnpm blokuje skrypty build z paczek git.
Jeśli pakiet ma jakiekolwiek skrypty postinstall, dodaj w package.json
projektu:
```json
"pnpm": { "onlyBuiltDependencies": ["@intecion/ipal-kit"] }
```
(Jeśli plugin nie ma `prepare`/postinstall — patrz niżej — to niepotrzebne.)
2. **peer-zależności** — plugin ich nie zaciąga, projekt musi mieć:
```bash
pnpm add @payloadcms/[email protected] @payloadcms/[email protected] \
nodemailer lucide-react slugify server-only
```
Wymagania po stronie pluginu (raz, przy wydawaniu):
- **`dist/` w repo** — bo instalacja z git nie buduje. Zbuduj i zacommituj
`dist` przed każdym wydaniem.
- **BRAK `prepare: pnpm build`** w package.json — inaczej pnpm próbuje budować
przy instalacji i żąda `onlyBuiltDependencies`. Skoro `dist` jest w repo, build
jest zbędny.
- **główny `exports` wskazuje `dist`, nie `src`** — instalacja z git czyta
główny `exports` (publishConfig działa TYLKO przy `npm publish`, nie przy git).
Wszystkie ścieżki `./dist/*.js` i `./dist/*.d.ts`.
- **jeden blok `exports`** — nie zostawiaj `publishConfig.exports` obok głównego;
dwa bloki potrafią rozjechać rozwiązywanie modułów.
- **żadnej self-reference** — pakiet nie może mieć siebie w `dependencies`
(`"@intecion/ipal-kit": "git+..."`). Wchodzi, gdy odpalisz `pnpm add` w
katalogu pluginu — NIGDY tego nie rób.
## Droga B — GitHub Packages (rejestr)
Publikujesz zbudowany pakiet; instalujący pobiera gotowy `dist`, nie buduje.
Plugin — `publishConfig.registry` + token z `write:packages`:
```bash
echo "//npm.pkg.github.com/:_authToken=TOKEN" >> ~/.npmrc
npm version patch && npm publish
```
Projekt — `.npmrc` ze scope + token z `read:packages`:
```
@intecion:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=TOKEN
```
```bash
pnpm add @intecion/ipal-kit
```
Zaleta nad Drogą A: semver (koniec z ręcznym commitowaniem `dist`), `publishConfig`
działa (nie musisz ruszać głównego `exports`). Wada: token u każdego instalującego.
## Droga C — lokalny tarball (development, przekazanie pliku)
```bash
# w pluginie
pnpm build && pnpm pack --out ipal-kit.tgz
# w PROJEKCIE (nie w pluginie!)
pnpm add ~/sciezka/ipal-kit/ipal-kit.tgz
```
`--out ipal-kit.tgz` daje stałą nazwę — bez tego scope zamienia `/` na `-`
(`intecion-ipal-kit-1.0.0.tgz`).
## Twarde zasady (wyparzone w boju)
- **NIGDY `pnpm add ...ipal-kit...` w katalogu pluginu.** Tworzy self-reference,
która zatruwa każdą kolejną instalację. Zawsze w katalogu projektu. Sprawdzaj
`pwd` przed każdym `pnpm add`.
- **Sprawdzaj REPO, nie plik lokalny.** pnpm z git bierze stan repo. Po zmianie:
`git show origin/main:package.json | grep '"import"'` — musi pokazać `./dist/`.
Commit z nazwą "fix" nie znaczy, że fix jest w commicie.
- **Nie edytuj package.json `sed`em.** Rozjeżdża strukturę (dwa bloki exports).
Nadpisuj cały plik.
- **Zostajesz na 1.0.0? Czyść cache przy każdym reinstall:**
```bash
rm -rf node_modules/@intecion node_modules/.pnpm/*ipal-kit* .next
pnpm store prune && pnpm install
```
Publikacja z bumpem wersji (Droga B) to znosi.
- **Weryfikuj rozwiązanie modułu, nie tylko instalację:**
```bash
node -e "console.log(require.resolve('@intecion/ipal-kit/next/middleware'))"
```
Ma wypisać ścieżkę do `dist`, nie błąd. Jeśli pokazuje `src/...ts` → główny
`exports` wskazuje src (patrz Droga A).
## Diagnostyka — objaw → przyczyna
| Objaw | Przyczyna |
|---|---|
| `ERR_PNPM_FETCH_404` na `@scope/...` | pakiet nieopublikowany; użyto nazwy rejestrowej zamiast `github:` |
| `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED` | `prepare` w package.json + brak `onlyBuiltDependencies` |
| `Cannot find module .../src/exports/X.ts` | główny `exports` wskazuje src; instalacja z git nie widzi publishConfig |
| `ENOENT ...ipal-kit-1.0.0.tgz` przy instalacji | self-reference w dependencies pluginu |
| Turbopack „Module not found" mimo pliku na dysku | dwa bloki exports w package.json; Node bierze zły |
| `pnpm add` przeszło, pakietu brak w node_modules | instalacja do złego katalogu, albo przerwana — sprawdź `pwd` |
| stary kod mimo reinstall (1.0.0) | cache; `pnpm store prune` + `rm -rf .next` |
Po instalacji — konfiguracja pluginu: getting-started.md.
+2 -2
View File
@@ -22,7 +22,7 @@ zna Twojej kolekcji — slug podajesz w opcji.
## Front — rozwiązanie ścieżki roli
```ts
import { getSystemPagePath } from 'ipal-kit'
import { getSystemPagePath } from '@intecion/ipal-kit'
// SiteSettings z locale:'all' + depth:1 (żeby relationship był obiektem, nie ID)
const settings = await payload.findGlobal({
@@ -50,6 +50,6 @@ stopce / bannerze cookies bierzesz z `getSystemPagePath({ role: privacyPolicy })
## Role
```ts
import { ALL_SYSTEM_PAGE_ROLES } from 'ipal-kit'
import { ALL_SYSTEM_PAGE_ROLES } from '@intecion/ipal-kit'
// ['homepage', 'privacyPolicy', 'cookiePolicy']
```
+2 -2
View File
@@ -11,7 +11,7 @@ Brak — te globale są zawsze budowane przez plugin. Nie ma osobnej opcji.
## Front — odczyt globali
```ts
import { getSiteSettings, getSiteIntegrations } from 'ipal-kit'
import { getSiteSettings, getSiteIntegrations } from '@intecion/ipal-kit'
import type { SiteSetting, SiteIntegration } from '@/payload-types'
const payload = await getPayload({ config })
@@ -49,6 +49,6 @@ return <Form siteKey={turnstileSiteKey} />
## Niższy poziom: getGlobal
```ts
import { getGlobal } from 'ipal-kit'
import { getGlobal } from '@intecion/ipal-kit'
const data = await getGlobal<MyType>(payload, 'moj-global', { locale: 'pl', depth: 1 })
```
-70
View File
@@ -1,70 +0,0 @@
# Publikacja ipal-kit — checklist
## package.json — wymagane pola
```json
{
"name": "@intecion/ipal-kit", // scoped pod organizację
"version": "1.0.0", // BUMP przy każdej publikacji (semver)
"files": ["dist"], // tylko dist trafia do pakietu (NIE src)
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": { ... }, // wszystkie entry points na ./dist/*
"publishConfig": {
"registry": "https://npm.pkg.github.com",
"exports": { ... } // mirror z ./dist (masz to już zrobione)
},
"scripts": {
"build": "...",
"prepublishOnly": "pnpm clean && pnpm build" // build ZAWSZE przed publish
},
"peerDependencies": { // NIE dependencies — klient już je ma
"payload": "3.84.1",
"next": ">=15",
"react": ">=19"
}
}
```
## Krytyczne przed pierwszą publikacją
- [ ] **peerDependencies zamiast dependencies** dla payload/next/react —
inaczej pakiet zaciąga drugą kopię Payloada i wszystko się sypie.
@payloadcms/plugin-seo i plugin-form-builder też jako peer (klient pinuje).
- [ ] **`"files": ["dist"]`** — bez tego do pakietu trafia src/ (widzieliśmy to
w stack trace). Sam dist.
- [ ] **usuń self-reference** — sprawdź, że w dependencies NIE ma
"ipal-kit": "file:..." (ta zaraza z pnpm add w złym katalogu).
- [ ] **prepublishOnly** buduje przed publikacją — nigdy nie publikuj ręcznie
zbudowanego dist (łatwo o nieaktualny).
- [ ] **bump wersji** — koniec z 1.0.0 na zawsze. Każda publikacja = nowy numer.
To rozwiązuje cały cykl cache/store prune, który gryzł podczas developmentu.
## Publikacja (GitHub Packages)
```bash
# jednorazowo: token GitHuba z prawami write:packages w ~/.npmrc
echo "//npm.pkg.github.com/:_authToken=TWÓJ_TOKEN" >> ~/.npmrc
# przy każdym wydaniu
npm version patch # 1.0.0 → 1.0.1 (albo minor/major)
npm publish
```
## Instalacja u pracownika
```bash
# ~/.npmrc w projekcie albo globalnie
@intecion:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=ICH_TOKEN
# potem normalnie
pnpm add @intecion/ipal-kit
pnpm add @payloadcms/[email protected] @payloadcms/[email protected] \
nodemailer lucide-react slugify server-only
```
## README pakietu
Wskaż na docs/getting-started.md jako pierwszy krok. Pracownik z dostępem do
rejestru + getting-started postawi projekt bez pytania Ciebie o nic.
+6 -6
View File
@@ -60,7 +60,7 @@ Pages, grupa meta), więc nie trzeba mu tego opisywać:
```ts
// app/(frontend)/[locale]/[[...slug]]/page.tsx
import { createPageMetadata } from 'ipal-kit'
import { createPageMetadata } from '@intecion/ipal-kit'
import { i18nConfig } from '@/i18n.config'
const pageMetadata = createPageMetadata({
@@ -93,7 +93,7 @@ Dla tras spoza konwencji: inna kolekcja (blog), własna logika obrazka, inny
global. Klient dostarcza resolvery.
```ts
import { createMetadataGenerator } from 'ipal-kit'
import { createMetadataGenerator } from '@intecion/ipal-kit'
const generate = createMetadataGenerator({
config: i18nConfig,
@@ -148,7 +148,7 @@ Next uruchamia `generateMetadata` i komponent strony niezależnie — bez React
Gdy chcesz pełną kontrolę:
```ts
import { buildMetadata, getLocalizedSlugs } from 'ipal-kit'
import { buildMetadata, getLocalizedSlugs } from '@intecion/ipal-kit'
return buildMetadata({
meta: doc.meta, // z plugin-seo
@@ -168,7 +168,7 @@ return buildMetadata({
## Pomocnicze
```ts
import { composeTitle, buildHreflangAlternates } from 'ipal-kit'
import { composeTitle, buildHreflangAlternates } from '@intecion/ipal-kit'
composeTitle({ pageTitle: 'O nas', siteName: 'Acme' })
// 'O nas | Acme'
@@ -184,7 +184,7 @@ buildHreflangAlternates({ slugs, config, baseUrl }) // { pl: '...', en: '...' }
eksportowanych, gdybyś budował własny generator metadanych:
```ts
import { readSiteMetaConfig, slugsAcrossLocales } from 'ipal-kit'
import { readSiteMetaConfig, slugsAcrossLocales } from '@intecion/ipal-kit'
// nazwa witryny, separator (dopełniony), kolejność, homeSlug — z SiteSettings
const site = await readSiteMetaConfig({ payload, locale })
@@ -239,7 +239,7 @@ Pomija: drafty (`_status !== 'published'`) i dokumenty z `meta.noindex`.
Niskopoziomowo (własna trasa zamiast handlera z fabryki):
```ts
import { buildSitemapEntries, buildRobots } from 'ipal-kit'
import { buildSitemapEntries, buildRobots } from '@intecion/ipal-kit'
const entries = await buildSitemapEntries({
payload, config: i18nConfig, baseUrl, content: contentConfig,
+59
View File
@@ -0,0 +1,59 @@
# slug
Pole slug generowane automatycznie z tytułu, per locale.
## Użycie (kolekcja klienta)
```ts
import { buildSlugField } from '@intecion/ipal-kit'
export const Pages: CollectionConfig = {
slug: 'pages',
fields: [
{ name: 'title', type: 'text', required: true, localized: true },
buildSlugField({ from: 'title' }),
// ...
],
}
```
Zwraca gotowe pole: `slug`, text, localized, required, z hookiem
`beforeValidate`.
## Zachowanie
Slug generuje się z pola źródłowego **tylko gdy jest pusty**. Cokolwiek edytor
wpisze ręcznie, zostaje — także po zmianie tytułu. Zmiana adresu opublikowanej
strony to zerwane linki, więc plugin nigdy nie robi tego sam.
Diakrytyki idą przez `slugify`: „Strona główna" → `strona-glowna`.
## Per locale
Slug jest zlokalizowany — każdy język ma własny. Edytując w PL ustawiasz
`slug.pl`, w EN `slug.en`.
**Konsekwencja:** slug generuje się tylko dla locale, w którym wypełniono tytuł.
Utworzysz stronę po polsku, przełączysz front na `/en/…` — 404, bo `slug.en`
jest puste. Trzeba przełączyć locale w adminie i wpisać tytuł po angielsku.
To ta sama mapa slugów, z której powstają hreflang alternates (patrz seo.md) i
przełącznik języka (patrz i18n.md).
## Opcje
```ts
buildSlugField({
from: 'title', // pole źródłowe
name: 'slug', // nazwa pola (domyślnie 'slug')
required: true, // domyślnie true
})
```
## Pomocnicze
```ts
import { toSlug } from '@intecion/ipal-kit'
toSlug('Strona główna') // 'strona-glowna'
```
+4 -4
View File
@@ -11,11 +11,11 @@ SiteIntegrations (tab Turnstile). Edytor wpisuje klucze w panelu.
## Front — widget (client)
Import z `ipal-kit/client`. `siteKey` pobierz server-side i przekaż jako prop:
Import z `@intecion/ipal-kit/client`. `siteKey` pobierz server-side i przekaż jako prop:
```tsx
// server component — pobiera publiczny siteKey z panelu
import { getSiteIntegrations } from 'ipal-kit'
import { getSiteIntegrations } from '@intecion/ipal-kit'
const { turnstileSiteKey } = await getSiteIntegrations(payload)
// przekaż do swojego client-formularza → widget
@@ -23,7 +23,7 @@ const { turnstileSiteKey } = await getSiteIntegrations(payload)
```tsx
'use client'
import { Turnstile } from 'ipal-kit/client'
import { Turnstile } from '@intecion/ipal-kit/client'
import { useState } from 'react'
function ContactForm({ siteKey }) {
@@ -44,7 +44,7 @@ Widget ładuje skrypt Turnstile sam (bez `next/script`), zwraca token przez
## Front — verify (server)
```ts
import { verifyTurnstile } from 'ipal-kit/server'
import { verifyTurnstile } from '@intecion/ipal-kit/server'
const ok = await verifyTurnstile({ token, payload, ip })
if (!ok) {