diff --git a/docs/deployment.md b/docs/deployment.md
index 08221e7..2240d1d 100644
--- a/docs/deployment.md
+++ b/docs/deployment.md
@@ -121,7 +121,7 @@ utrata SEO. **Każdy projekt** tego potrzebuje w next.config:
```ts
const nextConfig: NextConfig = {
htmlLimitedBots:
- /Googlebot|Google-InspectionTool|Bingbot|Yandex|DuckDuckBot|Screaming Frog|AhrefsBot|SemrushBot/i,
+ /Googlebot|Google-InspectionTool|Bingbot|Yandex|DuckDuckBot|Screaming Frog|AhrefsBot|SemrushBot/i,
// ...
}
```
@@ -129,6 +129,22 @@ const nextConfig: NextConfig = {
Weryfikacja: `curl -A "Googlebot" URL | grep canonical` — musi być w `
`.
Szczegóły i objawy: seo.md (sekcja htmlLimitedBots).
+## 3a2. ISR — cache stron (metadata w head + szybkość)
+
+Dla stron contentowych (page.tsx) użyj ISR: `export const revalidate = 3600`.
+Cache'uje całą stronę z gotowym `` → metadata zawsze w head (nie body,
+brak race condition streamingu), TTFB ~20ms, brak 503 (cold start).
+
+```ts
+// app/(frontend)/[locale]/[[...slug]]/page.tsx
+export const revalidate = 3600 // 1h; albo krócej, albo on-demand
+```
+
+UWAGA: ISR i `force-dynamic` się WYKLUCZAJĄ. Strony → ISR (revalidate).
+sitemap/robots → force-dynamic (bo generują przy żądaniu). Nie mieszaj na jednej
+trasie. Treść z panelu: ISR = redaktor czeka do rewalidacji; rozważ on-demand
+revalidation (hook afterChange → revalidatePath). Patrz seo.md, HOOKS.md.
+
## 3b. Pułapka: prerender tras zależnych od bazy (KONIECZNE)
Next domyślnie **prerenderuje** trasy typu `sitemap.ts` w czasie `next build` —
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 823d7d0..a43ef9f 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -359,7 +359,11 @@ export default async function LocaleLayout({ children, params }) {
return (
+ {/* BEZ jawnego ! Sztywny wypycha metadata do
+ (canonical/title poza head → crawlery ich nie widzą). Next zarządza
+ sam; MediaPreconnect w body, React 19 hoistuje link do head. */}
+ {/* preconnect CDN, jeśli R2 */}
{children}
@@ -392,6 +396,11 @@ const pageMetadata = createPageMetadata({
baseUrl: process.env.NEXT_PUBLIC_SERVER_URL,
})
+// ISR — cache strony z gotowym . Eliminuje race condition streamingu
+// metadata (canonical/title zawsze w head, nie w body). TTFB ~20ms, brak 503.
+// Redaktor widzi zmiany po rewalidacji — patrz seo.md (ISR a treść z panelu).
+export const revalidate = 3600
+
export async function generateMetadata({ params }): Promise {
const { locale, slug } = await params
return pageMetadata({ payload: await getCachedPayload(), locale, slug })
@@ -408,6 +417,8 @@ export default async function Page({ params, searchParams }) {
- brak slug (`/pl`) → home przez System Pages (nie hardkod slug)
- slug (`/pl/o-nas`) → resolveRoute po slug w danym locale
+- **ISR (`revalidate`)** → metadata zawsze w `` (nie body), szybki TTFB.
+ KRYTYCZNE dla SEO — patrz seo.md (metadata w head).
- `depth: 2` → relacje w blokach (form) się populują
## 11. Metadata / SEO (szczegóły)
@@ -486,10 +497,15 @@ przypisanie strony-archiwum w System Pages.
```ts
// app/sitemap.ts
export { sitemap as default } from '@/lib/content'
+export const dynamic = 'force-dynamic' // KONIECZNE dla deployu kontenerowego
// app/robots.ts
export { robots as default } from '@/lib/content'
```
+`force-dynamic` w sitemap.ts jest wymagane przy deployu w kontenerze (Coolify/
+Docker) — bez niego build próbuje prerenderować sitemap i łączy się z bazą,
+której kontener budujący nie widzi → build pada. Szczegóły: [deployment.md](./deployment.md).
+
---
## Kiedy coś nie działa
diff --git a/docs/seo.md b/docs/seo.md
index 29c93a7..d7031c4 100644
--- a/docs/seo.md
+++ b/docs/seo.md
@@ -482,54 +482,103 @@ typu, itp.):
- jeśli to uniwersalne i powtarzalne → rozważ zgłoszenie do pluginu zamiast
ręcznie (patrz ANTIGRAVITY-ZASADY-AGENT.md A0)
-## KRYTYCZNE: metadata w dla Google (htmlLimitedBots)
+## KRYTYCZNE: metadata w dla Google
**Największa pułapka SEO w Next.js — dotyczy KAŻDEGO projektu.** Dla dynamicznie
renderowanych stron (SSR) Next.js **streamuje metadata do ``**, nie ``,
i przenosi ją do head skryptem JS. Skutek: canonical, hreflang, title, favicon
-lądują w body w surowym HTML. Crawlery, które nie wykonują JS (Screaming Frog,
-część botów), widzą je poza head → ignorują → utrata SEO.
+lądują w body w surowym HTML. Crawlery bez JS (Screaming Frog, część botów) widzą
+je poza head → ignorują → utrata SEO.
-Google *twierdzi*, że wykonuje JS i widzi przeniesione tagi, ale praktyka
-(i audyty) pokazują realne problemy z indeksacją canonical. Bezpieczniej wymusić
-metadata do head dla crawlerów.
+**To wyścig czasowy (race condition):** gdy baza odpowie szybko, metadata zdąży
+do head; gdy wolniej (albo crawler odpytuje wiele stron naraz, obciążając bazę),
+Next zamyka `` i dokleja metadata w ``. Dlatego pojedynczy `curl`
+może pokazać head OK, a test 10 zapytań — 5/10 w body. **Testuj wielokrotnie.**
-### Rozwiązanie — htmlLimitedBots w next.config
+### Rozwiązanie GŁÓWNE — ISR (revalidate) w stronach
+
+Najskuteczniejsze: **cache całej strony (ISR)**. Strona generowana raz z gotowym
+``, kolejne żądania serwują cache — zero zapytań do bazy przy renderowaniu,
+więc race condition ZNIKA (metadata zawsze w head). Bonus: TTFB spada z ~500ms do
+~20ms, znikają sporadyczne 503 (cold start).
+
+```ts
+// app/(frontend)/[locale]/[[...slug]]/page.tsx
+export const revalidate = 3600 // cache 1h, regeneracja w tle
+```
+
+> **UWAGA — ISR a treść z panelu:** strona cache'owana `revalidate` sekund NIE
+> pokaże zmian redaktora od razu (czeka do rewalidacji). Dla treści zmienianej
+> rzadko OK. Jeśli redaktor ma widzieć zmiany natychmiast — użyj **on-demand
+> revalidation**: hook `afterChange` w kolekcji → `revalidatePath(path)` (patrz
+> HOOKS.md). Albo krótszy `revalidate` (np. 300 = 5 min). NIE łącz ISR z
+> `force-dynamic` — wykluczają się.
+
+### Rozwiązanie DRUGIE — usuń jawny z layoutu
+
+Sztywny `` w layoucie App Router wymusza przedwczesne zamknięcie head —
+zanim strona wygeneruje metadane. To pcha metadata do body. NIE deklaruj ``:
+
+```tsx
+// ŹLE — jawny zamyka head za wcześnie
+
+
+ {children}
+
+
+// DOBRZE — MediaPreconnect w body, React 19 hoistuje link do head
+
+
+
+ {children}
+
+
+```
+
+React 19 sam przenosi `` do head. Jawny `` jest
+zbędny i szkodliwy (wymusza wczesne zamknięcie).
+
+### Rozwiązanie TRZECIE — htmlLimitedBots (uzupełnienie)
+
+Dodatkowo można wymusić blocking metadata dla crawlerów (przydatne, gdy strona
+z jakiegoś powodu nie może być ISR):
```ts
// next.config.ts
const nextConfig: NextConfig = {
- // Wymusza blocking metadata (canonical, hreflang, title, favicon) w
- // dla crawlerów SEO — zamiast streamingu do .
htmlLimitedBots:
/Googlebot|Google-InspectionTool|Storebot-Google|Bingbot|Yandex|DuckDuckBot|Baiduspider|Screaming Frog|AhrefsBot|SemrushBot/i,
- // ...reszta
}
```
-`htmlLimitedBots` mówi Next: dla tych User-Agentów wyłącz streaming, wstaw
-metadata do `` w surowym HTML (blocking). Użytkownicy dalej dostają
-streaming (szybkie ładowanie); crawlery dostają poprawny head.
+`htmlLimitedBots` wyłącza streaming dla tych User-Agentów (metadata w head). ALE
+to słabsze niż ISR — bo strona dalej renderuje dynamicznie (zapytanie do bazy →
+wolniej, ryzyko race). **ISR eliminuje przyczynę, htmlLimitedBots łagodzi objaw.**
+Najlepiej: ISR + brak jawnego head. htmlLimitedBots jako dodatkowa warstwa.
### Objawy (że masz ten problem)
-- Screaming Frog: „canonical/hreflang/title outside "
-- Search Console: „brak canonical", favicon nie pokazuje się (glob)
-- W surowym HTML canonical/title są PO ``, na końcu body, ze skryptem
- `document.querySelectorAll('body link[rel=icon]')...appendChild`
+- Screaming Frog: „canonical/hreflang/title outside " (na wielu podstronach)
+- Search Console: „brak canonical", favicon glob
+- Surowy HTML: canonical/title PO ``, na końcu body, ze skryptem appendChild
+- Test 10 zapytań: część w head, część w body (race condition)
-### Weryfikacja
+### Weryfikacja — TESTUJ WIELOKROTNIE (nie pojedynczo)
+
+Pojedynczy `curl` może trafić w „szczęśliwy" timing. Testuj 10 razy:
```bash
-# jako Googlebot — metadata MUSI być w
-curl -A "Googlebot" https://twojadomena.pl/pl/strona | grep -o '.*' | grep canonical
-# jako user — streaming (metadata w body — OK dla ludzi wykonujących JS)
-curl -A "Mozilla/5.0" https://twojadomena.pl/pl/strona
+# 10 zapytań jako Googlebot — ile ma canonical w
+for i in $(seq 1 10); do
+ curl -s -A "Googlebot" https://twojadomena.pl/pl/strona | \
+ python3 -c "import sys; h=sys.stdin.read(); e=h.find(''); c=h.find('rel=\"canonical\"'); print('head' if 0