Коли я додавала hreflang у ICanUp, UK та EN були зрозумілими: кожна locale отримує власний alternate URL.
Найбільше питань у мене викликав x-default. Він не є ще однією мовою і не означає, що search engine має завжди вибирати default locale.
У моїй реалізації x-default - це fallback pointer, який повторно використовує URL application default locale.

Який hreflang contract уже був у ICanUp
До x-default у мене вже був основний routing contract: одна entity, shared slug і locale-aware public URLs.
Для default UK locale URL не має prefix, а EN отримує /en.
Цей підхід я детальніше розбирала у статті про shared slug vs translated slugs у multilingual Laravel.
UK:https://icanup.com.ua/posts/example-slug EN:https://icanup.com.ua/en/posts/example-slugДля hreflang я не створюю окрему URL logic.
SEO layer використовує той самий routing contract, що і public application.
Що x-default означає в моїй реалізації
Я не трактую x-default як locale.
У configuration немає locale з назвою x-default, для нього немає translation і окремого content.
Це додатковий HreflangUrl, який додається після language-specific entries.
ukвказує на UK localized URL.enвказує на EN localized URL.x-defaultповторно використовує URL default locale.
Чому fallback у мене - саме default locale
У ICanUp default locale - UK.
Це вже application-level setting, а не рішення SEO service.
Тому я не hardcode UK усередині hreflang logic. Service читає default_locale з тих самих localization settings, які використовує решта application.
hreflang="uk"https://icanup.com.ua/posts/example-slug hreflang="en"https://icanup.com.ua/en/posts/example-slug hreflang="x-default"https://icanup.com.ua/posts/example-slugCanonical і x-default вирішують різні задачі
Canonical і hreflang легко змішати, але вони відповідають на різні питання.
Canonical визначає canonical URL поточної localized page. Hreflang описує relationship між locale versions.
x-default належить до hreflang contract і не замінює canonical.
| Signal | Що означає |
|---|---|
| canonical | Canonical URL поточної locale page |
| hreflang="uk" | UK alternate |
| hreflang="en" | EN alternate |
| hreflang="x-default" | Fallback URL |
EN сторінка не canonicalize на default locale
Те, що x-default вказує на UK URL, не означає, що EN page повинна мати UK canonical.
English page залишається повноцінною localized page і має власний self-canonical.
x-default не згортає мовні сторінки в одну.
<link rel="canonical" href="https://icanup.com.ua/en/posts/example-slug"> <link rel="alternate" hreflang="uk" href="https://icanup.com.ua/posts/example-slug"> <link rel="alternate" hreflang="en" href="https://icanup.com.ua/en/posts/example-slug"> <link rel="alternate" hreflang="x-default" href="https://icanup.com.ua/posts/example-slug">Цю різницю я окремо розбирала у статті про Laravel hreflang і canonical URLs.
x-default може збігатися з URL іншого hreflang entry, але canonical кожної localized page залишається її власним.
Як x-default реально будується в HreflangService
Production implementation виявилася простішою, ніж окрема x-default branch.
Спочатку build() проходить по available_locales і додає звичайні HreflangUrl.
Після цього той самий resolver ще раз викликається для default_locale.
$defaultUrl = $urlResolver( $this->localeSettings->default_locale,); if (filled($defaultUrl)) { $urls[] = new HreflangUrl( locale: 'x-default', url: $defaultUrl, );}Один resolver використовується двічі
Це невелика деталь, але саме вона мені подобається в цьому implementation.
Service не має окремої функції для x-default URL. Він використовує той самий resolver, що вже генерує URL звичайної default locale.
foreach ($this->localeSettings->available_locales as $locale) { $url = $urlResolver($locale); if (blank($url)) { continue; } $urls[] = new HreflangUrl( locale: $locale, url: $url, );}URL теж проходить через routing layer
Я не додаю /en або інший prefix вручну.
localizedRoute() бере route parameter name з AVN localization configuration і передає locale у Laravel route generation.
Routing залишається source of truth і для navigation, і для hreflang.
$localeParameter = (string) config( 'avn-localization.route_parameter', 'locale',); return route($routeName, [ $localeParameter => $locale, $routeParameter => $slug,]);Це продовжує architecture, яку я описувала у статті про logical route names with localized URLs.
Available locale і available content - не завжди одне й те саме
Тут production code має важливий нюанс.
Для Post, Category і PostSeries resolver повертає URL для кожної available_locale, якщо shared slug існує.
А для Page я додала додаткову перевірку title translation і повертаю null, якщо localized page фактично недоступна.
return $this->build( function (string $locale) use ($page): ?string { if (blank( $page->translationForLocale( 'title', $locale, ) )) { return null; } return $this->localizedRoute( routeName: 'pages.show', routeParameter: 'page', slug: $page->slug, locale: $locale, ); },);x-default успадковує поведінку resolver
Оскільки x-default викликає той самий $urlResolver, він автоматично отримує правила конкретного content type.
Для Page default URL буде доданий тільки якщо resolver повернув valid URL. Для Post fallback формується через shared slug route.
Це дозволяє build() не знати domain-specific details конкретної entity.
Що я перевіряю тестами
- Кожна
available_localeотримує hreflang entry, якщо її resolver повертає URL. - Default locale URL додається ще раз як
x-default. - Default UK route не отримує зайвий locale prefix.
- EN route отримує
/en. - Зміна active request locale не повинна змінювати application default locale.
- Page без title translation для locale не генерує цей alternate.
- Shared slug використовується в Post і Category locale URLs.
- x-default проходить через той самий resolver, що і default locale.
Я перевіряю raw HTML
SEO tags мають бути доступні crawler без client-side reconstruction, тому фінальну перевірку я роблю в server-rendered HTML.
Для Inertia SSR це надійніша перевірка, ніж просто побачити теги після hydration у browser DOM.
curl -s \ https://icanup.com.ua/en/posts/example-slug \ | grep -E 'canonical|hreflang'Що я б не робила
- Не створювала б pseudo-locale з назвою
x-default. - Не canonicalize всі translations на default locale.
- Не hardcode UK окремо всередині SEO service.
- Не будувала б locale URL простим string concatenation.
- Не створювала б окремий URL generator тільки для x-default.
- Не приписувала б однакові availability rules усім content types без перевірки їх реального resolver.
Для мене x-default - це не третя мовна сторінка. Це повторне використання application fallback URL усередині hreflang contract.
Що я залишила собі на майбутнє
- Спочатку визначити application default locale, а вже потім SEO fallback.
- Не змішувати x-default із canonical.
- Генерувати x-default через той самий resolver, що й default locale URL.
- Не hardcode default locale в SEO infrastructure.
- Тримати routing source of truth для public URLs.
- Перевіряти availability behavior окремо для кожного content type.
- Перевіряти server-rendered HTML, а не тільки browser DOM.
Висновок
У ICanUp x-default виявився маленькою частиною output, але важливою частиною multilingual routing contract.
Я не створювала для нього окремий content або окрему URL strategy. HreflangService просто повторно викликає current resolver для default_locale.
UK та EN при цьому залишаються окремими localized alternates з власними canonical URLs.
У результаті x-default не додає ще одну систему URL generation - він повторно використовує fallback, який application уже визначила.



