Files

232 lines
8.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# storage — media na Cloudflare R2
Offload mediów (obrazy, pliki) do Cloudflare R2 zamiast lokalnego dysku. R2 jest
S3-kompatybilny; plugin dostarcza `buildR2Storage`, który czyta dane z `.env`
i konfiguruje adapter.
> **Storage to infrastruktura, nie treść.** Dane R2 (klucze, bucket) idą do
> `.env` — jak DATABASE_URI, PAYLOAD_SECRET, GRAPH_*. NIE do panelu (to sekrety
> agencyjne, wiążą się przy starcie, nie zmienia ich redaktor).
## Zależność
```bash
pnpm add @payloadcms/storage-s3
```
## Zmienne .env
Patrz [R2-ENV-przyklad](../R2-ENV-przyklad.md) po pełną instrukcję skąd wziąć wartości.
```bash
R2_BUCKET=nazwa-bucketa
R2_ENDPOINT=https://<ACCOUNT_ID>.r2.cloudflarestorage.com
R2_ACCESS_KEY_ID=<access-key-id>
R2_SECRET_ACCESS_KEY=<secret-access-key>
```
## Wpięcie (payload.config.ts)
```ts
import { buildR2Storage } from '@intecion/ipal-kit'
export default buildConfig({
// ...
plugins: [
ipalKit({ /* ... */ }),
buildR2Storage(['media']), // slugi kolekcji upload do offloadu
],
})
```
`buildR2Storage` przyjmuje listę kolekcji upload (domyślnie `['media']`). Jeśli
masz więcej kolekcji plików: `buildR2Storage(['media', 'documents'])`.
## Zachowanie (fallback)
- **Wszystkie 4 zmienne** → media w R2.
- **Brak zmiennych** → fallback na lokalny dysk (dev działa bez R2, zero konfiguracji).
- **Część zmiennych** → ostrzeżenie w logu + fallback (częściowa konfiguracja =
pewnie pomyłka).
To wzorzec „degrade gracefully" — jak mailAdapter, który wraca do SMTP, gdy brak
Graph. Projekt działa niezależnie od tego, czy R2 jest skonfigurowany.
## Publiczny dostęp + custom domena (WAŻNE — krok po kroku)
R2 domyślnie prywatny. Upload zadziała, ale obrazy się NIE wyświetlą (403),
dopóki nie skonfigurujesz publicznego odczytu przez custom domenę. To proces
w Cloudflare (nie w kodzie), wieloetapowy — poniżej dokładnie.
### Dlaczego custom domena, nie „r2.dev"
R2 oferuje szybki publiczny URL `*.r2.dev`, ALE:
- jest rate-limitowany (nie do produkcji)
- nie przechodzi przez cache Cloudflare (brak CDN, wolniej, drożej)
- brzydki URL (nie Twoja domena)
Dla produkcji ZAWSZE custom domena (np. `media.klient.pl`) — daje CDN, cache,
własny URL. r2.dev tylko do szybkiego testu.
### Warunek wstępny: domena w Cloudflare
Custom domena dla R2 wymaga, żeby domena (albo subdomena) była zarządzana przez
Cloudflare (nameservery klienta wskazują na Cloudflare). Jeśli domena klienta
jest u innego rejestratora — trzeba ją najpierw dodać do Cloudflare (Add Site)
i przełączyć nameservery. Sama subdomena `media.klient.pl` wystarczy, jeśli
główna domena jest już w Cloudflare.
### Krok po kroku — podpięcie custom domeny
1. **Cloudflare Dashboard → R2 → wybierz bucket**
2. Zakładka **Settings** → sekcja **Public access** → **Custom Domains**
3. **Connect Domain** → wpisz subdomenę, np. `media.klient.pl`
4. Cloudflare automatycznie doda rekord CNAME (bo domena jest w Cloudflare) i
wystawi certyfikat SSL. Poczekaj, aż status = **Active** (kilka minut).
5. Od tej chwili pliki są publiczne pod `https://media.klient.pl/<klucz-pliku>`.
### Krok: ustaw publiczny URL w projekcie
Payload musi generować URL-e mediów wskazujące na custom domenę, nie na endpoint
S3. Dodaj zmienną i przekaż ją do adaptera:
```bash
# .env
R2_PUBLIC_URL=https://media.klient.pl
```
Adapter `buildR2Storage` czyta ją i ustawia jako bazowy URL mediów (jeśli
ustawiona). Bez niej Payload zwróci URL wskazujący na prywatny endpoint S3 →
403 na froncie. (Patrz aktualizacja buildR2Storage niżej.)
### Weryfikacja
1. Wgraj obraz w panelu (Media).
2. Sprawdź URL obrazu w panelu — powinien być `https://media.klient.pl/...`,
NIE `https://<account>.r2.cloudflarestorage.com/...`.
3. Otwórz URL w przeglądarce — obraz się pokazuje (nie 403).
4. Na froncie `<img src>` działa.
### Częsty błąd: 403 mimo custom domeny
- **URL wskazuje na endpoint S3, nie custom domenę** → brakuje `R2_PUBLIC_URL`
albo adapter jej nie używa. Sprawdź URL w panelu.
- **Custom domena nie Active** → poczekaj na SSL/CNAME w Cloudflare.
- **Public access wyłączony** → w bucket Settings sprawdź, czy custom domena jest
podpięta (nie tylko utworzona).
Bez tego media wgrają się do R2, ale front pokaże 403. Konfiguracja domeny jest
po stronie Cloudflare, publiczny URL po stronie projektu (.env).
## Migracja istniejących mediów
Jeśli projekt miał media lokalnie i przełączasz na R2 — nowe uploady idą do R2,
ale STARE zostają na dysku (i znikną przy redeployu bez wolumenu). Przed
przełączeniem na produkcji przenieś istniejące pliki do bucketa (np. `rclone`
albo ręcznie przez R2 dashboard), inaczej stare obrazy znikną.
## Weryfikacja
```bash
# po wpięciu i ustawieniu .env:
pnpm dev
# wgraj obraz w panelu (Media) → sprawdź w Cloudflare R2, czy plik się pojawił
```
## Root subdomeny media zwraca 404 (to normalne)
`media.klient.pl/plik.jpg` → R2 zwraca plik. Ale `media.klient.pl/` (sam root,
bez pliku) → **404**, bo R2 nie ma obiektu pod rootem. To NORMALNE zachowanie
R2, nie błąd.
Audyty SEO (Screaming Frog) czasem zgłaszają to 404 — bo crawler widzi URL-e
plików (`media.../logo.svg`) i próbuje roota. Ale:
- **NIE linkuj do samego roota** `media.klient.pl/` — tylko do plików. Kod nie
powinien nigdzie mieć `media.klient.pl/` bez nazwy pliku.
- Root media 404 **nie szkodzi SEO** głównej domeny (Google indeksuje klient.pl,
nie media.klient.pl). Nikt nie trafia na root media.
**Plugin tego nie naprawi** — subdomena media to serwis R2/Cloudflare, nie
aplikacja Next. Żądania do media.klient.pl nie docierają do Twojego kodu.
Jeśli chcesz „czysto" w Search Console (opcjonalne): Cloudflare → Rules →
Redirect Rules → gdy hostname = `media.klient.pl` i path = `/` → 301 na
`klient.pl`. Jednorazowo w panelu CF. Ale to kosmetyka — root media 404 jest
nieszkodliwe.
## Dev na lokalnym I na R2 (seedowanie podczas developmentu)
Fallback (brak zmiennych → lokalny dysk) oznacza, że **dev działa w obu trybach**:
- **Dev bez R2 w .env** → media na lokalnym dysku. Szybki start, zero konfiguracji.
- **Dev z R2 w .env** → media w R2 już podczas developmentu. Przydatne, gdy
seedujesz treść w devie i chcesz, żeby od razu lądowała w buckecie (np. wspólny
bucket dev, albo test realnego flow przed produkcją).
Przełączasz trybem po prostu obecnością zmiennych R2 w `.env`. Ten sam kod,
`buildR2Storage` sam wykrywa. Nie musisz nic zmieniać w configu między trybami.
> Jeśli seedujesz w devie do R2 — pamiętaj, że to realny bucket. Używaj osobnego
> bucketa dev (nie produkcyjnego), żeby nie mieszać danych testowych z realnymi.
## Normalizacja nazw plików (automatyczna)
Plik `normalizeFilenameHook` czyści nazwy wgrywanych plików — slugifikuje nazwę,
zachowuje rozszerzenie:
```
"Zdjęcie jeden nad morzem.jpg" → "zdjecie-jeden-nad-morzem.jpg"
"Faktura #12 (2024).PDF" → "faktura-12-2024.pdf"
```
Wpięcie w kolekcję Media (projekt):
```ts
import { normalizeFilenameHook } from '@intecion/ipal-kit'
export const Media: CollectionConfig = {
slug: 'media',
upload: { staticDir: 'media' /* ... */ },
hooks: {
beforeOperation: [normalizeFilenameHook], // czyści nazwę przed zapisem
},
fields: [ /* alt itd. */ ],
}
```
Działa z lokalnym dyskiem i z R2/S3 (hook biegnie PRZED warstwą storage, więc
czysta nazwa trafia i do bazy, i do bucketa). Dlaczego to ważne:
- **URL-e mediów są czyste** — `/media/zdjecie-nad-morzem.jpg`, nie
`/media/Zdjęcie%20jeden%20nad%20morzem.jpg` (spacje/diakrytyki w URL = problemy).
- **Przenośność** — nazwa bez polskich znaków/spacji działa wszędzie (CDN, S3, systemy plików).
- **Bez kolizji kodowania** — spacje i `#`, `()` w nazwach plików potrafią psuć
ścieżki i cache.
Sama funkcja `normalizeFilename(name)` też jest wyeksportowana, gdybyś potrzebował
jej poza hookiem.
## Preconnect do domeny mediów (wydajność)
Komponent `MediaPreconnect` generuje `<link rel="preconnect">` + `dns-prefetch`
dla domeny mediów (R2_PUBLIC_URL) — przeglądarka nawiązuje połączenie TLS/DNS
z CDN zawczasu, zanim napotka pierwszy `<img>`. Zysk ~150–300 ms na pierwszym
obrazie.
```tsx
// layout.tsx — w <head> (albo górze <body>, Next hoistuje link tagi)
import { MediaPreconnect } from '@intecion/ipal-kit/rsc'
<head>
<MediaPreconnect />
</head>
```
Czyta domenę z **R2_PUBLIC_URL** (to samo źródło co buildR2Storage) — zero
hardkodu, jedno źródło prawdy. Gdy R2_PUBLIC_URL nie ustawione (lokalny dysk,
brak CDN) → nie renderuje nic. Zmiana domeny mediów = zmiana jednej zmiennej
env, komponent podąża automatycznie.
> NIE hardkoduj `<link rel="preconnect" href="https://media.klient.pl">` ręcznie
> w layoutcie — to zaszywa domenę klienta w kodzie. Użyj MediaPreconnect, który
> bierze ją z env.