When I added hreflang to ICanUp, UK and EN were straightforward: every locale gets its own alternate URL.
The part that raised more questions was x-default. It is not another language, and it does not mean a search engine should always choose the default locale.
In my implementation, x-default is a fallback pointer that reuses the application default locale URL.

The Hreflang Contract I Already Had in ICanUp
Before x-default, I already had the core routing contract: one entity, a shared slug, and locale-aware public URLs.
The default UK locale has no prefix, while EN uses /en.
I covered this model in more detail in my article about shared slugs vs translated slugs in multilingual Laravel.
UK:https://icanup.com.ua/posts/example-slug EN:https://icanup.com.ua/en/posts/example-slugI do not create separate URL logic for hreflang.
The SEO layer consumes the same routing contract as the public application.
What x-default Means in My Implementation
I do not treat x-default as a locale.
There is no locale named x-default in configuration, and it has no translation or separate content.
It is an additional HreflangUrl appended after the language-specific entries.
ukpoints to the UK localized URL.enpoints to the EN localized URL.x-defaultreuses the default locale URL.
Why My Fallback Is the Default Locale
In ICanUp, UK is the default locale.
That is already an application-level setting, not an SEO-specific decision.
I therefore do not hardcode UK inside hreflang logic. The service reads default_locale from the same localization settings used by the rest of the 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 and x-default Solve Different Problems
Canonical and hreflang are easy to mix together, but they answer different questions.
Canonical identifies the canonical URL of the current localized page. Hreflang describes relationships between locale versions.
x-default belongs to the hreflang contract. It does not replace canonical.
| Signal | Meaning |
|---|---|
| canonical | Canonical URL of the current locale page |
| hreflang="uk" | UK alternate |
| hreflang="en" | EN alternate |
| hreflang="x-default" | Fallback URL |
The EN Page Does Not Canonicalize to the Default Locale
The fact that x-default points to the UK URL does not mean the English page should use a UK canonical.
The English page remains a full localized page with its own self-canonical.
x-default does not collapse language versions into one page.
<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">I covered this distinction separately in my article about Laravel hreflang and canonical URLs.
x-default can point to the same URL as another hreflang entry while each localized page keeps its own canonical.
How x-default Is Actually Built in HreflangService
The production implementation is simpler than a separate x-default branch.
First, build() loops over available_locales and creates the ordinary HreflangUrl entries.
Then the same resolver runs once more for default_locale.
$defaultUrl = $urlResolver( $this->localeSettings->default_locale,); if (filled($defaultUrl)) { $urls[] = new HreflangUrl( locale: 'x-default', url: $defaultUrl, );}The Same Resolver Runs Twice
This small detail is one of the parts I like most about the implementation.
The service has no separate x-default URL function. It uses the same resolver that already generates the regular default locale URL.
foreach ($this->localeSettings->available_locales as $locale) { $url = $urlResolver($locale); if (blank($url)) { continue; } $urls[] = new HreflangUrl( locale: $locale, url: $url, );}The URL Still Goes Through the Routing Layer
I do not manually add /en or another locale prefix.
localizedRoute() reads the locale parameter name from the AVN localization configuration and passes it into Laravel route generation.
Routing remains the source of truth for both public navigation and hreflang.
$localeParameter = (string) config( 'avn-localization.route_parameter', 'locale',); return route($routeName, [ $localeParameter => $locale, $routeParameter => $slug,]);This continues the architecture I described in my article about logical route names with localized URLs.
Available Locale and Available Content Are Not Always the Same Thing
The production code has an important nuance here.
For Posts, Categories, and PostSeries, the resolver returns a URL for every available_locale when the shared slug exists.
For Pages, I added an explicit title translation check and return null when that localized page is not available.
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 Inherits Resolver Behavior
Because x-default calls the same $urlResolver, it automatically inherits the rules of the current content type.
For a Page, the default URL is added only if the resolver returns a valid URL. For a Post, the fallback is generated through the shared-slug route.
This keeps build() independent of domain-specific details for each entity.
What I Verify in Tests
- Each
available_localereceives an hreflang entry when its resolver returns a URL. - The default locale URL is added again as
x-default. - The default UK route has no unnecessary locale prefix.
- The EN route receives
/en. - Changing the active request locale does not redefine the application default locale.
- A Page without a title translation does not generate that locale alternate.
- The shared slug is used in Post and Category locale URLs.
- x-default uses the same resolver as the default locale.
I Verify Raw HTML
SEO tags need to be available to crawlers without client-side reconstruction, so I verify the server-rendered HTML.
For Inertia SSR, this is a stronger check than simply seeing tags after hydration in the browser DOM.
curl -s \ https://icanup.com.ua/en/posts/example-slug \ | grep -E 'canonical|hreflang'What I Would Avoid
- Do not create a pseudo-locale named
x-default. - Do not canonicalize every translation to the default locale.
- Do not hardcode UK separately inside the SEO service.
- Do not build localized URLs through simple string concatenation.
- Do not create a separate URL generator only for x-default.
- Do not assume every content type has the same availability rules without checking its real resolver.
For me, x-default is not a third language page. It is a reuse of the application fallback URL inside the hreflang contract.
What I Kept for Next Time
- Define the application default locale before defining the SEO fallback.
- Do not mix x-default with canonical.
- Generate x-default through the same resolver as the default locale URL.
- Do not hardcode the default locale inside SEO infrastructure.
- Keep routing as the source of truth for public URLs.
- Verify availability behavior separately for each content type.
- Verify server-rendered HTML instead of only browser DOM.
Conclusion
In ICanUp, x-default turned out to be a small part of the output but an important part of the multilingual routing contract.
I did not create separate content or a separate URL strategy for it. HreflangService simply calls the current resolver again for default_locale.
UK and EN remain separate localized alternates with their own canonical URLs.
As a result, x-default does not add another URL generation system - it reuses the fallback the application already defines.



