<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[HebrewGlot Engineering]]></title><description><![CDATA[HebrewGlot Engineering]]></description><link>https://hebrewglot.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ae63e1e25d90129d8b60a52/002b88ba-6a14-4aa5-a5bc-2798450cacd2.png</url><title>HebrewGlot Engineering</title><link>https://hebrewglot.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sun, 01 Nov 2026 19:22:34 GMT</lastBuildDate><atom:link href="https://hebrewglot.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Two i18n mistakes that kept half our Next.js site out of Google]]></title><description><![CDATA[We run HebrewGlot, a platform for learning Hebrew, built on Next.js 15 with the App Router and two locales, /en and /ru. Almost all of our traffic comes…]]></description><link>https://hebrewglot.hashnode.dev/two-i18n-mistakes-that-kept-half-our-next-js-site-out-of-google</link><guid isPermaLink="true">https://hebrewglot.hashnode.dev/two-i18n-mistakes-that-kept-half-our-next-js-site-out-of-google</guid><category><![CDATA[Next.js]]></category><category><![CDATA[SEO]]></category><category><![CDATA[i18n]]></category><dc:creator><![CDATA[HebrewGlot Team]]></dc:creator><pubDate>Sat, 31 Oct 2026 19:31:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ae63e1e25d90129d8b60a52/f2df7408-8ac5-4034-8f28-a762f2b61c9d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We run HebrewGlot, a platform for learning Hebrew, built on Next.js 15 with the App Router and two locales, <code>/en</code> and <code>/ru</code>. Almost all of our traffic comes from search, so when an audit showed that <strong>Google had indexed only 256 of the 467 URLs in our sitemap (55%)</strong>, we went looking.</p>
<p>Several things were wrong. Two of them were pure internationalization bugs that we suspect are very common, so here they are in detail.</p>
<h2>Mistake 1: redirecting locale-less URLs with a 302</h2>
<p>Like most multilingual sites, we redirect a request without a locale (<code>/</code>, <code>/tools/numbers</code>) to the right version based on <code>Accept-Language</code>. Our middleware did this:</p>
<pre><code class="language-ts">// before
return NextResponse.redirect(new URL(`/${locale}${pathname}`, request.url));
</code></pre>
<p><code>NextResponse.redirect</code> defaults to a <strong>temporary</strong> redirect. To Google, a temporary redirect says: "the real page is the original URL; this is just for now." So Google kept treating <code>/</code> as canonical and was reluctant to index <code>/en</code> and <code>/ru</code> as separate pages.</p>
<p>The fix is small, with one important detail:</p>
<pre><code class="language-ts">const locale = getPreferredLocale(request);
const target = new URL(pathname === '/' ? `/${locale}` : `/${locale}${pathname}`, request.url);
target.search = request.nextUrl.search;

const response = NextResponse.redirect(target, { status: 308 });
response.headers.set('Vary', 'Accept-Language');
return response;
</code></pre>
<ul>
<li><strong>308</strong> is the permanent equivalent of 307 (it preserves the method). Search engines treat it like a 301.</li>
<li><strong><code>Vary: Accept-Language</code></strong> matters because the response depends on a request header. Without it, a CDN or proxy may cache "redirect to /en" and serve it to Russian speakers too.</li>
</ul>
<p>A few days after deploying, URL Inspection showed <code>/ru</code> and <code>/ru/tools/numbers</code> indexed and selected as their own canonicals.</p>
<h2>Mistake 2: a language switcher that generated 404s</h2>
<p>Search Console reported 96 "Not found" errors. Nearly all of them were blog posts in the wrong locale: <code>/en/blog/kulinarnyj-ivrit</code>, <code>/ru/blog/binyanim-complete-guide</code>.</p>
<p>The cause was the language switcher:</p>
<pre><code class="language-ts">// before
const href = pathname.replace(`/${locale}/`, `/${nextLocale}/`);
</code></pre>
<p>That works for pages whose path is the same in both languages (<code>/en/lessons</code> ↔ <code>/ru/lessons</code>). It breaks for content with localized slugs. Our Russian alphabet guide is <code>alfavit-ivrita-polnyy-gid</code> and the English one is <code>hebrew-alphabet-guide</code>: a translation pair, not one URL. Swapping the prefix produced a page that doesn't exist, and Googlebot happily followed every one of those links.</p>
<p>The fix has two parts:</p>
<ol>
<li><strong>The switcher looks up the translation</strong> in a map of translation pairs (the same map that generates <code>hreflang</code>). If a post has no translation, it links to the blog index of the other locale instead of guessing.</li>
<li><strong>The middleware redirects old wrong-locale URLs</strong> to the translation (or the index), so links already in search results stop producing 404s.</li>
</ol>
<pre><code class="language-ts">// sketch of the switcher logic
function switchHref(pathname: string, from: Locale, to: Locale) {
  const m = pathname.match(/^\/(en|ru)\/blog\/([^/]+)$/);
  if (m) {
    const translated = getTranslationSlug(m[2], from, to); // from the hreflang map
    return translated ? `/${to}/blog/${translated}` : `/${to}/blog`;
  }
  return pathname.replace(`/${from}`, `/${to}`);
}
</code></pre>
<h2>A quick checklist for multilingual Next.js sites</h2>
<ol>
<li>Redirects from locale-less URLs: <strong>301/308 only</strong>, with <code>Vary: Accept-Language</code>.</li>
<li>If any content has <strong>localized slugs</strong>, your language switcher must know the translation pairs.</li>
<li>Reuse <strong>one source of truth</strong> for translation pairs: hreflang, the switcher and redirects should all read from it.</li>
<li>Run your sitemap through the <strong>URL Inspection API</strong> at least once. You won't notice "55% indexed" by browsing your own site.</li>
</ol>
<p>These weren't our only problems: orphaned blog posts and client-rendered dictionary pages mattered too. But the two i18n bugs were the cheapest to fix and the easiest to miss.</p>
<p>If you want to see the result, <a href="https://hebrewglot.com/en?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=dev-i18n&amp;utm_content=end-link">HebrewGlot</a> is live. Questions about the implementation are welcome in the comments.</p>
]]></content:encoded></item></channel></rss>