{"slug":"fix-cdn-cached-redirect-killing-google-indexing","locale":"en","isFallback":false,"translationAvailable":["en","id"],"translationUrls":{"en":"/api/posts/fix-cdn-cached-redirect-killing-google-indexing?locale=en","id":"/api/posts/fix-cdn-cached-redirect-killing-google-indexing?locale=id"},"title":"How a Cached 307 Redirect Quietly Killed My Google Indexing","description":"A cookie-based locale redirect got cached by Firebase CDN without Vary: Cookie, poisoning Googlebot with 307 redirects. Here's how I diagnosed and fixed it.","date":"2026-05-03","updated":null,"tags":["web","nextjs","seo","firebase","debugging"],"category":"Development","content":"\nI noticed something alarming in Google Search Console last week — impressions for SnipGeek dropped from around 180 per day to nearly zero in the span of three days. No deployment errors, no downtime alerts, no content changes. Just silence from Google.\n\nMy first instinct was to check for accidental `noindex` tags or a broken `robots.txt`. Both turned out fine. The real problem was far more subtle — and honestly, I didn't expect a locale cookie to be the culprit.\n\n## The Symptom: Homepage Redirecting to `/id/`\n\nI ran a quick `curl` against the live homepage to check response headers:\n\n```bash\ncurl -sI https://snipgeek.com/ | grep -iE 'HTTP|location|cache|cdn'\n```\n\nThe result stopped me cold:\n\n```\nHTTP/2 307\nlocation: /id/\ncache-control: public, s-maxage=3600, stale-while-revalidate=86400\ncdn-cache-status: hit\nage: 2326\n```\n\nThe homepage was returning a **307 temporary redirect to `/id/`** — the Indonesian locale — for *every single visitor*, including Googlebot. And the CDN had cached it for over 38 minutes already.\n\nI checked other pages like `/blog` and `/about` — those returned 200 normally. But the homepage, the most important page for crawling, was broken.\n\n## The Root Cause: Cookie + CDN = Poison\n\nSnipGeek is a bilingual site (English/Indonesian) built with [Next.js](/blog/firebase-studio-sunset-migration-guide) and deployed on Firebase App Hosting. The i18n routing lives in `proxy.ts` (Next.js 16's replacement for middleware).\n\nHere's the code that caused the issue:\n\n```typescript\n// proxy.ts (BEFORE — the broken version)\nconst preferredLocale = request.cookies.get(\"NEXT_LOCALE\")?.value;\n\nif (pathnameIsMissingLocale) {\n  if (preferredLocale && preferredLocale !== i18n.defaultLocale) {\n    // This redirect gets cached by the CDN!\n    return NextResponse.redirect(\n      new URL(`/${preferredLocale}${pathname}`, request.url),\n    );\n  }\n  return NextResponse.rewrite(\n    new URL(`/${i18n.defaultLocale}${pathname}`, request.url),\n  );\n}\n```\n\nThe logic itself seems reasonable: if a visitor has a `NEXT_LOCALE=id` cookie, redirect them to the Indonesian version. Otherwise, rewrite to the default English locale.\n\nThe problem is what happens *after* the redirect response leaves the server.\n\n## Why the CDN Made It Catastrophic\n\nFirebase App Hosting sits behind a CDN. My `next.config.ts` applied this cache header to all page routes:\n\n```typescript\n{\n  key: \"Cache-Control\",\n  value: \"public, s-maxage=3600, stale-while-revalidate=86400\",\n}\n```\n\nThis told the CDN: \"cache this response for 1 hour, and serve stale for up to 24 hours.\" The critical missing piece? **No `Vary: Cookie` header.**\n\nSo when *I* (or any Indonesian-preferring visitor) hit the homepage with a `NEXT_LOCALE=id` cookie, the CDN cached the 307 redirect. Every subsequent visitor — including Googlebot — got that cached redirect instead of the actual English homepage.\n\nGooglebot doesn't send cookies. It expected to see HTML content at `snipgeek.com/`. Instead, it got bounced to `/id/` over and over, depending on CDN cache timing. The result: Google stopped trusting the homepage as a stable entry point, and impressions collapsed.\n\n<Callout variant=\"danger\" title=\"The Dangerous Pattern\">\n  A cookie-based redirect + public CDN caching without `Vary: Cookie` = **cache poisoning**. The first visitor's cookie determines what everyone else sees until the cache expires.\n</Callout>\n\n## How I Diagnosed It\n\nThe audit was systematic. I checked each layer that could affect indexing:\n\n<Steps>\n  <Step>\n\n  ### Check robots.txt and sitemap.xml\n\n  Both were clean. `robots.txt` allowed `/` for all user agents, and `sitemap.xml` contained all 216 published URLs including every blog post.\n\n  ```bash\n  curl -sS https://snipgeek.com/robots.txt | head -20\n  curl -sS https://snipgeek.com/sitemap.xml | grep -c '<url>'\n  ```\n\n  </Step>\n  <Step>\n\n  ### Check meta robots tags on blog posts\n\n  I fetched a sample blog post and checked the HTML for `noindex` directives:\n\n  ```bash\n  curl -sS https://snipgeek.com/blog/disable-windows-defender-windows-11 \\\n    | grep -i 'robots'\n  ```\n\n  Result: `<meta name=\"robots\" content=\"index, follow\"/>` — correct. No `X-Robots-Tag` header either.\n\n  </Step>\n  <Step>\n\n  ### Test with Googlebot user-agent\n\n  I wanted to make sure Googlebot saw the same thing:\n\n  ```bash\n  curl -sI -A \"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\" \\\n    https://snipgeek.com/blog/disable-windows-defender-windows-11\n  ```\n\n  Blog posts returned 200 with full HTML. But when I tested the homepage — that's where the 307 appeared.\n\n  </Step>\n  <Step>\n\n  ### Trace the redirect origin\n\n  The `age: 2326` and `cdn-cache-status: hit` headers confirmed it wasn't a fresh response from the server. The CDN was serving a stale redirect. I traced it back to `proxy.ts` and found the cookie-based redirect branch.\n\n  </Step>\n</Steps>\n\n## The Fix: Three Lines Removed\n\nThe fix was minimal. I removed the cookie-based redirect branch entirely from `proxy.ts`:\n\n```typescript\n// proxy.ts (AFTER — the fixed version)\nif (pathnameIsMissingLocale) {\n  return NextResponse.rewrite(\n    new URL(\n      `/${i18n.defaultLocale}${pathname.startsWith(\"/\") ? \"\" : \"/\"}${pathname}`,\n      request.url,\n    ),\n  );\n}\n```\n\nNow locale-less URLs **always** rewrite to the default English locale. No redirect, no cache poisoning risk.\n\nThe cookie-based language preference still works — but entirely client-side. The `LanguageSwitcher` component and the `LocaleSuggestionBanner` both use `router.push()` to navigate to the `/id/` URL when the user switches languages. The cookie is still set for the banner's \"don't show again\" logic, but the proxy never reads it.\n\n<Callout variant=\"info\" title=\"The Principle\">\n  Server-side redirects that depend on cookies should never be publicly cached. Either add `Vary: Cookie` (which effectively disables CDN caching) or move the logic client-side.\n</Callout>\n\n## What Else I Hardened\n\nWhile I had the hood open, I made a few more defensive changes:\n\n- **Removed duplicate `/en` redirect** from `next.config.ts` — the proxy already canonicalizes `/en/*` → `/*` with a 308 redirect. Having both was redundant.\n- **Added explicit `robots` meta** to blog and note `generateMetadata` — previously relied on layout inheritance, which is fragile if the layout metadata changes.\n- **Added HowTo JSON-LD schema** for tutorial-tagged articles — this enables [rich results in Google Search](/blog/to-do-after-install-windows11) for step-by-step content.\n\n## Lessons Learned\n\nThe scariest part of this bug was how invisible it was. The site loaded fine in my browser (I had the cookie, so I saw the redirect and landed on `/id/` — exactly as designed). No error logs, no 500s, no build failures.\n\nThe only signal was the Search Console graph dropping to zero. If I hadn't checked GSC that week, it could have gone unnoticed for much longer.\n\nThree takeaways I'm keeping pinned:\n\n- **Never return a cookie-dependent redirect with `public` cache headers.** If the response varies by cookie, either mark it `private` or use `Vary: Cookie`.\n- **Test your homepage with `curl`, not a browser.** Browsers carry cookies and cache state that hide CDN-level issues.\n- **Check Search Console weekly.** It's the only early warning system for crawl-level problems that don't show up in application monitoring.\n\nIf you're running a multilingual Next.js site on any CDN-fronted hosting (Firebase, Vercel, Cloudflare), I'd strongly recommend running `curl -sI your-homepage.com` right now. You might be surprised by what comes back.\n\n### References\n1. [Google on 307 redirects and indexing](https://developers.google.com/search/docs/crawling-indexing/301-redirects)\n2. [Next.js proxy.ts documentation](https://nextjs.org/docs/app/api-reference/file-conventions/proxy)\n3. [HTTP Vary header — MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary)\n"}