Once ICanUp had localized public routes, having one correct URL for a page was no longer enough.
A Post has one shared slug but is available as separate Ukrainian and English pages. I needed to tell a search engine two things at the same time: which URL is canonical for the current language version and which other URLs are translations of that page.
That is where canonical, hreflang, x-default, and Laravel localization routing met in my SEO infrastructure.
This article continues my Laravel SEO Infrastructure series. I previously covered automatic URL submission with IndexNow and locale-aware sitemap lastmod.
The Problem Started With Two Correct URLs for One Entity
After moving to a shared slug, the Post URL structure in ICanUp became very simple.
Postslug: laravel-indexnow ukhttps://example.com/posts/laravel-indexnow enhttps://example.com/en/posts/laravel-indexnowAt the domain level, this is the same Post with the same canonical slug.
To a search engine, however, these are two public URLs with different localized content.
That meant I could not simply choose the Ukrainian URL as canonical for both pages. Doing so would undermine the model of separate language versions.
The First Important Rule: Do Not Point Every Locale to the Default Canonical
Wrong uk pagecanonical → /posts/laravel-indexnow en pagecanonical → /posts/laravel-indexnowCorrect for my model uk pagecanonical → /posts/laravel-indexnow en pagecanonical → /en/posts/laravel-indexnowEach Language Version Gets Its Own Self-Canonical
The Ukrainian page gets a canonical URL generated from the Ukrainian public route.
The English page gets its English URL.
Ukrainian is the default locale in ICanUp, so its URL has no locale prefix. English uses /en.
That rule belongs to the localization infrastructure. The SEO layer simply consumes already-correct public URLs.
<link rel="canonical" href="https://example.com/posts/laravel-indexnow"><link rel="canonical" href="https://example.com/en/posts/laravel-indexnow">I use the same locale-aware public URL wherever the canonical address of the current page is required, including metadata such as `og:url`.
Hreflang Describes the Relationship Between Language Versions
Canonical answers the question: “What is the primary URL for this specific page?”
Hreflang answers a different question: “Which other public URLs are language versions of this content?”
That is why both locale pages expose the same alternate set while keeping their own canonical URL.
<link rel="alternate" hreflang="uk" href="https://example.com/posts/laravel-indexnow"> <link rel="alternate" hreflang="en" href="https://example.com/en/posts/laravel-indexnow">Canonical identifies the current page. Hreflang identifies its language siblings.
A Shared Slug Made This Contract Much Simpler
The project still had a legacy idea of translated slugs.
During the my avn/laravel-localization package integration, I moved Posts and Categories to one product-owned slug shared by all locales.
Now the language URLs differ by route prefix rather than slug value. That removed another source of complexity from canonical, hreflang, sitemap, RSS, and the language switcher.
sluglaravel-indexnow uk route/posts/laravel-indexnow en route/en/posts/laravel-indexnowI Did Not Build the URLs by Hand
I also wanted to avoid SEO code that says something like “if locale is en, prepend /en”.
The localization layer already knows the default-locale prefix mode, enabled locales, and localized route variants.
Hreflang generation therefore uses the existing locale-aware routing infrastructure. The SEO layer should not maintain a second copy of URL localization rules.
Post / Page / Category ↓localized route generation ↓canonical public URLs ↓SEO layer ↓canonical + hreflangI Point x-default to the Default Locale
The default locale in ICanUp is uk.
For that reason, x-default points to the canonical URL of the default locale. It does not create a third page and has no dedicated route.
It is another alternate signal for the already-existing default URL.
<link rel="alternate" hreflang="x-default" href="https://example.com/posts/laravel-indexnow">There is an interesting difference here: in hreflang metadata, the default URL can appear under both `uk` and `x-default` because those links carry different semantics. In my IndexNow batch, however, x-default is not submitted as another URL because that flow needs a unique list of real targets.
Do Not Advertise a Language Version That Is Not Publicly Available
Another important edge case appeared with Pages.
Having a locale enabled in the application does not automatically mean that a particular Page has a public version in that language.
For content where a translation can be missing, hreflang should describe the localized pages that actually exist instead of generating an alternate URL that ends in a 404 or an unintended fallback.
Wrong Page has only uk public content hreflang uk → validhreflang en → generated anyway → 404Page has only uk public content hreflang uk → validhreflang en → not exposedOne SEO Contract Works Across Different Public Content Types
After that, I did not want a separate hreflang implementation for every new public module.
Posts, Categories, Pages, and later Post Series should all reuse the same SEO infrastructure together with their existing public URL rules.
The only meaningful difference is which localized variants are genuinely available for each entity.
Content | URL rule | hreflang rule |
|---|---|---|
Post | shared slug + locale route | available locale variants |
Category | shared slug + locale route | available locale variants |
Page | canonical Page slug + locale route | only real public translations |
Post Series | shared public slug + locale route | available locale variants |
Canonical and Hreflang Stay in the Shared SEO Layer
Another deliberate decision was keeping this logic out of individual Vue pages.
The public backend prepares the SEO contract, the existing SEO layer resolves metadata, and the frontend renders those values.
That prevents Page, Post, or Series from growing independent canonical and hreflang implementations.
Public entity ↓public visibility + locale ↓canonical localized URLs ↓shared SEO metadata ↓canonicalhreflang ukhreflang enx-defaultThen I Had to Verify That the Tags Were Really in the HTML
SEO metadata in ICanUp is server-rendered through Inertia SSR.
For search-engine behavior, seeing the correct canonical in DevTools after JavaScript runs was not enough for me. I wanted canonical and alternate links to exist in the initial server-rendered <head>.
curl -s https://example.com/posts/laravel-indexnow \ | sed -n '/<head>/,/<\/head>/p'<link rel="canonical" href="https://example.com/posts/laravel-indexnow"> <link rel="alternate" hreflang="uk" href="https://example.com/posts/laravel-indexnow"> <link rel="alternate" hreflang="en" href="https://example.com/en/posts/laravel-indexnow"> <link rel="alternate" hreflang="x-default" href="https://example.com/posts/laravel-indexnow">My Automated Tests Lock Down the URL Behavior
An alternate URL exists for every supported and available locale.
The default-locale URL does not get an unnecessary prefix.
English uses the /en route variant.
Posts and Categories use the shared canonical slug.
Legacy translated slugs are not used for new hreflang URLs.
x-default points to the canonical URL of the configured default locale.
Disabled locales are not exposed.
Missing translations do not create broken alternate URLs.
Canonical matches the active locale.
Existing SEO metadata does not regress.
Mistakes I Now Avoid
Do not point every language version to the default-locale canonical.
Do not generate hreflang URLs from an obsolete slug architecture.
Do not concatenate locale prefixes manually in SEO code.
Do not expose disabled locales.
Do not generate an alternate for a translation that does not exist.
Do not forget x-default.
Do not verify SEO metadata only after client-side rendering.
Do not build a separate hreflang implementation for every new content module.
Post Series Did Not Need Another SEO System
This became a useful architecture test.
When I added public Post Series, I could reuse the same contract: localized canonical URLs, hreflang for uk and en, x-default, Open Graph metadata, and sitemap integration.
The SEO infrastructure gained another public content type without introducing a parallel Series-specific implementation.
What I Kept for Next Time
Canonical and hreflang solve different problems.
Each valid localized page can have its own self-canonical.
Hreflang should use the same URLs as public routing.
Default-locale prefix rules should not be duplicated inside the SEO layer.
x-default may point to the same URL as the default-locale alternate.
A shared slug makes the multilingual SEO contract much simpler.
Do not invent unavailable locale variants.
SEO metadata should be present in the server-rendered HTML.
Related Articles
Conclusion
When I started working on multilingual SEO, canonical and hreflang looked like two small tags in <head>.
In a real Laravel application, the difficult part was not writing those tags. It was making sure they did not introduce a second URL reality.
In ICanUp, routing remains the source of truth: the shared slug belongs to the content entity, the localization layer produces the correct uk or en public URL, and the SEO layer consumes those URLs for self-canonical, hreflang, and x-default.
As a result, the language switcher, canonical tags, hreflang, sitemap, IndexNow, and new public content types all describe the same site structure.
For multilingual SEO, I do not want to build separate SEO URLs. I want SEO to describe the public URLs that are already true for the application.



