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:
- Coverage: no errors
- Sitemap: "Submitted and processed" ✓
- Last crawled: 4 weeks ago
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
}
]
}
- Added
sitemapandrobotsto the redirect exclusion regex (not just the rewrite exclusion) - 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
-
Vercel deploys silently. No validation of
vercel.jsonagainst your actual routes. The redirect rule was technically valid — it just matched more URLs than intended. -
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.
-
Old pages still ranked. Google had already indexed the older content. Only new content was invisible, which takes weeks to notice.
-
Local development doesn't use vercel.json.
next devignores 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.