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
+51 -11
View File
@@ -33,14 +33,29 @@ theirs, and don't put it in any file that ends up in a repository.
### Store the token in your environment
Don't paste the token straight into project files. Keep it in an environment
variable, so your install config carries no secret.
variable, so your install config carries no secret. How you set it depends on
your system:
**macOS** (default shell is zsh):
```bash
# add to ~/.zshrc (or ~/.bashrc)
export GITEA_TOKEN=paste_your_token_here
echo 'export GITEA_TOKEN=paste_your_token_here' >> ~/.zshrc
source ~/.zshrc
```
Reload your shell (`source ~/.zshrc`) or open a new terminal window.
**Linux / Ubuntu** (default shell is bash):
```bash
echo 'export GITEA_TOKEN=paste_your_token_here' >> ~/.bashrc
source ~/.bashrc
```
**Windows (PowerShell)** — set it permanently for your user, then open a new
terminal so it takes effect:
```powershell
setx GITEA_TOKEN "paste_your_token_here"
```
> On Windows, `setx` writes the variable but does **not** affect the current
> window — close it and open a new PowerShell for the token to be visible.
---
@@ -55,16 +70,40 @@ specific, unreleased commit straight from the repository.
The plugin is published to the package registry in Gitea. You pull a ready-built
package and build nothing locally.
**1. Configure the registry.** Create an `.npmrc` file in your project directory
(or add these two lines to `~/.npmrc` to make it work globally):
**1. Configure the registry.** Add these two lines to an `.npmrc` file:
```
@intecion:registry=https://git.intecion.net/api/packages/IntecionSoftware/npm/
//git.intecion.net/api/packages/IntecionSoftware/npm/:_authToken=${GITEA_TOKEN}
```
Because the token lives in the `${GITEA_TOKEN}` variable, this file holds no
secret — you can safely commit it.
You can put this file **in the project directory** (applies to that project) or
**globally** (applies everywhere). The global location differs by system:
| System | Global `.npmrc` path |
|---|---|
| macOS | `~/.npmrc` (i.e. `/Users/you/.npmrc`) |
| Linux / Ubuntu | `~/.npmrc` (i.e. `/home/you/.npmrc`) |
| Windows | `%USERPROFILE%\.npmrc` (i.e. `C:\Users\you\.npmrc`) |
Fastest way to create the global file:
```bash
# macOS / Linux
npm config set @intecion:registry https://git.intecion.net/api/packages/IntecionSoftware/npm/
```
```powershell
# Windows (PowerShell) — same command, npm handles the path
npm config set @intecion:registry https://git.intecion.net/api/packages/IntecionSoftware/npm/
```
Then add the auth line manually (npm config doesn't set tokens with variables).
Because the token lives in the `${GITEA_TOKEN}` variable, the file holds no
secret — you can safely commit a project-level `.npmrc`.
> **Windows note:** the `${GITEA_TOKEN}` syntax in `.npmrc` is expanded by npm/pnpm
> itself, not by the shell — so it works the same on Windows as on macOS/Linux,
> as long as you set the variable with `setx` (see above).
**2. Install:**
@@ -141,7 +180,7 @@ nobody gets stuck on a stale one from the cache.
Everything lives in the **[docs/](./docs)** directory. To get started:
- **[getting-started.md](./docs/getting-started.md)** — project setup, step by step
- **[install.md](./docs/install.md)** — more on installation and troubleshooting
- **[publishing.md](./docs/publishing.md)** — releasing new plugin versions
- **[README.md](./docs/README.md)** — index of the plugin's modules
---
@@ -155,5 +194,6 @@ Everything lives in the **[docs/](./docs)** directory. To get started:
| `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED` | an attempt to build on install from git — report it to whoever publishes the plugin |
| old code after reinstalling | cache: run `pnpm store prune`, then remove `.next` and `node_modules/@intecion` |
If you get stuck, check **[docs/install.md](./docs/install.md)** or message the
team that maintains the plugin.
If you get stuck, check the troubleshooting table above, see
**[docs/publishing.md](./docs/publishing.md)** for distribution issues, or message
the team that maintains the plugin.
+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) {