From 99490bfc4da42ef0d84b74c654e0ffdc9c1cb19d Mon Sep 17 00:00:00 2001 From: rasm-its Date: Wed, 9 Sep 2026 00:06:18 +0200 Subject: [PATCH] Updated docs --- docs/deployment.md | 18 +++++++- docs/getting-started.md | 16 +++++++ docs/seo.md | 99 ++++++++++++++++++++++++++++++----------- 3 files changed, 107 insertions(+), 26 deletions(-) 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