DEV Community

SeanX
SeanX

Posted on

How I Fixed the Canonical and Hreflang Mess on My Multilingual Website

A multilingual website with language switcher showing different flags

I run Animate Old Photos, an AI-powered tool that brings old photographs to life. The site supports multiple languages — English, Spanish, Portuguese, French, and more. Sounds great in theory. In practice, I had a silent SEO problem that took me a while to notice: my canonical and hreflang tags were misconfigured across pages that didn't have full language coverage.

If your website has multilingual support but not every page is translated into every language, this article is for you. I'll walk through the exact scenarios I encountered, what I did wrong, and how I fixed it — with concrete code examples.

The Problem: Not Every Page Has Every Language

Here's the reality of running a multilingual site: your product pages might be fully translated, but your blog posts and changelog entries often aren't. On Animate Old Photos, I had three types of pages:

  1. Fully translated pages — available in all supported languages (e.g., homepage, pricing)
  2. Partially translated pages — available in some languages but not all (e.g., a blog post in English, Spanish, and Portuguese, but not French)
  3. English-only pages — only the English version exists (e.g., some newer blog posts)

The question is: what happens when a French user visits a blog post that doesn't have a French version? And more importantly, how should you configure canonical and hreflang for each of these scenarios?

What I Was Doing Wrong

Before fixing things, my site had two main issues.

Issue 1: The blog listing page showed untranslated content.

On the French blog page (/fr/blog), the page title, navigation, and category headings were all in French, but the article cards themselves — titles, descriptions, and even the "Read more" buttons — were in English. It looked broken.

Screenshot of /fr/blog page before the fix showing a mix of French headers and untranslated English blog post cards

Issue 2: Fallback pages had wrong canonical and hreflang tags.

Pages like /fr/blog/how-to-restore-old-photos-online-free-ai were serving English content but declaring themselves as independent French pages with canonical pointing to themselves and hreflang listing French as an available language. This confused search engines — they saw duplicate English content across multiple URLs and couldn't determine which was the authoritative version.

HTML source code showing incorrect self-referencing canonical and hreflang tags on a fallback language page

The Fix: Three Scenarios, Three Configurations

Let me break down exactly how I configured canonical and hreflang for each scenario.

Scenario 1: Page Is Fully Translated

A page like the pricing page exists in English, Spanish, Portuguese, and French. Every version has real translated content.

<!-- /en/pricing -->
<link rel="canonical" href="https://animateoldphotos.org/en/pricing" />
<link rel="alternate" hreflang="en" href="https://animateoldphotos.org/en/pricing" />
<link rel="alternate" hreflang="es" href="https://animateoldphotos.org/es/pricing" />
<link rel="alternate" hreflang="pt" href="https://animateoldphotos.org/pt/pricing" />
<link rel="alternate" hreflang="fr" href="https://animateoldphotos.org/fr/pricing" />
<link rel="alternate" hreflang="x-default" href="https://animateoldphotos.org/en/pricing" />
Enter fullscreen mode Exit fullscreen mode
<!-- /fr/pricing -->
<link rel="canonical" href="https://animateoldphotos.org/fr/pricing" />
<link rel="alternate" hreflang="en" href="https://animateoldphotos.org/en/pricing" />
<link rel="alternate" hreflang="es" href="https://animateoldphotos.org/es/pricing" />
<link rel="alternate" hreflang="pt" href="https://animateoldphotos.org/pt/pricing" />
<link rel="alternate" hreflang="fr" href="https://animateoldphotos.org/fr/pricing" />
<link rel="alternate" hreflang="x-default" href="https://animateoldphotos.org/en/pricing" />
Enter fullscreen mode Exit fullscreen mode

The rule: Each translated page's canonical points to itself. All translated versions cross-reference each other via hreflang. The x-default points to English as the fallback.

Scenario 2: Page Is Partially Translated

A blog post exists in English, Spanish, and Portuguese — but not French. This was the trickiest scenario for me.

The translated pages (EN, ES, PT) reference each other, but do NOT include French:

<!-- /en/blog/animate-old-photos-guide -->
<link rel="canonical" href="https://animateoldphotos.org/en/blog/animate-old-photos-guide" />
<link rel="alternate" hreflang="en" href="https://animateoldphotos.org/en/blog/animate-old-photos-guide" />
<link rel="alternate" hreflang="es" href="https://animateoldphotos.org/es/blog/animate-old-photos-guide" />
<link rel="alternate" hreflang="pt" href="https://animateoldphotos.org/pt/blog/animate-old-photos-guide" />
<link rel="alternate" hreflang="x-default" href="https://animateoldphotos.org/en/blog/animate-old-photos-guide" />
<!-- No hreflang="fr" — because French version doesn't exist -->
Enter fullscreen mode Exit fullscreen mode

Spanish and Portuguese pages follow the same pattern — canonical to self, hreflang only among EN/ES/PT.

The French fallback page gets special treatment:

<!-- /fr/blog/animate-old-photos-guide (displays English fallback content) -->
<link rel="canonical" href="https://animateoldphotos.org/en/blog/animate-old-photos-guide" />
<!-- No hreflang tags at all -->
Enter fullscreen mode Exit fullscreen mode

The canonical points to the English original, not to itself. No hreflang tags. This tells search engines: "This is not an independent page. The real content lives at the English URL."

Scenario 3: Page Is English-Only

Some of my newer blog posts only exist in English. No translations at all.

<!-- /en/blog/chrome-extension-tutorial -->
<link rel="canonical" href="https://animateoldphotos.org/en/blog/chrome-extension-tutorial" />
<!-- No hreflang needed — only one language version exists -->
Enter fullscreen mode Exit fullscreen mode

That's it. No hreflang at all. It only serves a purpose when there are multiple language versions to cross-reference.

If the French URL (/fr/blog/chrome-extension-tutorial) is still accessible and shows English fallback content:

<!-- /fr/blog/chrome-extension-tutorial (fallback) -->
<link rel="canonical" href="https://animateoldphotos.org/en/blog/chrome-extension-tutorial" />
<!-- No hreflang -->
Enter fullscreen mode Exit fullscreen mode

Same pattern as Scenario 2 — canonical to English, no hreflang.

Beyond Meta Tags: What I Changed in the UI

Getting the meta tags right was only half the battle. I also made changes to how untranslated content appears to users.

Blog Listing Pages: Hide Untranslated Posts

On /fr/blog, I stopped showing articles that don't have a French translation. Previously, the French blog page was a mess — French headings mixed with English article cards. Now, it only shows articles that actually have French content.

Clean French blog listing page on Animate Old Photos showing only fully translated French articles

At the bottom of the page, I added a link for users who want more: "Voir plus d'articles en anglais" (View more articles in English), linking to the English blog.

Language Switcher: Only Show Available Languages

On any given page, the language switcher now only displays languages for which a real translation exists. If a blog post is only in English and Spanish, the switcher shows just those two — no French or Portuguese option.

Optimized language switcher dropdown showing only languages where a translation is available for the current page

Fallback Pages: Show a Notice Banner

For product and tool pages (not blog posts), if a user lands on a URL like /fr/pricing but there's no French version, I show the English content with a notice bar at the top — written in French so the user can understand it:

Cette page n'est pas encore disponible en français. Vous consultez la version anglaise.

This way, the user knows what's happening and isn't confused by the language mismatch.

Open Graph Tags on Fallback Pages

One detail that's easy to miss: the og:url on fallback pages should also point to the English original, matching the canonical.

<!-- /fr/blog/some-post (fallback, showing English content) -->
<meta property="og:url" content="https://animateoldphotos.org/en/blog/some-post" />
<meta property="og:locale" content="en" />
Enter fullscreen mode Exit fullscreen mode

This ensures that when someone shares the French URL on social media, the preview card pulls from the canonical English version instead of showing a confusing mix of French URL with English content.

The Decision Framework

After going through this process, I boiled it down to a simple decision framework:

Does this page have a real translation in this language?

  • Yes → canonical points to self, include in hreflang cross-references
  • No, but the URL is accessible → canonical points to English original, no hreflang
  • No, and the URL doesn't exist → Nothing to configure (404 or redirect to English)

Should untranslated content be visible in this language?

  • Blog/content pages → Don't show in listing pages. If URL is directly accessed, either redirect or show fallback with notice.
  • Product/tool pages → Show English fallback with notice banner. Hide from language switcher.

Common Mistakes to Avoid

From my experience fixing Animate Old Photos, here are the mistakes I'd warn others about:

1. Declaring hreflang for languages that don't have real translations. If /fr/blog/post shows English content, don't include hreflang="fr" in the tag set. It tells Google that a French version exists when it doesn't.

2. Setting canonical to self on fallback pages. If /fr/blog/post displays English content, its canonical must point to /en/blog/post. Otherwise, Google sees two URLs with identical English content and has to guess which is authoritative.

3. Forgetting about og:url. Social sharing metadata should be consistent with your canonical setup. Mismatched og:url and canonical creates confusion for social platforms.

4. Leaving the html lang attribute as the URL language. If /fr/pricing displays English fallback content, <html lang="fr"> is technically incorrect — the content is English. Ideally set it to lang="en". In practice, if changing this is complex in your framework, it's a minor issue — Google doesn't rely on the lang attribute for language detection.

5. Including untranslated pages in your sitemap for that language. Your French sitemap should only list URLs with actual French content. Fallback pages should not appear.

Wrapping Up

Multilingual SEO isn't just about translating content — it's about correctly signaling to search engines which pages are real translations and which are fallbacks. The canonical and hreflang tags are your primary tools for this, but they need to be paired with thoughtful UI decisions about what to show (and what to hide) from users in each language.

If you want to see these changes in action, check out the blog and changelog sections on Animate Old Photos — switch between languages and see how untranslated content is handled differently from fully translated pages.


I built Animate Old Photos to help people bring their old family photos to life using AI. If you have questions about multilingual website configuration or want to share your own approach, feel free to reach out.

Top comments (0)