Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db2ac80a1e | ||
|
|
99490bfc4d |
@@ -129,6 +129,22 @@ const nextConfig: NextConfig = {
|
|||||||
Weryfikacja: `curl -A "Googlebot" URL | grep canonical` — musi być w `<head>`.
|
Weryfikacja: `curl -A "Googlebot" URL | grep canonical` — musi być w `<head>`.
|
||||||
Szczegóły i objawy: seo.md (sekcja htmlLimitedBots).
|
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 `<head>` → 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)
|
## 3b. Pułapka: prerender tras zależnych od bazy (KONIECZNE)
|
||||||
|
|
||||||
Next domyślnie **prerenderuje** trasy typu `sitemap.ts` w czasie `next build` —
|
Next domyślnie **prerenderuje** trasy typu `sitemap.ts` w czasie `next build` —
|
||||||
|
|||||||
@@ -359,7 +359,11 @@ export default async function LocaleLayout({ children, params }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={locale}>
|
<html lang={locale}>
|
||||||
|
{/* BEZ jawnego <head>! Sztywny <head> wypycha metadata do <body>
|
||||||
|
(canonical/title poza head → crawlery ich nie widzą). Next zarządza
|
||||||
|
<head> sam; MediaPreconnect w body, React 19 hoistuje link do head. */}
|
||||||
<body>
|
<body>
|
||||||
|
<MediaPreconnect /> {/* preconnect CDN, jeśli R2 */}
|
||||||
<ConsentProvider texts={texts}>
|
<ConsentProvider texts={texts}>
|
||||||
<main>{children}</main>
|
<main>{children}</main>
|
||||||
<CookieBanner />
|
<CookieBanner />
|
||||||
@@ -392,6 +396,11 @@ const pageMetadata = createPageMetadata({
|
|||||||
baseUrl: process.env.NEXT_PUBLIC_SERVER_URL,
|
baseUrl: process.env.NEXT_PUBLIC_SERVER_URL,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ISR — cache strony z gotowym <head>. 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<Metadata> {
|
export async function generateMetadata({ params }): Promise<Metadata> {
|
||||||
const { locale, slug } = await params
|
const { locale, slug } = await params
|
||||||
return pageMetadata({ payload: await getCachedPayload(), locale, slug })
|
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)
|
- brak slug (`/pl`) → home przez System Pages (nie hardkod slug)
|
||||||
- slug (`/pl/o-nas`) → resolveRoute po slug w danym locale
|
- slug (`/pl/o-nas`) → resolveRoute po slug w danym locale
|
||||||
|
- **ISR (`revalidate`)** → metadata zawsze w `<head>` (nie body), szybki TTFB.
|
||||||
|
KRYTYCZNE dla SEO — patrz seo.md (metadata w head).
|
||||||
- `depth: 2` → relacje w blokach (form) się populują
|
- `depth: 2` → relacje w blokach (form) się populują
|
||||||
|
|
||||||
## 11. Metadata / SEO (szczegóły)
|
## 11. Metadata / SEO (szczegóły)
|
||||||
@@ -486,10 +497,15 @@ przypisanie strony-archiwum w System Pages.
|
|||||||
```ts
|
```ts
|
||||||
// app/sitemap.ts
|
// app/sitemap.ts
|
||||||
export { sitemap as default } from '@/lib/content'
|
export { sitemap as default } from '@/lib/content'
|
||||||
|
export const dynamic = 'force-dynamic' // KONIECZNE dla deployu kontenerowego
|
||||||
// app/robots.ts
|
// app/robots.ts
|
||||||
export { robots as default } from '@/lib/content'
|
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
|
## Kiedy coś nie działa
|
||||||
|
|||||||
+74
-25
@@ -482,54 +482,103 @@ typu, itp.):
|
|||||||
- jeśli to uniwersalne i powtarzalne → rozważ zgłoszenie do pluginu zamiast
|
- jeśli to uniwersalne i powtarzalne → rozważ zgłoszenie do pluginu zamiast
|
||||||
ręcznie (patrz ANTIGRAVITY-ZASADY-AGENT.md A0)
|
ręcznie (patrz ANTIGRAVITY-ZASADY-AGENT.md A0)
|
||||||
|
|
||||||
## KRYTYCZNE: metadata w <head> dla Google (htmlLimitedBots)
|
## KRYTYCZNE: metadata w <head> dla Google
|
||||||
|
|
||||||
**Największa pułapka SEO w Next.js — dotyczy KAŻDEGO projektu.** Dla dynamicznie
|
**Największa pułapka SEO w Next.js — dotyczy KAŻDEGO projektu.** Dla dynamicznie
|
||||||
renderowanych stron (SSR) Next.js **streamuje metadata do `<body>`**, nie `<head>`,
|
renderowanych stron (SSR) Next.js **streamuje metadata do `<body>`**, nie `<head>`,
|
||||||
i przenosi ją do head skryptem JS. Skutek: canonical, hreflang, title, favicon
|
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,
|
lądują w body w surowym HTML. Crawlery bez JS (Screaming Frog, część botów) widzą
|
||||||
część botów), widzą je poza head → ignorują → utrata SEO.
|
je poza head → ignorują → utrata SEO.
|
||||||
|
|
||||||
Google *twierdzi*, że wykonuje JS i widzi przeniesione tagi, ale praktyka
|
**To wyścig czasowy (race condition):** gdy baza odpowie szybko, metadata zdąży
|
||||||
(i audyty) pokazują realne problemy z indeksacją canonical. Bezpieczniej wymusić
|
do head; gdy wolniej (albo crawler odpytuje wiele stron naraz, obciążając bazę),
|
||||||
metadata do head dla crawlerów.
|
Next zamyka `</head>` i dokleja metadata w `<body>`. 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
|
||||||
|
`<head>`, 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 <head> z layoutu
|
||||||
|
|
||||||
|
Sztywny `<head>` w layoucie App Router wymusza przedwczesne zamknięcie head —
|
||||||
|
zanim strona wygeneruje metadane. To pcha metadata do body. NIE deklaruj `<head>`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ŹLE — jawny <head> zamyka head za wcześnie
|
||||||
|
<html lang={locale}>
|
||||||
|
<head><MediaPreconnect /></head>
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
// DOBRZE — MediaPreconnect w body, React 19 hoistuje link do head
|
||||||
|
<html lang={locale}>
|
||||||
|
<body>
|
||||||
|
<MediaPreconnect />
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
React 19 sam przenosi `<link rel="preconnect">` do head. Jawny `<head>` 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
|
```ts
|
||||||
// next.config.ts
|
// next.config.ts
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
// Wymusza blocking metadata (canonical, hreflang, title, favicon) w <head>
|
|
||||||
// dla crawlerów SEO — zamiast streamingu do <body>.
|
|
||||||
htmlLimitedBots:
|
htmlLimitedBots:
|
||||||
/Googlebot|Google-InspectionTool|Storebot-Google|Bingbot|Yandex|DuckDuckBot|Baiduspider|Screaming Frog|AhrefsBot|SemrushBot/i,
|
/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
|
`htmlLimitedBots` wyłącza streaming dla tych User-Agentów (metadata w head). ALE
|
||||||
metadata do `<head>` w surowym HTML (blocking). Użytkownicy dalej dostają
|
to słabsze niż ISR — bo strona dalej renderuje dynamicznie (zapytanie do bazy →
|
||||||
streaming (szybkie ładowanie); crawlery dostają poprawny head.
|
wolniej, ryzyko race). **ISR eliminuje przyczynę, htmlLimitedBots łagodzi objaw.**
|
||||||
|
Najlepiej: ISR + brak jawnego head. htmlLimitedBots jako dodatkowa warstwa.
|
||||||
|
|
||||||
### Objawy (że masz ten problem)
|
### Objawy (że masz ten problem)
|
||||||
|
|
||||||
- Screaming Frog: „canonical/hreflang/title outside <head>"
|
- Screaming Frog: „canonical/hreflang/title outside <head>" (na wielu podstronach)
|
||||||
- Search Console: „brak canonical", favicon nie pokazuje się (glob)
|
- Search Console: „brak canonical", favicon glob
|
||||||
- W surowym HTML canonical/title są PO `</head>`, na końcu body, ze skryptem
|
- Surowy HTML: canonical/title PO `</head>`, na końcu body, ze skryptem appendChild
|
||||||
`document.querySelectorAll('body link[rel=icon]')...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
|
```bash
|
||||||
# jako Googlebot — metadata MUSI być w <head>
|
# 10 zapytań jako Googlebot — ile ma canonical w <head>
|
||||||
curl -A "Googlebot" https://twojadomena.pl/pl/strona | grep -o '<head>.*</head>' | grep canonical
|
for i in $(seq 1 10); do
|
||||||
# jako user — streaming (metadata w body — OK dla ludzi wykonujących JS)
|
curl -s -A "Googlebot" https://twojadomena.pl/pl/strona | \
|
||||||
curl -A "Mozilla/5.0" https://twojadomena.pl/pl/strona
|
python3 -c "import sys; h=sys.stdin.read(); e=h.find('</head>'); c=h.find('rel=\"canonical\"'); print('head' if 0<c<e else 'BODY')"
|
||||||
|
done
|
||||||
|
# Cel: 10x 'head'. Jeśli część 'BODY' → race condition, dodaj ISR.
|
||||||
```
|
```
|
||||||
|
|
||||||
Bez htmlLimitedBots ten sam problem dotknie favicon (glob w Google), canonical
|
Testuj też PODSTRONY (nie tylko główną) — race częściej dotyka podstron.
|
||||||
(„User-declared canonical: None"), hreflang i title. Jedna linia w config
|
DevTools (F12) NIE nadaje się do testu — hoistuje tagi do head automatycznie,
|
||||||
naprawia wszystko naraz.
|
pokazując fałszywie poprawny head. Używaj `curl` / Ctrl+U (surowe źródło).
|
||||||
|
|
||||||
## SEO wielojęzyczne — hreflang, x-default, redirect roota
|
## SEO wielojęzyczne — hreflang, x-default, redirect roota
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@intecion/ipal-kit",
|
"name": "@intecion/ipal-kit",
|
||||||
"version": "1.2.3",
|
"version": "1.2.4",
|
||||||
"description": "Intecion Payload Advanced Library — a Payload CMS 3 plugin: i18n, SEO, forms, consent, analytics, blog/archives.",
|
"description": "Intecion Payload Advanced Library — a Payload CMS 3 plugin: i18n, SEO, forms, consent, analytics, blog/archives.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
Reference in New Issue
Block a user