У multilingual Laravel application дуже легко автоматично припустити, що кожна мова повинна мати власний slug.
У ICanUp я в результаті прийшла до іншої моделі: Post і Category мають один shared slug, а locale визначає URL prefix і локалізований content, але не identity сторінки.
Це рішення вплинуло не тільки на routing. Воно спростило hreflang, sitemap, IndexNow, internal links і весь contract між backend та frontend.


Поточний contract у ICanUp
У поточній схемі ICanUp поле slug належить самому Post або Category.
Validation перевіряє його uniqueness без locale dimension, а model lookup працює через один shared value.
Переклад змінює content, але не створює іншу route identity для тієї самої entity.
posts id slug UNIQUE ... categories id slug UNIQUE ... translations locale localized content localized SEO fieldsЦей contract закріплений не тільки application validation, а й unique constraints на shared slug у database.
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
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 зараз активна.
$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.
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.
У 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.



