Dokumentacja mechanizmów wielojęzyczności w Intum CMS — Page, Paragraph, Layout, Site, Domain.
Powiązane: CMS API | Wytyczne CMS
Wprowadzenie
Intum CMS wspiera kilka strategii wielojęzyczności — od najprostszej (te same fields, różne lokale) po pełne tłumaczenia stron (osobne URL per język). Wybór strategii zależy od skali projektu i wymagań SEO.
Obsługiwane locale: pl, en, fr, cs, sk, de, es, uk (definiowane w Intum::LOCALES).
TL;DR — 3 sposoby zrobienia wielojęzycznej strony (na przykładzie /blog)
/blog)Konkretny case: chcesz mieć stronę pod path blog w kilku językach. Masz 3 podejścia — różnią się liczbą rekordów w DB, sposobem zarządzania treścią i URL-ami.
Sposób 1 — Jedna strona bez locale, tłumaczenia w fields
Tworzysz jeden rekord cms_pages z path: "blog" i locale: nil. Tłumaczenia trzymasz w fields jako locale-keyed hash:
{
"page": {
"code": "blog",
"path": "blog",
"locale": null,
"fields": {
"pl": { "title": "Blog firmowy", "intro": "Najnowsze wpisy..." },
"en": { "title": "Company Blog", "intro": "Latest posts..." }
}
}
}
Język wybierany w runtime przez Liquid wg domain.locale / site.locale / ?lang=. URL ten sam pod każdą domeną (firma.pl/blog, firma.com/blog). Patrz Strategia 1 + 2.
- ✅ Jedna strona w DB, zero duplikacji, łatwe dodanie nowego języka (dopisujesz klucz w
fields). - ❌ Ten sam URL na każdej domenie — brak różnych ścieżek per język. Sensowne tylko gdy treść jest 1:1 tłumaczona i path nie musi się różnić.
Sposób 2 — Dwie niezależne strony z tym samym path ale różnym locale
Tworzysz dwa rekordy: path: "blog", locale: "pl" i path: "blog", locale: "en". URL ten sam (/blog), ale router wybiera stronę pasującą do domain.locale (lub ?lang=). Patrz Strategia 3a (wariant z tym samym path).
{ "page": { "code": "blog-pl", "path": "blog", "locale": "pl", "content": "<h1>Blog</h1>..." } }
{ "page": { "code": "blog-en", "path": "blog", "locale": "en", "content": "<h1>Blog</h1>..." } }
Wymaga site.multilang = true żeby router preferował match po locale (najpierw szuka strony z path=blog, locale=domain.locale, potem locale=nil). W obrębie (path, site, account) można mieć po jednym rekordzie per locale + opcjonalnie jeden bez locale.
- ✅ Pełna swoboda — różny
content,layout_id,html_*per język. Każdy rekord żyje własnym życiem. - ❌ Duplikacja struktury (layout/content) gdy strony są podobne — wszystko trzymasz dwa razy.
Sposób 3 — Master + slave z based_on_page_id (dziedziczenie)
based_on_page_id (dziedziczenie)Tworzysz mastera (path: "blog", locale: "pl") z pełnym contentem, i slave’a (path: "blog", locale: "en") który dziedziczy puste pola od mastera przez based_on_page_id. Slave nadpisuje tylko to co inne (np. fields, html_title). Patrz Strategia 3b.
{ "page": { "code": "blog-pl", "path": "blog", "locale": "pl",
"content": "<h1>{{ title }}</h1><p>{{ intro }}</p>",
"fields": { "title": "Blog firmowy", "intro": "Najnowsze wpisy..." },
"html_title": "Blog — Firma" } }
{ "page": { "code": "blog-en", "path": "blog", "locale": "en",
"based_on_page_code": "blog-pl",
"fields": { "title": "Company Blog", "intro": "Latest posts..." },
"html_title": "Blog — Company" } }
Slave ma pusty content → leci z mastera. fields mergowane per klucz. Layout, html_description, html_keywords → fallback do mastera gdy puste.
- ✅ Brak duplikacji struktury — edytujesz
content/layoutraz na masterze, slave dziedziczy. Tłumaczenia per pole. - ❌ Dwa rekordy do utrzymania, fallback niewidoczny “gołym okiem” w slavie (puste pole = wartość mastera).
Którą wybrać?
| Sytuacja | Sposób |
|---|---|
| Treść identyczna, tylko inne stringi/przyciski |
1 — jeden rekord, locale-keyed fields
|
| Treść mocno różna per język, brak wspólnej struktury | 2 — niezależne strony |
| Wspólny layout/content, ale per język inne teksty/meta | 3 — master + slave |
Pełen opis każdej strategii i dodatkowe warianty (różne path per język, multi-domain, mixed routing) — w sekcjach “Strategia 1–4” poniżej.
Wybór języka renderowania (lokale chain)
Efektywny locale strony wybierany jest wg priorytetu:
-
page.locale— własny locale strony (przypisany do niej w bazie). Gdy ustawiony, wygrywa nad wszystkim innym (strona zlocale=enzawsze renderuje się jako EN, nawet podfirma.pli z?lang=de). -
?lang=en(URL parametr) — jawne wymuszenie języka dla strony bezpage.locale. Działa tylko dla obsługiwanych kodów (pl,en,fr,cs,sk,de,es,uk); nieobsługiwany jest ignorowany. -
domain.locale— locale aktualnej domeny z requestu (np.firma.pl=pl,firma.com=en). -
site.locale— domyślny locale całego site’a.
Resolve klucza w fields
Mając wybrany locale (np. pl), klucz w fields rozwiązywany jest osobno:
-
fields[locale][klucz]— jeśli klucz istnieje w wybranym locale, wygrywa -
fields[klucz](top-level) — fallback per-klucz, gdy klucza nie ma wfields[locale]
Fallback działa per-klucz — pojedyncza wartość może być tylko w locale, inna tylko top-level. Sub-hashe dla locale, których nie ma na liście obsługiwanych, są ignorowane.
Uwaga:
?lang=nie nadpisujepage.locale. Strona zlocale=enzawsze renderuje EN.?lang=służy głównie do wyboru wariantu w obrębie strony bez ustawionegopage.locale(np. strona główna z locale-keyedfields).
Strategia 1: Per-domain locale (najprostsza, zalecana dla 2-3 języków)
Use case: ten sam content, różne domeny → różne języki. Np. site.pl po polsku, site.com po angielsku.
Konfiguracja:
- Site ma podłączone dwie domeny:
site.plisite.com - W edycji każdej domeny (
/account/domains/{id}/edit) ustawiasz pole locale:pldlasite.pl,endlasite.com - Page/Paragraph/Layout mają
fieldsz locale-keyed strukturą (patrz niżej)
Działanie:
- Wejście
site.pl/contact→domain.locale=pl→ renderuje wartości zfields.pl.*z fallbackiem do top-level - Wejście
site.com/contact→domain.locale=en→ renderuje wartości zfields.en.*
Zalety: zero duplikacji content, jeden rekord page/paragraph na język, pełen SEO per domena.
Wady: ten sam path URL pod oboma domenami (/contact jest pod .pl i pod .com). Jeśli chcesz różnych URLi per język (np. /cennik po polsku, /pricing po angielsku) — zobacz Strategia 3.
Sitemap per domena: /sitemap.xml automatycznie filtruje strony po domain.locale — pod firma.pl/sitemap.xml lecą tylko strony z locale=pl lub locale puste, pod firma.com/sitemap.xml tylko locale=en lub puste. Strony bez ustawionego locale są uniwersalne i pojawiają się w obu sitemapach. Gdy domena nie ma locale — sitemap pokazuje wszystkie strony (jak dotąd).
Strategia 2: Locale-keyed fields (per-pole tłumaczenia w jednym rekordzie)
Struktura fields z locale (działa w Page, Paragraph, Layout):
{
"en": {
"position": "CTO",
"experience": "Expert in distributed systems."
},
"pl": {
"position": "Dyrektor Techniczny",
"experience": "Ekspert w systemach rozproszonych."
},
"default_field": "wartość bez locale"
}
Działanie (przy page.locale = nil, domain.locale = "pl"):
- bez
?lang=→{{ position }}= “Dyrektor Techniczny” (zpl— chain: brakpage.locale→domain.locale=pl) -
?lang=en→{{ position }}= “CTO” (zen—?lang=bijedomain.localewcurrent_locale) -
?lang=xx→{{ position }}= “Dyrektor Techniczny” (nieobsługiwany kod ignorowany → fallback dodomain.locale=pl) -
{{ default_field }}= “wartość bez locale” (top-level zawsze widoczny gdy klucz nie istnieje w wybranym locale)
Gdy page.locale = "pl" jest ustawiony — ?lang=en nie wybierze wariantu en, bo page.locale ma priorytet (własny locale strony bije locale z requestu). Locale-keyed fields w połączeniu z jawnym page.locale ma sens głównie dla wzbogacenia tłumaczeniami stron specyficznych dla jednego języka.
Layout również wspiera locale w fields — dziedziczy kontekst (page.locale → current_locale → site.locale) z aktualnie renderowanej strony, więc {{ layout.title }} zwróci wariant językowy zgodnie z tymi samymi regułami fallback.
Uwaga: Struktura locale (pl, en, itp.) jest opcjonalna. Jeśli nie potrzebujesz wielojęzyczności, używaj zwykłych pól:
{
"position": "Developer",
"experience": "10 lat"
}
Strategia 3: Osobne strony per język (path per locale)
Dwie strony — jedna pl, druga en — z różnymi path. Każda z własnym locale ustawionym jawnie. Zalecane dla pełnej kontroli SEO i kiedy treść stron znacząco się różni między językami.
Wariant 3a: Niezależne strony
Tworzysz dwie osobne strony — bez powiązania, każda z pełnym contentem:
POST /cms/pages.json
{ "page": { "code": "about-pl", "path": "o-firmie", "locale": "pl", "content": "..." } }
POST /cms/pages.json
{ "page": { "code": "about-en", "path": "about", "locale": "en", "content": "..." } }
Zalety: pełna swoboda, niezależne content per język, łatwe w nawigacji.
Wady: duplikacja layoutu/struktury jeśli content jest podobny.
Wariant 3b: Strony z dziedziczeniem (based_on_page_id)
based_on_page_id)Master + slave — slave dziedziczy puste pola od mastera. Pola które slave ma własne (niepuste) — nadpisują wartości mastera.
// master (pl) - pełny content i fields
{ "page": { "code": "cennik-pl", "path": "cennik", "locale": "pl",
"content": "full polish content z {{ price }}",
"fields": {"title": "Cennik", "price": "100 zł"},
"html_title": "Cennik usług" } }
// slave (en) - nadpisuje tylko to co inne, reszta z mastera
{ "page": { "code": "cennik-en", "path": "pricing", "locale": "en",
"based_on_page_code": "cennik-pl",
"fields": {"title": "Pricing", "price": "$25"},
"html_title": "Service pricing" } }
Działanie: wszystkie puste pola slave’a (content, layout_id, html_title, html_description, html_keywords, fields[k]) lecą z mastera. Slave nadpisuje tylko to co ma własne.
Pola dziedziczone z fallbackiem do mastera (effective_*):
-
content(treść strony) -
layout_id(szablon) -
html_title,html_description,html_keywords(meta tagi) -
fields[*]— merge per klucz (slave nadpisuje master, brakujące klucze lecą z mastera; locale-keyed sub-hashe też są merge’owane)
Ograniczenia:
-
Tylko 1 poziom dziedziczenia — slave nie może być masterem dla innej strony. Walidacja blokuje ustawienie
based_on_page_idna stronę, która sama już jest slave’em (mabased_on_page_id≠ NULL). - Self-reference zakazane — strona nie może dziedziczyć sama z siebie.
- Same site only — master i slave muszą należeć do tego samego site’a.
-
Po
destroymastera —based_on_page_idslave’ów jest zerowane (dependent: :nullify), slave staje się standalone z własnymi (potencjalnie pustymi) polami.
Zalety: brak duplikacji, łatwa edycja “tylko tytułu i kilku stringów per język”, prosty fallback (jeden hop do mastera).
Wady: dwa rekordy do utrzymania, magia inheritance niewidoczna w surowym JSON.
UI: w /cms/pages/new i /cms/pages/{id}/edit jest selector “Dziedziczy ze strony” z dostępnymi master-stronami (z tego samego site, bez stron które same dziedziczą). Na liście /cms/pages slave’y mają pod nazwą małą ikonkę z linkiem do mastera.
Strategia 4: Mieszany routing (path per locale, jedna lub wiele domen)
path per locale, jedna lub wiele domen)Use case: chcesz wymieszać schematy URL — np. polski na własnej domenie, angielski i francuski pod tą samą .com, francuski jeszcze dodatkowo z prefiksem /fr/. Wszystko na jednym site z dziedziczeniem based_on_page_id.
Jak to działa “out of the box”
Router CMS-a matchuje dowolny ciąg w polu path strony — łącznie ze slashami. Więc strona z path: "fr/tariff" jest naturalnie dostępna pod domena.com/fr/tariff. Nie ma żadnej dedykowanej obsługi prefiksów typu /pl/ / /en/ — to po prostu wynika z tego jak nazwiesz path.
Przykład: master + 2 slaves z różnymi schematami URL
POST /cms/pages.json
{ "page": { "code": "cennik-pl", "path": "cennik", "locale": "pl",
"site_code": "strona1", "kind": "text",
"content": "<h1>{{ title }}</h1><p>{{ price }}</p>",
"fields": { "title": "Cennik", "price": "499 zł" } } }
POST /cms/pages.json
{ "page": { "code": "cennik-en", "path": "pricing", "locale": "en",
"site_code": "strona1", "based_on_page_code": "cennik-pl",
"fields": { "title": "Pricing", "price": "$129" } } }
POST /cms/pages.json
{ "page": { "code": "cennik-fr", "path": "fr/tariff", "locale": "fr",
"site_code": "strona1", "based_on_page_code": "cennik-pl",
"fields": { "title": "Tarifs", "price": "119 €" } } }
Zmienna Liquid {{ seo_alternates }} na masterze zwróci wszystkie trzy warianty (+ x-default), więc po włączeniu site.multilang w layoucie automatycznie pojawią się tagi <link rel="alternate" hreflang> dla pl, en, fr i x-default.
Konfiguracje domenowe — co działa, co ma haczyk
A) Jedna domena, path-prefix per język (firma.com/cennik, firma.com/pricing, firma.com/fr/tariff) — działa idealnie. get_url zwraca każdy URL pod tą samą domeną, hreflang i 301 są spójne. Najprostszy setup dla mieszanego routingu.
B) Wiele domen, jedna na język (firma.pl z domain.locale=pl, firma.com z domain.locale=en) — domain.locale służy jako fallback dla efektywnego locale, a 301 redirect przerzuca między wariantami w obrębie domeny. Przy site.multilang = true URL generowany dla strony automatycznie wybiera domenę pasującą do page.locale (jeśli istnieje), więc seo_alternates/canonical_url poprawnie wskazują:
-
cennik(localepl) →https://firma.pl/cennik -
pricing(localeen) →https://firma.com/pricing
Fallback: jeśli żadna domena site’a nie ma pasującego locale, wraca do site.domain \|\| site.domains.first (jak poza trybem multilang).
C) Mix A+B (firma.pl/cennik + firma.com/pricing + firma.com/fr/tariff) — działa w obrębie jednego site’a: master cennik (locale pl) trafi pod firma.pl, slave pricing (locale en) i slave fr/tariff (locale fr) trafią pod firma.com (jeśli firma.com ma domain.locale=en jako default; FR korzysta z fallbacku do tej samej domeny, bo nie ma osobnej domain.locale=fr). Wystarczy ustawić site.multilang = true i nadać domenom odpowiednie locale.
Kiedy wystarczy to co jest
- Jeden site, jedna domena, wszystko po path → ✅ działa.
- Jeden site, multi-domain z
domain.locale+site.multilang→ ✅get_urlwybiera domenę popage.locale, hreflang/canonical/301 spójne. - Pełny mix multi-domain + multi-path → ✅ działa w obrębie jednego site’a (patrz wariant C).
Przykład end-to-end: strona główna (multilang) + cennik z dziedziczeniem
Realny case: site strona1 z dwoma domenami (firma.pl po polsku, firma.com po angielsku). Mamy:
-
strona główna
/— jeden rekord page, content ten sam, ale teksty wfieldsper locale (Strategia 1+2) -
cennik —
/cennik(PL master) i/pricing(EN slave dziedziczący z mastera; różne URL, Strategia 3b)
1. Domeny
-
firma.plzlocale: "pl", podłączona do sitestrona1 -
firma.comzlocale: "en", podłączona do sitestrona1
2. Strona główna — jeden rekord, ten sam URL pod oboma domenami
POST /cms/pages.json
{
"page": {
"code": "home",
"name": "Home",
"path": "",
"kind": "text",
"site_code": "strona1",
"content": "<h1>{{ title }}</h1><p>{{ subtitle }}</p><a href=\"{{ cta_url }}\">{{ cta_label }}</a>",
"fields": {
"pl": {
"title": "Witamy w Firma",
"subtitle": "Dostarczamy najlepsze rozwiązania dla biznesu",
"cta_label": "Zobacz cennik",
"cta_url": "/cennik"
},
"en": {
"title": "Welcome to Firma",
"subtitle": "We deliver the best business solutions",
"cta_label": "See pricing",
"cta_url": "/pricing"
}
},
"html_title": "Firma — strona główna"
}
}
Działanie:
-
firma.pl/→domain.locale=pl→ “Witamy w Firma”, link CTA do/cennik -
firma.com/→domain.locale=en→ “Welcome to Firma”, link CTA do/pricing -
firma.pl/?lang=en→ wymuszenie EN mimo polskiej domeny → “Welcome to Firma”
3. Cennik — master (PL) + slave (EN) z dziedziczeniem
Master /cennik (PL): pełen content i fields.
POST /cms/pages.json
{
"page": {
"code": "cennik-pl",
"name": "Cennik",
"path": "cennik",
"locale": "pl",
"kind": "text",
"site_code": "strona1",
"content": "<h1>{{ title }}</h1><div class=\"price\">{{ price }} {{ currency }}</div><p>{{ description }}</p>",
"fields": {
"title": "Cennik usług",
"price": "499",
"currency": "zł / mies.",
"description": "Pełen pakiet usług w jednej cenie."
},
"html_title": "Cennik — Firma"
}
}
Slave /pricing (EN): wskazuje na master przez based_on_page_code, nadpisuje tylko zmienne pola.
POST /cms/pages.json
{
"page": {
"code": "cennik-en",
"name": "Pricing",
"path": "pricing",
"locale": "en",
"kind": "text",
"site_code": "strona1",
"based_on_page_code": "cennik-pl",
"fields": {
"title": "Service pricing",
"currency": "USD / month",
"description": "Full service package at a flat rate."
},
"html_title": "Pricing — Firma"
}
}
Co dzieje się w slave:
-
contentpuste → bierze z mastera (<h1>{{ title }}</h1>...) -
fields.titlewłasne → “Service pricing” -
fields.pricebrak w slave → bierze z mastera (“499”) -
fields.currencywłasne → “USD / month” -
fields.descriptionwłasne → “Full service package…” -
html_titlewłasne → “Pricing — Firma” -
layout_idpuste → layout mastera (lub site fallback)
Działanie pod domenami:
-
firma.pl/cennik→ master, localepl→ “Cennik usług, 499 zł / mies., Pełen pakiet…” -
firma.com/pricing→ slave, localeen→ “Service pricing, 499 USD / month, Full service package…”-
price= “499” leci z mastera, reszta ze slave’a
-
-
firma.com/cennik→ master pod EN domeną → wciąż content mastera, aledomain.locale=enpróbuje znaleźć wfields.en.*(brak — fallback do top-level master) → “Cennik usług” (wartości master z top-level)
Uwaga SEO: w wariancie 3b każda strona ma swój własny URL (
/cenniki/pricing), a tagi<link rel="alternate" hreflang="...">generują się automatycznie po włączeniusite.multilang(patrz “SEO multilang — zmienne Liquid” niżej).
Pola locale per model
Site
-
locale— domyślny locale dla wszystkich stron site’a. Ostatni fallback w chain (popage.localeicurrent_localez requestu).
Page
-
locale— własny locale strony (przypisany do niej w bazie). Wygrywa nad locale z requestu (?lang=,domain.locale) — jeśli strona malocale=en, zawsze renderuje się jako EN niezależnie od domeny i?lang=.
Paragraph
- nie ma własnego pola
locale— używa locale z kontekstu strony, w której jest renderowany.
Layout
- nie ma własnego pola
locale— używa locale z kontekstu strony, w której jest renderowany.
Domain (/account/domains/{id}/edit)
/account/domains/{id}/edit)-
locale— locale aktualnej domeny. Wchodzi docurrent_localew kontrolerze (po?lang=, przedsite.locale). Pozwala na strategię “per-domain locale” —site.pl=pl,site.com=en. Nie nadpisujepage.locale— strony z ustawionymlocalerenderują się we własnym języku.
Liquid — dostęp do localized fields
Wszystkie odwołania w Liquid automatycznie używają zlokalizowanych wartości:
{{ title }} — z page.fields, według locale chain
{{ page.title }} — to samo (page = drop)
{{ paragraph.position }} — z paragraph.fields, według locale chain
{{ layout.footer_text }} — z layout.fields, według locale chain
Top-level klucze w fields służą jako fallback gdy nie ma wpisu w aktualnym locale.
SEO multilang — zmienne Liquid
Włącz checkbox Wielojęzyczność (SEO) na site (site.multilang = true). Wtedy w renderowaniu stron tego site’u dostępne są dodatkowe zmienne Liquid, które używasz w layoucie żeby wyemitować poprawne tagi SEO i 301 redirecty:
| Zmienna | Typ | Opis |
|---|---|---|
{{ html_lang }} |
string | Effective locale: page.locale → site.locale → domain.locale. Do <html lang="...">. |
{{ canonical_url }} |
string | URL kanoniczny strony. Przy multilang wybiera domenę z domain.locale = page.locale (fallback: site.domain / pierwsza domena). |
{{ seo_alternates }} |
array | Lista { locale, url } wariantów językowych (master + slaves z based_on_page_id) + { locale: "x-default", url: <master> }. URL-e per locale-matching domain. |
{{ seo_head }} |
HTML | Pre-renderowany blok <link rel="canonical"> + <link rel="alternate" hreflang> — drop-in do <head>. |
301 redirect path↔locale działa automatycznie, gdy site.multilang = true: wejście na firma.com/cennik (PL master pod EN domeną) → 301 do firma.com/pricing (slave EN w grupie based_on_page).
W preview (/w/<site>/...) zmienne canonical_url, seo_alternates, seo_head nie są ustawiane (preview URL nie powinno trafić do indeksów).
Przykład — minimalny layout SEO (drop-in)
<!DOCTYPE html>
<html lang="{{ html_lang }}">
<head>
<meta charset="UTF-8">
<title>{{ html_title }}</title>
<meta name="description" content="{{ html_description }}">
{{ seo_head }}
</head>
<body>
{{ content }}
</body>
</html>
Przykład — manualne renderowanie hreflang
Gdy chcesz pełną kontrolę nad markupem (np. dodać własne atrybuty, zmienić kolejność):
<!DOCTYPE html>
<html lang="{{ html_lang }}">
<head>
<title>{{ html_title }}</title>
{% if canonical_url %}
<link rel="canonical" href="{{ canonical_url }}" />
{% endif %}
{% for alt in seo_alternates %}
<link rel="alternate" hreflang="{{ alt.locale }}" href="{{ alt.url }}" />
{% endfor %}
</head>
<body>{{ content }}</body>
</html>
Dla mastera /cennik (PL) z slave’em /pricing (EN) wygeneruje to:
<html lang="pl">
<head>
<link rel="canonical" href="https://firma.pl/cennik" />
<link rel="alternate" hreflang="pl" href="https://firma.pl/cennik" />
<link rel="alternate" hreflang="en" href="https://firma.com/pricing" />
<link rel="alternate" hreflang="x-default" href="https://firma.pl/cennik" />
</head>
Zalecenia
- 2-3 języki, ten sam URL → Strategia 1 (per-domain locale) + Strategia 2 (locale-keyed fields)
- Różne URL per język, prosta struktura → Strategia 3a (niezależne strony)
-
Różne URL, dużo wspólnego content → Strategia 3b (
based_on_page_id) - Mieszany routing (path-prefix + multi-domain) → Strategia 4 (najlepiej path-prefix pod jedną domeną, lub osobne sites na język)
Blog multilang: jeden category_code, język w locale
Tag <cms type="article"> filtruje artykuły po języku sam: gdy site ma multilang: true,
lista pokazuje tylko artykuły z locale domeny i artykuły bez locale. Język wpisujemy więc
w pole locale artykułu, a category_code (kategoria główna) zostaje ten sam dla wszystkich języków.
<cms type="article" category_code="blog" per_page="12">
<list><!-- karty --></list>
<show><!-- artykuł --></show>
</cms>
PATCH /cms/articles/123.json
{ "article": { "category_code": "blog", "locale": "en" } }
Nie dziel bloga na blog-pl, blog-en, blog-es:
- każdy kod to osobna kategoria (
Cms::Category) i osobna sekcja witryny, więc każda potrzebuje strony z tagiem<cms type="article">- bez niej system sam założy stronę/blogz tagiem „wszystkie kategorie” i tam trafią artykuły, - treść
<list>i<show>trzeba wtedy duplikować w każdym branchu{% if html_lang %}, - artykuł bez
locale(np. wspólna informacja) nie pojawi się na żadnej z takich list.
Gdy blog jest już podzielony per język: ustaw locale na artykułach, zamień wszystkie kody
na jeden (blog) i zostaw jedną stronę listingową. Przejściowo lista może połączyć stare kody:
category_code="blog-pl,blog-en,blog-es" (kilka kodów po przecinku w jednym tagu).
WAŻNE (gdy mimo wszystko używasz {% if %} wokół tagów): każdy branch MUSI mieć KOMPLETNY
<cms>...</cms> blok (open + list + show + close). NIE WOLNO dzielić <cms> między if/else —
CMS parsuje WSZYSTKIE tagi <cms> niezależnie od Liquid i dostaje 500.
Zasady:
-
<cms>tagi przetwarzane PRZED Liquid — nie można użyć{{ }}w atrybutach<cms>(np.category_code="{{ html_lang }}"NIE działa) - Blog listing (master) może mieć slave’y (np.
blog-en) — są PUSTE, dziedziczą content z mastera - Podział tematyczny wewnątrz bloga (chipy “Aktualności / Porady”) robi się dodatkowymi kategoriami z
path(category_codes: ["blog", "porady"]), nie kolejnymi kodami głównymi ani językiem w kodzie — patrz CMS API
Migracja stron na multilang (procedura)
Krok 1: Przygotuj hreflang mapę
Pobierz sitemapy ze starych domen. Stwórz mapę PL_path → {en: EN_path, es: ES_path}.
Krok 2: Stwórz PL mastery
- Zamień sufiksy
{{ field_en }}→{{ field }}(regex:\{\{(\s*)(\w+?)_(en\|es\|pl)(\s*)\}\}→{{\1\2\4}}) - Stwórz PL master z locale-keyed fields
- Path = polska ścieżka (nie angielska!)
Krok 3: Zamień EN/ES na slave’y
PATCH /cms/pages/{id}.json
{ "page": { "path": "pricing", "locale": "en", "based_on_page_code": "cennik-pl", "content": "", "fields": {} } }
Krok 4: Ustaw site
PATCH /cms/sites/{code}.json
{ "site": { "multilang": true, "locale": "pl" } }
Krok 5: Podepnij domeny z locale
Każda domena z odpowiednim locale (np. firma.pl → pl, firma.com → en).
Krok 6: Layout
<html lang="{{ html_lang \| default: 'pl' }}">-
{{ seo_head }}przed</head> - Używaj
{{ layout.nazwa }}dla layout fields (nie{{ nazwa }}— to resolves z page fields)
Częste błędy multilang
-
PL master z angielskim path — np.
pricingzamiastcennik. Slave EN nie może mieć pathpricingbo koliduje z masterem -
Sufiksy w template —
{{ hero_title_en }}nie działa, musi być{{ hero_title }}. CMS resolves z locale-keyed fields automatycznie -
{{ locale }}w layoucie — używaj{{ html_lang }}(effective locale), nie{{ locale }}(pole page) - Homepage — path=”” jest 1 per site. Homepage nie ma slave’a, tłumaczenia przez locale-keyed fields
- html_title/html_description na slave — to kolumny, nie fields. Slave zachowuje swoje meta (nie czyść ich)
-
Path slave’a z prefixem locale — domyślnie BEZ prefixu (
pricingnieen/pricing). Prefix tylko gdy path koliduje z masterem PL -
?lang=na stronie zpage.locale—page.localewygrywa, więc?lang=nic nie zmieni. Aby URL parametr działał, strona musi miećlocale=nil(np. homepage z locale-keyedfields)
Powrót: CMS API