Automation Pipeline4 min read

I Left the Redirect Temporary and Two Languages Vanished

Three duplicate-canonical notices in Search Console. Two weren't defects — they were success signals. The third was real: a 307 was keeping the root as canonical. But the bigger loss was somewhere else entirely.

#seo#search-console#nextjs#canonical#i18n#gotchas
Two-panel diagram. Left shows a 307 temporary redirect keeping the search engine's canonical on the root while the declared locale path is excluded as a duplicate. Right shows why the English path was undiscovered in 9 of 10 samples: no server-rendered link to other locales, and neither hreflang nor a sitemap entry is a crawl path.
hreflang is not a path. Crawlers follow links.

Three "Duplicate — Google chose a different canonical than the user" notices arrived in Search Console. All three sites split by locale path (/ko, /en, /ja).

Three identical notices, so I assumed one cause. The three had different causes, and two weren't defects at all.

Are you collapsing three identically-worded alerts into one cause?

Two were success signals

Two of the sites had already completed a migration from subdomains to path-based locales.

The canonical Google picked was the destination we intended. The notice only arrived now because the last crawl was three months ago — a snapshot from before the redirect existed.

On a property mid-migration, this notice should be read as a success signal. Time spent here fixes nothing.

The third was real

The root redirect was a 307 (temporary).

googleCanonical = https://<site>/        ← Google's pick (indexed)
userCanonical   = https://<site>/ko      ← our declaration → excluded as duplicate

A 307 is not a consolidation signal — see also the accept-language routing bug at the same redirect point, and a root canonical that duplicated every child, so Google keeps the original URL — the root — as canonical. Eight of ten sampled URLs showed the same pattern. Switch to 308 (permanent) and it consolidates onto the destination.

But the bigger loss was elsewhere

In 9 of 10 samples, /en was "URL is unknown to Google."

The reason is simple. The server-rendered HTML of /ko contained no <a> tag pointing at another locale. Language switching was client-side. For a crawler there is no path.

Two things had been reassuring me, and neither is a path.

  • <link rel="alternate" hreflang> alone does not get you indexed.
  • Neither does being in the sitemap. One site had three URLs in its sitemap and two of them were undiscovered.

The fix was a server-rendered language nav in the footer.

<nav aria-label="language"><a href="/en">English</a> · <a href="/ja">日本語</a></nav>

What would you do?

You need to turn a 307 into a 308. Where do you edit?

  • The page component's redirect() — the first place you notice in the code.
  • The framework config's redirects() — handled before the request reaches a page.
  • Both — without checking which one actually runs.

In 27 of 30 apps the config's redirects() handles the root, and the page component's redirect() is a fallback that never executes. Fix only the page side and ship, and live is still 307.

That gives a diagnostic rule: if a footer change from the same deploy is live but the redirect didn't change, it isn't a cache problem — the code you fixed is unreachable.

One site must not become a 308

I stopped short of applying it everywhere. One site branches at the root on the visitor's language preference. Make that response permanent and whichever language the first visitor received gets frozen into caches.

Finding one exception in a bulk edit usually doesn't mean there's exactly one. I re-checked the root behaviour of the other 29.

Three checks

  1. Is locale switching client-side? Then confirm a real <a href> exists in the server HTML. Without one, that language stays undiscovered.
  2. Is your root redirect a 307 or a 308? One curl -sI <root> tells you. If you want consolidation, it's 308.
  3. Fixed a redirect and live didn't change? Before suspecting caches, confirm that code is even on the executing path.

The honest part

The thing I spent longest on was the two that weren't defects — because the wording was identical, so I assumed the cause was too.

And the real loss, two languages undiscovered, never appeared in the notices at all. An unindexed URL isn't reported as a duplicate either. To find a loss that isn't reported, I had to inspect URLs one at a time rather than wait for an alert.

Related