Added support for single localization
This commit is contained in:
@@ -10,6 +10,30 @@ registry (prop), obsługuje zagnieżdżanie i rozszerzenia per-blok. Bloki
|
||||
Brak opcji w payload.config — bloki definiujesz w swoich kolekcjach
|
||||
(pole typu `blocks`). Plugin dostarcza tylko silnik renderujący.
|
||||
|
||||
## ⚠️ NIE pisz własnego renderera bloków (switch)
|
||||
|
||||
**Renderer bloków to `RenderBlocks` z pluginu — NIGDY własny `switch`/`if`.**
|
||||
Częsty błąd: projekt pisze własny `BlockRenderer` z `switch (block.blockType)`
|
||||
i 20 case'ami. To łamie A0 — plugin ma silnik, projekt dostarcza tylko MAPĘ
|
||||
komponentów.
|
||||
|
||||
```tsx
|
||||
// ŹLE — własny switch w projekcie (gadatliwy, bez enhanceProps, rośnie liniowo)
|
||||
switch (block.blockType) {
|
||||
case 'hero': return <Hero {...block} />
|
||||
case 'faq': return <FAQ {...block} />
|
||||
// ...20 case'ów
|
||||
}
|
||||
|
||||
// DOBRZE — mapa + RenderBlocks (silnik z pluginu)
|
||||
const registry = { hero: Hero, faq: FAQ, /* ... */ }
|
||||
<RenderBlocks blocks={page.layout} components={registry} />
|
||||
```
|
||||
|
||||
Dlaczego RenderBlocks, nie switch: enhanceProps (anchory nav, itp.), guardy,
|
||||
obsługa zagnieżdżeń, spójność między projektami. Switch tego nie ma i rośnie
|
||||
z każdym blokiem. Mapa jest płaska i deklaratywna.
|
||||
|
||||
## Front — RenderBlocks
|
||||
|
||||
Import z `@intecion/ipal-kit/rsc` (to komponent serwerowy):
|
||||
|
||||
+1
-1
@@ -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,
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
+95
-1
@@ -154,4 +154,98 @@ Zmiana języka (URL `/en` różny od cookie) → zapis nowego wyboru (za zgodą)
|
||||
|
||||
> **Migracja ze starej nazwy:** wcześniej cookie nazywało się `ipal-locale`.
|
||||
> Po zmianie na `NEXT_LOCALE` użytkownicy ze starą cookie przejdą raz ponowną
|
||||
> negocjację (stara cookie ignorowana). Jednorazowe, bez wpływu na nowe projekty.
|
||||
> negocjację (stara cookie ignorowana). Jednorazowe, bez wpływu na nowe projekty.
|
||||
|
||||
## Strona jednojęzyczna (bez prefiksu /pl)
|
||||
|
||||
Gdy projekt ma JEDEN język, adresy nie mają prefiksu locale: `/o-nas`, nie
|
||||
`/pl/o-nas`. Plugin wykrywa to automatycznie — **jeden locale w config = tryb
|
||||
jednojęzyczny**. Helpery (buildLocalizedPath, hreflang, middleware) dostosowują
|
||||
się same:
|
||||
|
||||
- **buildLocalizedPath** → `/o-nas` (bez `/pl`), home → `/`
|
||||
- **buildHreflangAlternates** → pusto (jeden język = brak alternatyw językowych)
|
||||
- **localeMiddleware** → pass-through (brak przekierowania `/` → `/pl`, brak negocjacji)
|
||||
- **canonical** → `https://klient.pl/o-nas` (bez prefiksu)
|
||||
|
||||
### Config — jeden locale
|
||||
|
||||
```ts
|
||||
// i18n.config.ts
|
||||
export const i18nConfig = {
|
||||
locales: [{ code: 'pl', label: 'Polski' }], // JEDEN locale
|
||||
defaultLocale: 'pl',
|
||||
}
|
||||
```
|
||||
|
||||
### Struktura katalogów — BEZ [locale]
|
||||
|
||||
To kluczowa różnica. Projekt jednojęzyczny NIE ma folderu `[locale]`:
|
||||
|
||||
```
|
||||
# JEDNOJĘZYCZNY (bez [locale])
|
||||
app/(frontend)/
|
||||
layout.tsx # locale stałe z config, nie z params
|
||||
not-found.tsx
|
||||
[[...slug]]/page.tsx # /o-nas, /kontakt
|
||||
|
||||
# WIELOJĘZYCZNY (z [locale]) — dla porównania
|
||||
app/(frontend)/[locale]/
|
||||
layout.tsx # locale z params
|
||||
[[...slug]]/page.tsx # /pl/o-nas, /en/about
|
||||
```
|
||||
|
||||
### Layout jednojęzyczny — locale z config
|
||||
|
||||
```tsx
|
||||
// app/(frontend)/layout.tsx (bez [locale])
|
||||
import { i18nConfig } from '@/i18n.config'
|
||||
|
||||
export default async function Layout({ children }: { children: React.ReactNode }) {
|
||||
const locale = i18nConfig.defaultLocale // stałe, nie z params
|
||||
const settings = await getSettings(locale)
|
||||
// ...reszta jak zwykle, ale locale jest stałe
|
||||
return <html lang={locale}>...</html>
|
||||
}
|
||||
```
|
||||
|
||||
### Strony jednojęzyczne — locale z config
|
||||
|
||||
```tsx
|
||||
// app/(frontend)/[[...slug]]/page.tsx (bez [locale])
|
||||
import { i18nConfig } from '@/i18n.config'
|
||||
|
||||
export async function generateMetadata({ params }) {
|
||||
const { slug } = await params // TYLKO slug, nie locale
|
||||
const locale = i18nConfig.defaultLocale // stałe
|
||||
return pageMetadata({ payload: await getCachedPayload(), locale, slug })
|
||||
}
|
||||
|
||||
export default async function Page({ params }) {
|
||||
const { slug } = await params
|
||||
const locale = i18nConfig.defaultLocale // stałe
|
||||
const route = await resolveRoute(locale, slug ?? [], pageNum)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
resolveRoute i inne helpery działają bez zmian — dostają stałe locale z config
|
||||
zamiast z URL. Cała różnica to: brak `[locale]` w strukturze, locale z config.
|
||||
|
||||
### Middleware/proxy — jednojęzyczny prawie go nie potrzebuje
|
||||
|
||||
Dla jednego locale middleware jest pass-through (nic nie przekierowuje). Możesz
|
||||
go pominąć albo zostawić — plugin i tak wykryje 1 locale i przepuści. Bez
|
||||
przełącznika języka (jeden język), bez cookie NEXT_LOCALE (nie ma co pamiętać).
|
||||
|
||||
### Przejście jedno- → wielojęzyczny (later)
|
||||
|
||||
Jeśli klient później doda drugi język, to PRZEBUDOWA, nie przełącznik:
|
||||
- dodaj locale do config
|
||||
- przenieś strukturę do `[locale]/`
|
||||
- layout/strony czytają locale z params
|
||||
- wróci prefiks `/pl`, `/en` + hreflang
|
||||
|
||||
Warto to przewidzieć na starcie: jeśli jest szansa na drugi język, rozważ od razu
|
||||
strukturę wielojęzyczną (z [locale]), nawet dla jednego locale — wtedy prefiks
|
||||
`/pl` jest, ale dodanie języka to tylko config, nie przebudowa struktury.
|
||||
+25
@@ -53,6 +53,31 @@ Per strona (tab SEO):
|
||||
|
||||
Domyślnie: `Tytuł | Nazwa witryny`.
|
||||
|
||||
### Skąd bierze się „Tytuł" (priorytet źródła)
|
||||
|
||||
Tytuł strony (część przed nazwą witryny) pochodzi z, w kolejności:
|
||||
|
||||
1. **titleOverride** — jeśli wypełniony, jest całym tytułem (bez składania).
|
||||
2. **meta.title** — tytuł SEO wpisany w tab SEO.
|
||||
3. **page.title** — nazwa dokumentu (np. „Sprzątanie biur”), gdy meta.title puste.
|
||||
|
||||
Punkt 3 (fallback na nazwę strony) działa na dwa sposoby, uzupełniające się:
|
||||
|
||||
- **buildAutoFillMetaHook** (przy ZAPISIE) — wypełnia puste `meta.title` z pola
|
||||
dokumentu (`title`). Jeśli wpięty w kolekcje, meta.title nigdy nie jest puste.
|
||||
- **pageTitle w buildMetadata** (przy RENDEROWANIU) — jeśli meta.title mimo to
|
||||
puste (np. auto-fill niewpięty), używa `page.title`. Druga linia obrony.
|
||||
|
||||
Efekt: strona bez wypełnionego SEO title i tak pokaże swoją nazwę w karcie, nie
|
||||
pusty tytuł ani sam siteName.
|
||||
|
||||
> **Uwaga — „Strona Główna” w tytule:** jeśli strona główna ma nazwę dokumentu
|
||||
> „Strona Główna” (i auto-fill skopiował ją do meta.title), tytuł wyjdzie
|
||||
> „Nazwa – Strona Główna” — bezużyteczne dla SEO. Napraw: wpisz **titleOverride**
|
||||
> dla strony głównej (np. „Firma X – Usługa Miasto”), albo zmień meta.title na
|
||||
> coś ze słowami kluczowymi. Fallback page.title nie pomoże, bo problemem jest
|
||||
> sama treść nazwy, nie brak tytułu.
|
||||
|
||||
## Front — createPageMetadata
|
||||
|
||||
Dla zwykłych stron. Zna konwencje pluginu (kolekcja pages, SiteSettings, System
|
||||
|
||||
@@ -133,6 +133,27 @@ 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**:
|
||||
|
||||
Reference in New Issue
Block a user