0% прочитано

Laravel Multilingual URLs: shared slug чи translated slugs для локалізованих сторінок

У ICanUp Post і Category мають один shared slug, а локалізовані URLs відрізняються locale prefix і контентом. Розбираю, чому я відмовилася від translated slugs, як це спростило routing, hreflang, sitemap та IndexNow, і в яких проєктах language-specific slugs все ж можуть бути кращим вибором.

9 вересня 2026 р. 9 хв читанняЛаравель

У multilingual Laravel application дуже легко автоматично припустити, що кожна мова повинна мати власний slug.

У ICanUp я в результаті прийшла до іншої моделі: Post і Category мають один shared slug, а locale визначає URL prefix і локалізований content, але не identity сторінки.

Це рішення вплинуло не тільки на routing. Воно спростило hreflang, sitemap, IndexNow, internal links і весь contract між backend та frontend.

Shared slug і translated slugs - це не питання правильного або неправильного підходу. Це два різні routing contracts. Для ICanUp стабільна identity entity виявилася важливішою за language-specific slug.
До
Багатомовна маршрутизація Laravel з окремими перекладеними слугами для локалізованих URL-адрес у UK та EN
Після
Багатомовна маршрутизація Laravel з одним спільним слугом, що використовується для локалізованих URL-адрес у UK та EN

Поточний contract у ICanUp

У поточній схемі ICanUp поле slug належить самому Post або Category.

Validation перевіряє його uniqueness без locale dimension, а model lookup працює через один shared value.

Переклад змінює content, але не створює іншу route identity для тієї самої entity.

Спрощена current content model
posts  id  slug UNIQUE  ... categories  id  slug UNIQUE  ... translations  locale  localized content  localized SEO fields

Цей contract закріплений не тільки application validation, а й unique constraints на shared slug у database.

PHP - Shared slug uniqueness
Rule::unique('posts', 'slug'); Rule::unique('categories', 'slug');

Shared slug і translated slugs означають різні URL models

Model

UK URL

EN URL

Shared slug

/posts/laravel-localization

/en/posts/laravel-localization

Translated slugs

/posts/laravel-lokalizatsiia

/en/posts/laravel-localization

У shared-slug model locale змінює route namespace, але сам content identifier залишається тим самим.

У translated-slug model locale стає частиною lookup contract: щоб знайти entity, вже недостатньо знати тільки slug.

Чому я вибрала shared slug

  • Одна entity має одну стабільну route identity.
  • Slug lookup не залежить від translation storage.
  • Зміна перекладу не змінює URL іншої locale.
  • Frontend і backend використовують той самий logical route contract.
  • Sitemap, hreflang та IndexNow працюють із однією entity і генерують її locale-aware URLs.
  • Internal links легше будувати без пошуку окремого slug для кожної мови.

Це природно продовжує підхід, який я вже описувала у статті про logical route names і localized URLs у Laravel.

Я хочу, щоб application code думав про route як про logical destination, а localization layer вже вирішував, який locale prefix потрібен.

Default locale і shared slug не означають однакові URLs

Locale-aware public URLs
UK default locale:https://icanup.com.ua/posts/example-slug EN locale:https://icanup.com.ua/en/posts/example-slug

У ICanUp default locale - UK, тому її URL не має додаткового prefix.

English URL отримує /en, але slug залишається той самий.

Локалізованість URL тут визначається не перекладом slug, а routing layer.

Route lookup стає простішим

При shared slug entity можна знайти незалежно від того, яка locale зараз активна.

PHP - Lookup Post за shared slug
$post = Post::query()    ->where('slug', $slug)    ->firstOrFail();

У translated-slug model lookup зазвичай уже повинен враховувати locale або translation relation.

Це не проблема саме по собі, але locale стає частиною identity resolution, а значить проходить через routing, validation, redirects, caching та tests.

Локалізований content при цьому нікуди не зникає

Shared slug не означає, що сторінка однакова для всіх мов.

Title, short text, content blocks, SEO metadata та інші localized values можуть залишатися різними для UK і EN.

Я просто відділяю content translation від route identity.

Field

Shared

Localized

Post ID

Yes

No

Slug

Yes

No

Title

No

Yes

Content

No

Yes

Meta Title

No

Yes

Meta Description

No

Yes

Public URL prefix

No

Yes

Shared slug не створює SEO duplicates

Однаковий slug fragment у двох locale URLs не робить сторінки duplicate content автоматично.

Для search engine важливо, що це окремі URLs із локалізованим content, self-canonical і правильним hreflang relationship.

Цю частину infrastructure я окремо розбирала у статті про Laravel hreflang і canonical URLs.

Canonical не повинен зводити EN сторінку до UK лише тому, що slug спільний. Кожна locale URL має власний self-canonical, а hreflang зв’язує мовні версії.

Sitemap і IndexNow теж виграють від стабільної identity

Коли SEO layer отримує Post, йому не потрібно шукати route slug у translation table.

Він бере одну entity і генерує потрібні locale-aware URLs через routing/localization contract.

Цей самий принцип використовується у моїх реалізаціях IndexNow та locale-aware sitemap lastmod.

  • HreflangService генерує URLs для доступних locales.
  • Sitemap працює з тією самою entity identity.
  • IndexNow отримує current public URLs замість власної slug logic.
  • Зміна visible translation не вимагає іншого slug lookup contract.

Що змінилося б із translated slugs

Translated slugs дають красивіші language-specific paths, але вони переносять частину complexity у infrastructure.

Тепер для однієї entity існує кілька route identifiers, і кожен із них може змінитися незалежно.

Це означає, що slug стає localized content із routing consequences.

Concern

Shared slug

Translated slugs

Lookup

slug

locale + translated slug

Uniqueness

global slug

usually locale + slug

Slug change

one route identity

independent per locale

Redirects

one shared history

history per locale

Internal links

shared entity slug

resolve target-locale slug

SEO URL generation

route + locale

route + locale + translated slug

Redirect history стає locale-specific

Якщо EN slug змінюється, а UK ні, redirect потрібен тільки для EN URL.

Якщо потім змінюється UK slug, з’являється інша redirect history.

Для проєкту, де URLs часто редагуються, це вже окремий domain concern, який треба зберігати і тестувати.

Admin validation теж стає складнішою

Shared slug має одну uniqueness validation.

Для translated slugs потрібно визначити, чи slug має бути unique глобально, тільки всередині locale, чи ще й у межах content type.

Також потрібно вирішити, що відбувається, якщо переклад існує, але translated slug порожній.

Коли translated slugs можуть бути кращими

  • URL має бути максимально природним для кожної мови.
  • Мови використовують різні алфавіти і slug є важливою частиною UX.
  • Keyword strategy суттєво відрізняється між locales.
  • Проєкт уже має надійну locale-specific redirect history.
  • CMS редактори очікують повний контроль URL окремо для кожної мови.
  • Routing infrastructure від початку спроєктована навколо locale + slug identity.
Я б не переходила на translated slugs тільки заради того, щоб URL виглядав більш перекладеним. Це має бути свідомий architecture decision, тому що змінюється не лише display URL, а весь routing contract.

У ICanUp trade-off вийшов на користь shared slug

Для мого блогу важливі stable routing, multilingual SEO і можливість використовувати одну й ту саму content identity в різних services.

Shared slug забирає частину localization flexibility, але натомість зменшує кількість places, де application повинна знати про translated route keys.

Для ICanUp цей trade-off виявився правильним.

Я локалізую content і URL namespace, але не дублюю identity entity без реальної потреби.

Що я перевіряю в цьому contract

  • Post slug unique на рівні posts.
  • Category slug unique на рівні categories.
  • UK і EN URLs використовують той самий shared slug.
  • Default locale не отримує зайвий locale prefix.
  • EN URL отримує /en prefix.
  • Hreflang використовує current shared slug.
  • Disabled або unavailable locale не рекламується як доступний alternate.
  • Sitemap та IndexNow використовують ті самі canonical public URLs.
  • Legacy translated slugs не повертаються в routing contract.

Що я залишила собі на майбутнє

  • Спочатку визначити identity entity, а вже потім вирішувати, чи slug має бути localized.
  • Не змішувати content translation і route identity автоматично.
  • Враховувати redirect history ще до впровадження translated slugs.
  • Використовувати один URL generation contract для routing, hreflang, sitemap, RSS та IndexNow.
  • Не будувати SEO service, який сам вирішує, як виглядає localized route.
  • Тестувати unavailable locales окремо від enabled locales.

Висновок

Shared slug у multilingual Laravel application спочатку може виглядати менш локалізованим рішенням, ніж окремий slug для кожної мови.

Але після того, як routing починають використовувати frontend, hreflang, sitemap, IndexNow, RSS та internal links, стає видно справжню ціну route identity.

У ICanUp я залишила slug shared і перенесла localization у locale-aware routing та content layer.

Для мене це дало простіший contract: одна entity, одна slug identity, кілька повноцінних localized URLs.