Skip to main content
Back to Blog
5 min read

The vercel.json that silently broke my sitemap for a month

A single redirect rule in vercel.json caused my sitemap to return HTML instead of XML. Google stopped indexing new pages for 4 weeks. Here's how I found it and the debugging process.

performancenext.js

For four weeks, Google didn't index a single new page on a client's site. No errors in Search Console. No crawl issues. The sitemap showed "success" status. I didn't notice until I searched for a blog post by its exact title and got nothing.

The root cause was one line in vercel.json.

The symptom

I published 3 new blog posts over a month. None appeared in Google search. Older posts still ranked fine. The site was healthy — no 500 errors, no deployment issues, fast load times.

I checked Google Search Console:

That last line was the clue. Google had crawled the sitemap a month ago and hadn't come back. But the sitemap status said "success." Why would Google stop re-crawling a sitemap that it successfully processed?

The investigation

Step 1: I opened the sitemap URL in a browser.

https://client-site.com/sitemap.xml

It rendered the homepage. Not XML — the actual HTML homepage.

Step 2: I checked with curl to rule out browser redirects.

curl -v https://client-site.com/sitemap.xml
< HTTP/2 200
< content-type: text/html; charset=utf-8

The sitemap URL returned a 200 with text/html. The sitemap was being served as an HTML page. Google "successfully" fetched it, saw it was HTML, and silently gave up trying to parse it as a sitemap.

Step 3: I checked vercel.json.

{
  "redirects": [
    {
      "source": "/:path((?!api|_next).*)",
      "has": [{ "type": "host", "value": "client-site.com" }],
      "destination": "https://client-site.com/:path",
      "permanent": true
    }
  ],
  "rewrites": [
    {
      "source": "/((?!en|ru|uk|api|_next|sitemap|robots).*)",
      "destination": "/en/$1"
    }
  ]
}

The rewrites rule was meant to redirect bare URLs (without a locale prefix) to the English locale. For example, /about/en/about. The regex excluded api, _next, sitemap, and robots.

Except it didn't exclude sitemap.xml. It excluded sitemap. The URL /sitemap.xml starts with sitemap, but the regex ((?!en|ru|uk|api|_next|sitemap|robots).*) checks from the start of the path. Let me re-read that regex...

Actually, the regex was correct in excluding paths that start with sitemap. So /sitemap.xml should have matched the exclusion. I was wrong about the regex.

Step 4: I checked if there was a trailing slash issue.

curl -v https://client-site.com/sitemap.xml/

Same result — 200, HTML. But then I checked:

curl -I https://client-site.com/sitemap.xml
HTTP/2 301
location: /en/sitemap.xml

There it was. A different rule — the redirects block — was catching /sitemap.xml and redirecting it to /en/sitemap.xml. The rewrites exclusion was irrelevant because the redirects block ran first.

/en/sitemap.xml didn't exist. Next.js rendered the catch-all 404 page (which returned 200 because of a separate issue — a misconfigured not-found page). Google saw a 200 HTML response and marked the sitemap as "processed."

The fix

Two changes:

{
  "redirects": [
    {
      "source": "/:path((?!api|_next|sitemap|robots).*)",
      "has": [{ "type": "host", "value": "client-site.com" }],
      "destination": "https://client-site.com/:path",
      "permanent": true
    }
  ]
}
  1. Added sitemap and robots to the redirect exclusion regex (not just the rewrite exclusion)
  2. Fixed the not-found page to actually return a 404 status code

After deploying:

curl -I https://client-site.com/sitemap.xml
HTTP/2 200
content-type: application/xml

Correct. XML sitemap served directly by Next.js's app/sitemap.ts.

Why I didn't catch it sooner

  1. Vercel deploys silently. No validation of vercel.json against your actual routes. The redirect rule was technically valid — it just matched more URLs than intended.

  2. Google Search Console said "success." A 200 response with HTML isn't an error to Google's sitemap processor — it just silently ignores non-XML content. No warning, no alert.

  3. Old pages still ranked. Google had already indexed the older content. Only new content was invisible, which takes weeks to notice.

  4. Local development doesn't use vercel.json. next dev ignores Vercel-specific redirect rules. The sitemap worked perfectly locally.

The prevention checklist

After this incident, I added these to my deploy pipeline:

# Post-deploy smoke test
SITEMAP_TYPE=$(curl -s -o /dev/null -w "%{content_type}" https://yoursite.com/sitemap.xml)
if [[ "$SITEMAP_TYPE" != *"xml"* ]]; then
  echo "ALERT: Sitemap is not returning XML"
  exit 1
fi

ROBOTS_TYPE=$(curl -s -o /dev/null -w "%{content_type}" https://yoursite.com/robots.txt)
if [[ "$ROBOTS_TYPE" != *"text/plain"* ]]; then
  echo "ALERT: robots.txt is not returning text/plain"
  exit 1
fi

Two curl checks. 10 seconds to run. Would have caught this on the first deploy instead of 4 weeks later.

The meta-lesson

vercel.json redirects and rewrites are processed in order: headers → redirects → rewrites → filesystem. If a redirect matches before a rewrite exclusion, the exclusion never runs. And if your catch-all route returns 200 instead of 404, every broken redirect becomes invisible — it looks like it's working because the response code says OK.

Test your infrastructure routes (/sitemap.xml, /robots.txt, /api/*) after every vercel.json change. Not just your app routes.


Worried about silent SEO issues on your Next.js site? Let's talk — I run post-deploy checks on every client project specifically to catch this class of invisible bug.