Przejdź do treści
Intum Pomoc

Wielojęzyczność witryny

Aktualizacja: 18 min czytania
Na tej stronie

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)

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)

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/layout raz 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:

  1. page.locale — własny locale strony (przypisany do niej w bazie). Gdy ustawiony, wygrywa nad wszystkim innym (strona z locale=en zawsze renderuje się jako EN, nawet pod firma.pl i z ?lang=de).
  2. ?lang=en (URL parametr) — jawne wymuszenie języka dla strony bez page.locale. Działa tylko dla obsługiwanych kodów (pl, en, fr, cs, sk, de, es, uk); nieobsługiwany jest ignorowany.
  3. domain.locale — locale aktualnej domeny z requestu (np. firma.pl=pl, firma.com=en).
  4. 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:

  1. fields[locale][klucz] — jeśli klucz istnieje w wybranym locale, wygrywa
  2. fields[klucz] (top-level) — fallback per-klucz, gdy klucza nie ma w fields[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 nadpisuje page.locale. Strona z locale=en zawsze renderuje EN. ?lang= służy głównie do wyboru wariantu w obrębie strony bez ustawionego page.locale (np. strona główna z locale-keyed fields).

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:

  1. Site ma podłączone dwie domeny: site.pl i site.com
  2. W edycji każdej domeny (/account/domains/{id}/edit) ustawiasz pole locale: pl dla site.pl, en dla site.com
  3. Page/Paragraph/Layout mają fields z locale-keyed strukturą (patrz niżej)

Działanie:

  • Wejście site.pl/contact → domain.locale=pl → renderuje wartości z fields.pl.* z fallbackiem do top-level
  • Wejście site.com/contact → domain.locale=en → renderuje wartości z fields.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” (z pl — chain: brak page.locale → domain.locale=pl)
  • ?lang=en → {{ position }} = “CTO” (z en — ?lang= bije domain.locale w current_locale)
  • ?lang=xx → {{ position }} = “Dyrektor Techniczny” (nieobsługiwany kod ignorowany → fallback do domain.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)

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_id na stronę, która sama już jest slave’em (ma based_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 destroy mastera — based_on_page_id slave’ó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)

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 (locale pl) → https://firma.pl/cennik
  • pricing (locale en) → 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_url wybiera domenę po page.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 w fields per locale (Strategia 1+2)
  • cennik — /cennik (PL master) i /pricing (EN slave dziedziczący z mastera; różne URL, Strategia 3b)

1. Domeny

  • firma.pl z locale: "pl", podłączona do site strona1
  • firma.com z locale: "en", podłączona do site strona1

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:

  • content puste → bierze z mastera (<h1>{{ title }}</h1>...)
  • fields.title własne → “Service pricing”
  • fields.price brak w slave → bierze z mastera (“499”)
  • fields.currency własne → “USD / month”
  • fields.description własne → “Full service package…”
  • html_title własne → “Pricing — Firma”
  • layout_id puste → layout mastera (lub site fallback)

Działanie pod domenami:

  • firma.pl/cennik → master, locale pl → “Cennik usług, 499 zł / mies., Pełen pakiet…”
  • firma.com/pricing → slave, locale en → “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, ale domain.locale=en próbuje znaleźć w fields.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 (/cennik i /pricing), a tagi <link rel="alternate" hreflang="..."> generują się automatycznie po włączeniu site.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 (po page.locale i current_locale z requestu).

Page

  • locale — własny locale strony (przypisany do niej w bazie). Wygrywa nad locale z requestu (?lang=, domain.locale) — jeśli strona ma locale=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)

  • locale — locale aktualnej domeny. Wchodzi do current_locale w kontrolerze (po ?lang=, przed site.locale). Pozwala na strategię “per-domain locale” — site.pl=pl, site.com=en. Nie nadpisuje page.locale — strony z ustawionym locale renderują 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

  1. 2-3 języki, ten sam URL → Strategia 1 (per-domain locale) + Strategia 2 (locale-keyed fields)
  2. Różne URL per język, prosta struktura → Strategia 3a (niezależne strony)
  3. Różne URL, dużo wspólnego content → Strategia 3b (based_on_page_id)
  4. 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ę /blog z 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

  1. PL master z angielskim path — np. pricing zamiast cennik. Slave EN nie może mieć path pricing bo koliduje z masterem
  2. Sufiksy w template — {{ hero_title_en }} nie działa, musi być {{ hero_title }}. CMS resolves z locale-keyed fields automatycznie
  3. {{ locale }} w layoucie — używaj {{ html_lang }} (effective locale), nie {{ locale }} (pole page)
  4. Homepage — path=”” jest 1 per site. Homepage nie ma slave’a, tłumaczenia przez locale-keyed fields
  5. html_title/html_description na slave — to kolumny, nie fields. Slave zachowuje swoje meta (nie czyść ich)
  6. Path slave’a z prefixem locale — domyślnie BEZ prefixu (pricing nie en/pricing). Prefix tylko gdy path koliduje z masterem PL
  7. ?lang= na stronie z page.locale — page.locale wygrywa, więc ?lang= nic nie zmieni. Aby URL parametr działał, strona musi mieć locale=nil (np. homepage z locale-keyed fields)

Powrót: CMS API

Czy ten wpis był pomocny?

Komentarze