<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: SeanX</title>
    <description>The latest articles on DEV Community by SeanX (@seanx).</description>
    <link>https://dev.to/seanx</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1435198%2Fd61ae4b3-bae1-4629-b181-c4b5cc34b809.jpg</url>
      <title>DEV Community: SeanX</title>
      <link>https://dev.to/seanx</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/seanx"/>
    <language>en</language>
    <item>
      <title>How I Fixed the Canonical and Hreflang Mess on My Multilingual Website</title>
      <dc:creator>SeanX</dc:creator>
      <pubDate>Mon, 27 Apr 2026 05:58:46 +0000</pubDate>
      <link>https://dev.to/seanx/how-i-fixed-the-canonical-and-hreflang-mess-on-my-multilingual-website-59lk</link>
      <guid>https://dev.to/seanx/how-i-fixed-the-canonical-and-hreflang-mess-on-my-multilingual-website-59lk</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl2611ajsbjm81ouu925b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl2611ajsbjm81ouu925b.png" alt="A multilingual website with language switcher showing different flags" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I run &lt;a href="https://animateoldphotos.org" rel="noopener noreferrer"&gt;Animate Old Photos&lt;/a&gt;, 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 &lt;code&gt;canonical&lt;/code&gt; and &lt;code&gt;hreflang&lt;/code&gt; tags were misconfigured across pages that didn't have full language coverage.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Not Every Page Has Every Language
&lt;/h2&gt;

&lt;p&gt;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 &lt;a href="https://animateoldphotos.org" rel="noopener noreferrer"&gt;Animate Old Photos&lt;/a&gt;, I had three types of pages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fully translated pages&lt;/strong&gt; — available in all supported languages (e.g., homepage, pricing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partially translated pages&lt;/strong&gt; — available in some languages but not all (e.g., a blog post in English, Spanish, and Portuguese, but not French)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;English-only pages&lt;/strong&gt; — only the English version exists (e.g., some newer blog posts)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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 &lt;code&gt;canonical&lt;/code&gt; and &lt;code&gt;hreflang&lt;/code&gt; for each of these scenarios?&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Was Doing Wrong
&lt;/h2&gt;

&lt;p&gt;Before fixing things, my site had two main issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Issue 1: The blog listing page showed untranslated content.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On the French blog page (&lt;code&gt;/fr/blog&lt;/code&gt;), 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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc9xw5nyzbfvaet4vrtxg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc9xw5nyzbfvaet4vrtxg.png" alt="Screenshot of /fr/blog page before the fix showing a mix of French headers and untranslated English blog post cards" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Issue 2: Fallback pages had wrong canonical and hreflang tags.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Pages like &lt;code&gt;/fr/blog/how-to-restore-old-photos-online-free-ai&lt;/code&gt; were serving English content but declaring themselves as independent French pages with &lt;code&gt;canonical&lt;/code&gt; pointing to themselves and &lt;code&gt;hreflang&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7pn2fgaolk1c63ob935r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7pn2fgaolk1c63ob935r.png" alt="HTML source code showing incorrect self-referencing canonical and hreflang tags on a fallback language page" width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Three Scenarios, Three Configurations
&lt;/h2&gt;

&lt;p&gt;Let me break down exactly how I configured &lt;code&gt;canonical&lt;/code&gt; and &lt;code&gt;hreflang&lt;/code&gt; for each scenario.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 1: Page Is Fully Translated
&lt;/h3&gt;

&lt;p&gt;A page like the pricing page exists in English, Spanish, Portuguese, and French. Every version has real translated content.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- /en/pricing --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"canonical"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://animateoldphotos.org/en/pricing"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://animateoldphotos.org/en/pricing"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"es"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://animateoldphotos.org/es/pricing"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"pt"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://animateoldphotos.org/pt/pricing"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"fr"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://animateoldphotos.org/fr/pricing"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"x-default"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://animateoldphotos.org/en/pricing"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- /fr/pricing --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"canonical"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://animateoldphotos.org/fr/pricing"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://animateoldphotos.org/en/pricing"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"es"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://animateoldphotos.org/es/pricing"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"pt"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://animateoldphotos.org/pt/pricing"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"fr"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://animateoldphotos.org/fr/pricing"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"x-default"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://animateoldphotos.org/en/pricing"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; Each translated page's &lt;code&gt;canonical&lt;/code&gt; points to itself. All translated versions cross-reference each other via &lt;code&gt;hreflang&lt;/code&gt;. The &lt;code&gt;x-default&lt;/code&gt; points to English as the fallback.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 2: Page Is Partially Translated
&lt;/h3&gt;

&lt;p&gt;A blog post exists in English, Spanish, and Portuguese — but not French. This was the trickiest scenario for me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The translated pages (EN, ES, PT) reference each other, but do NOT include French:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- /en/blog/animate-old-photos-guide --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"canonical"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://animateoldphotos.org/en/blog/animate-old-photos-guide"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://animateoldphotos.org/en/blog/animate-old-photos-guide"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"es"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://animateoldphotos.org/es/blog/animate-old-photos-guide"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"pt"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://animateoldphotos.org/pt/blog/animate-old-photos-guide"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"x-default"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://animateoldphotos.org/en/blog/animate-old-photos-guide"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- No hreflang="fr" — because French version doesn't exist --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spanish and Portuguese pages follow the same pattern — &lt;code&gt;canonical&lt;/code&gt; to self, &lt;code&gt;hreflang&lt;/code&gt; only among EN/ES/PT.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The French fallback page gets special treatment:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- /fr/blog/animate-old-photos-guide (displays English fallback content) --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"canonical"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://animateoldphotos.org/en/blog/animate-old-photos-guide"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- No hreflang tags at all --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h3&gt;
  
  
  Scenario 3: Page Is English-Only
&lt;/h3&gt;

&lt;p&gt;Some of my newer blog posts only exist in English. No translations at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- /en/blog/chrome-extension-tutorial --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"canonical"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://animateoldphotos.org/en/blog/chrome-extension-tutorial"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- No hreflang needed — only one language version exists --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No &lt;code&gt;hreflang&lt;/code&gt; at all. It only serves a purpose when there are multiple language versions to cross-reference.&lt;/p&gt;

&lt;p&gt;If the French URL (&lt;code&gt;/fr/blog/chrome-extension-tutorial&lt;/code&gt;) is still accessible and shows English fallback content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- /fr/blog/chrome-extension-tutorial (fallback) --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"canonical"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://animateoldphotos.org/en/blog/chrome-extension-tutorial"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- No hreflang --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same pattern as Scenario 2 — &lt;code&gt;canonical&lt;/code&gt; to English, no &lt;code&gt;hreflang&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond Meta Tags: What I Changed in the UI
&lt;/h2&gt;

&lt;p&gt;Getting the meta tags right was only half the battle. I also made changes to how untranslated content appears to users.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blog Listing Pages: Hide Untranslated Posts
&lt;/h3&gt;

&lt;p&gt;On &lt;code&gt;/fr/blog&lt;/code&gt;, 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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhpxd0sdzaeeio3a7fm1u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhpxd0sdzaeeio3a7fm1u.png" alt="Clean French blog listing page on Animate Old Photos showing only fully translated French articles" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Language Switcher: Only Show Available Languages
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fosct1nc1x1mwxmmm2om6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fosct1nc1x1mwxmmm2om6.png" alt="Optimized language switcher dropdown showing only languages where a translation is available for the current page" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Fallback Pages: Show a Notice Banner
&lt;/h3&gt;

&lt;p&gt;For product and tool pages (not blog posts), if a user lands on a URL like &lt;code&gt;/fr/pricing&lt;/code&gt; 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:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cette page n'est pas encore disponible en français. Vous consultez la version anglaise.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This way, the user knows what's happening and isn't confused by the language mismatch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Open Graph Tags on Fallback Pages
&lt;/h3&gt;

&lt;p&gt;One detail that's easy to miss: the &lt;code&gt;og:url&lt;/code&gt; on fallback pages should also point to the English original, matching the &lt;code&gt;canonical&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- /fr/blog/some-post (fallback, showing English content) --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:url"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"https://animateoldphotos.org/en/blog/some-post"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:locale"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Decision Framework
&lt;/h2&gt;

&lt;p&gt;After going through this process, I boiled it down to a simple decision framework:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this page have a real translation in this language?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Yes →&lt;/strong&gt; &lt;code&gt;canonical&lt;/code&gt; points to self, include in &lt;code&gt;hreflang&lt;/code&gt; cross-references&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No, but the URL is accessible →&lt;/strong&gt; &lt;code&gt;canonical&lt;/code&gt; points to English original, no &lt;code&gt;hreflang&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No, and the URL doesn't exist →&lt;/strong&gt; Nothing to configure (404 or redirect to English)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Should untranslated content be visible in this language?&lt;/strong&gt;&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Common Mistakes to Avoid
&lt;/h2&gt;

&lt;p&gt;From my experience fixing &lt;a href="https://animateoldphotos.org" rel="noopener noreferrer"&gt;Animate Old Photos&lt;/a&gt;, here are the mistakes I'd warn others about:&lt;/p&gt;

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

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

&lt;p&gt;&lt;strong&gt;3. Forgetting about og:url.&lt;/strong&gt; Social sharing metadata should be consistent with your canonical setup. Mismatched &lt;code&gt;og:url&lt;/code&gt; and &lt;code&gt;canonical&lt;/code&gt; creates confusion for social platforms.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;5. Including untranslated pages in your sitemap for that language.&lt;/strong&gt; Your French sitemap should only list URLs with actual French content. Fallback pages should not appear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;canonical&lt;/code&gt; and &lt;code&gt;hreflang&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;If you want to see these changes in action, check out the blog and changelog sections on &lt;a href="https://animateoldphotos.org" rel="noopener noreferrer"&gt;Animate Old Photos&lt;/a&gt; — switch between languages and see how untranslated content is handled differently from fully translated pages.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I built &lt;a href="https://animateoldphotos.org" rel="noopener noreferrer"&gt;Animate Old Photos&lt;/a&gt; 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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
