0% прочитано

Laravel hreflang and Canonical URLs: SEO for Multilingual Pages Without Duplicates

In a multilingual Laravel project, having separate UK and EN URLs is not enough. I explain how I configured canonical and hreflang in ICanUp, connected language versions, added x-default, and verified that search engines receive the correct relationship between localized pages.

4 вересня 2026 р. 7 хв читанняLaravel

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.

In my implementation, canonical does not collapse the Ukrainian and English pages into one URL. Each locale has its own self-canonical URL, while hreflang connects those URLs.

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.

One Post - two locale URLs
Postslug: laravel-indexnow ukhttps://example.com/posts/laravel-indexnow enhttps://example.com/en/posts/laravel-indexnow

At 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

До
text
Wrong uk pagecanonical → /posts/laravel-indexnow en pagecanonical → /posts/laravel-indexnow
Після
text
Correct for my model uk pagecanonical → /posts/laravel-indexnow en pagecanonical → /en/posts/laravel-indexnow
When localized pages are separate valid indexable pages, canonical should not be used as a way to “remove duplicates” between languages. Hreflang exists to describe translation relationships.

Each 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.

HTML - Canonical for the default locale
<link    rel="canonical"    href="https://example.com/posts/laravel-indexnow">
HTML - Canonical for the English locale
<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.

HTML - Hreflang for uk and en
<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.

Shared slug contract
sluglaravel-indexnow uk route/posts/laravel-indexnow en route/en/posts/laravel-indexnow
New hreflang URLs in ICanUp are not generated from legacy translated slugs. For Posts and Categories, the source of truth is the shared canonical slug.

I 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.

Responsibility boundary
Post / Page / Categorylocalized route generationcanonical public URLsSEO layercanonical + hreflang
If routing already has a canonical URL generator, hreflang should use it. Manually concatenating locale prefixes in SEO code almost always creates a second source of truth.

I 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.

html
<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.

До
text
Wrong Page has only uk public content hreflang uk → validhreflang en → generated anyway → 404
Після
text
Page has only uk public content hreflang uk → validhreflang en → not exposed
Hreflang should not promise a language version that the public routing layer cannot actually serve.

One 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.

SEO data flow
Public entitypublic visibility + localecanonical localized URLsshared SEO metadatacanonicalhreflang ukhreflang enx-default

Then 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>.

BASH - Check the server-rendered head
curl -s https://example.com/posts/laravel-indexnow \  | sed -n '/<head>/,/<\/head>/p'
HTML - Expected SEO links
<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">
After the SSR verification, canonical and hreflang are visible without executing JavaScript. For me, that makes them part of the server SEO contract rather than frontend-only state.

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.

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.