You search site:vercel.app your-project and find preview URLs in Google’s index: your-site-git-feature-x.vercel.app, your-site-abc123.vercel.app, deployment hashes you forgot existed. Worse, some rank for your brand keyword instead of your real domain.
Fastest fix: confirm the leaking host is missing X-Robots-Tag: noindex (curl -I <preview-url>), add the header back, fix any sitemap/canonical that points at the preview, then submit the URLs in Search Console Removals to evict them in hours instead of weeks. Details below.
One thing that has changed and trips people up: as of June 2026, both Vercel and Netlify already add X-Robots-Tag: noindex to standard preview deployments automatically. So if a preview is indexed, you are almost never in the “I forgot to add noindex” bucket. You are in one of a few specific edge cases — a custom domain on a non-production branch, an indexable latest branch deploy, an old production deploy, or a sitemap/canonical that contradicts the header. The sections below sort you into the right bucket.
Which bucket are you in?
| Symptom | Likely cause | Jump to |
|---|---|---|
Indexed host is *.vercel.app / *.netlify.app | Header was stripped, or sitemap/canonical overrides it | Cause 1, 4, 6 |
Indexed host is a custom subdomain (e.g. staging.your-site.com) | Custom domain on a non-production branch — Vercel omits noindex here | Cause 1 |
| Only your most recent branch deploy is indexed | Netlify keeps the latest branch deploy indexable by default | Cause 1 |
| Previews open with no login prompt in incognito | Deployment Protection set to None | Cause 5 |
| Indexed previews map 1:1 to PRs in a public repo | Preview link harvested from public GitHub | Cause 3 |
| Old deployments from months ago still indexed | URLs still resolve and sit in Google’s crawl queue | Cause 7 |
Common causes
Ordered by how each leaked into the index.
1. The X-Robots-Tag: noindex header is missing on the leaking host
This is the core diagnostic. Vercel and Netlify both auto-inject X-Robots-Tag: noindex on standard preview deployments, so a leak almost always means the header is absent on the specific host that got indexed. There are three documented ways that happens:
- Vercel custom domain on a non-production branch. This is the big one. Per Vercel’s own docs, when you assign a custom domain (e.g.
staging.your-site.com) to a non-production branch, Vercel does not setX-Robots-Tag: noindex. The automatic protection only covers the generated*.vercel.appURLs. - Netlify’s latest branch deploy. Netlify adds
noindexto Deploy Previews, old branch deploys, and unpublished production deploys — but it lets the most recent branch deploy stay indexable by default. If you publish a persistent branch (astagingbranch), its latest deploy is crawlable. - A framework or middleware that overwrites response headers before the platform header is applied, dropping the platform’s
noindex.
How to spot it: curl -I https://<the-indexed-host>/ does not contain x-robots-tag: noindex. Run this against the exact host Google indexed, not a fresh preview.
2. Preview URL shared in a public Slack/Discord with link unfurling
Slack and Discord both fetch a URL preview when a link is posted. That fetch can seed Google’s crawl if the link later lands in any public archive (a Discourse forum, a public Slack export, a GitHub issue). The unfurl itself does not index the page, but it creates the inbound link Google follows.
How to spot it: Indexed preview URLs trace back to a Slack/Discord share. The link surfaces in a Google search of the channel’s public web archive.
3. PR comment with preview link visible in a public GitHub repo
The Vercel/Netlify GitHub integration auto-posts a preview URL as a PR comment. Public repos make these comments world-readable, Google crawls public GitHub, follows the link, and reaches the preview. This only causes indexing if the preview itself lacks noindex (see cause 1) — but it is the most common way Google discovers the URL in the first place.
How to spot it: Indexed previews correspond 1-to-1 with PRs in a public repo. site:vercel.app plus your project name shows them in roughly PR-number order.
4. Production code outputs preview URLs in the sitemap
A bug in the sitemap generator uses process.env.VERCEL_URL (or Netlify’s process.env.URL / process.env.DEPLOY_PRIME_URL) instead of a hardcoded canonical domain, so preview deploys emit a sitemap pointing at themselves. A sitemap entry is a strong indexing signal that can override the header in Google’s eyes.
How to spot it: curl https://<preview-host>/sitemap.xml contains URLs starting with the preview host, not the canonical domain.
5. Preview deploys have Deployment Protection turned off
Vercel new projects default to Standard Protection (prod_deployment_urls_and_all_previews), which gates every preview behind a Vercel login. But if someone set the project (or the team default) to None, previews are fully public. Netlify password protection on branch deploys / deploy previews is off until you enable it.
How to spot it: Open an incognito window, paste a preview URL — it loads with no auth prompt. (With Standard Protection on, you’d get a “Log in to Vercel” screen.)
6. Canonical tags point at the preview host, not production
<link rel="canonical" href="${import.meta.env.SITE}/path"> where SITE is derived per-deploy puts the preview domain in the canonical. Google reads the self-referential canonical on the preview and can treat the preview as the canonical version even when a noindex header is present — conflicting signals confuse the consolidation. (Strictly, a noindex page should not also be a canonical target; mixed signals are exactly what causes “Google selected a different canonical than the user” in Search Console.)
How to spot it: View source on a preview deploy. The <link rel="canonical"> href starts with the preview host, not your real domain.
7. Old preview/production URLs still resolve and have inbound links
Vercel keeps deployment URLs alive indefinitely by default (a “feature” for permalinks). If old URLs were posted publicly months ago, Google’s crawl queue still has them and rechecks periodically. An old production deploy URL is especially dangerous because at the time it was live it had no noindex.
How to spot it: Indexed URLs include deployments from many months ago. Even after fixing config today, old ones persist until evicted.
Before you start
- Run
site:vercel.app your-projectandsite:netlify.app your-project(or your provider’s preview suffix). Note the count and the exact hosts. - Check Google Search Console for “Duplicate without user-selected canonical”, “Duplicate, Google chose different canonical than user”, or “Crawled - currently not indexed”.
- Identify whether the leaking host is a generated
*.vercel.apphost, a custom subdomain, or a Netlify branch deploy — this decides which cause applies. - Check your team/project Deployment Protection level (Vercel: Settings → Deployment Protection).
- Verify what
process.env.VERCEL_URL/process.env.URLresolves to on a preview build (it changes per deploy). - Note whether your repo is public — that drastically changes the discovery surface.
Information to collect
- Sample of 5 indexed URLs and their corresponding deployment IDs / hosts.
- Output of
curl -Iagainst the exact indexed host: check forx-robots-tag,x-vercel-deployment-url(Vercel) orx-nf-request-id(Netlify). - Your canonical-URL generation code (grep for
canonical,og:url,import.meta.env.SITE,VERCEL_URL). - Your sitemap generation code (often
scripts/build-sitemap.mjsor framework-auto-generated). - The provider’s Deployment Protection settings (and the team-wide default).
- Search Console “Pages” report filtered by
vercel.appornetlify.appto count affected URLs.
Step-by-step fix
Ordered by what stops the bleeding first.
Step 1: Restore the noindex header on the leaking host
If curl -I shows no x-robots-tag, you are in cause 1. The platform’s automatic header does not cover your case, so add it explicitly.
For Vercel — a custom-domain preview needs a vercel.json rule keyed to the non-production host. Vercel’s docs recommend doing this in your framework first; vercel.json is the fallback:
{
"headers": [
{
"source": "/(.*)",
"has": [
{ "type": "host", "value": "(?!your-site\\.com$).*" }
],
"headers": [
{ "key": "X-Robots-Tag", "value": "noindex, nofollow" }
]
}
]
}
For Netlify, force noindex on every non-production context (this overrides Netlify keeping the latest branch deploy indexable):
[[headers]]
for = "/*"
[headers.values]
X-Robots-Tag = "noindex, nofollow"
[context.production]
[context.production.environment]
# production keeps the header OFF — set it only via the rule below
Because the global [[headers]] block applies everywhere, scope the rule per branch with Netlify’s _headers file generated at build time, or gate it on process.env.CONTEXT (production vs deploy-preview vs branch-deploy) so production stays indexable.
For an Astro / framework-level guard that works on any host:
---
const host = Astro.url.hostname;
const isProd = host === "your-site.com";
---
{!isProd && <meta name="robots" content="noindex, nofollow" />}
Keying off the production host (allow-list) rather than the preview host (deny-list) is safer — a new preview suffix can’t slip through.
This stops new crawls. Already-indexed URLs still need step 4 to evict.
Step 2: Fix canonical generation to always use the production hostname
Hardcode or env-pin the canonical so a preview never points at itself:
// src/lib/site.ts
export const SITE = "https://your-site.com"; // never derived from VERCEL_URL
<link rel="canonical" href={new URL(Astro.url.pathname, SITE).toString()} />
Even on a preview deploy, the canonical now points at production, so Google consolidates ranking signals there and stops treating the preview as a distinct page.
Step 3: Lock down preview deployments
Vercel: Project → Settings → Deployment Protection → set Standard Protection (or All Deployments) so previews require a Vercel login. Confirm the team-wide default (Team Settings → Deployment Protection) isn’t set to None, which would silently override new projects.
Netlify: Site → Site configuration → Visitor access → enable Password protection for branch deploys and deploy previews.
With protection on, Googlebot gets the login wall and cannot crawl the preview at all — this is stronger than noindex because the bot never sees the page body.
Step 4: Submit the indexed URLs for removal
For URLs already in Google’s index:
- Google Search Console → Removals → New Request → Remove all URLs with this prefix →
https://your-site-abc123.vercel.app/. - Submit. The block takes effect within hours but is temporary, about 6 months (per Google’s Removals docs). It hides the URL; it does not stop crawling.
- For permanence, pair it with step 1’s
noindexheader so Google drops the URL on its next crawl, or return404/410from the dead hosts.
This is the only fast way to clear indexed previews — natural recrawl-and-drop can take 1-4 weeks per URL. See old deployment url in search for the long-tail removal flow.
Step 5: Audit your sitemap for leaked URLs
Run on a preview build:
npm run build
grep -E "vercel\.app|netlify\.app|deploy-preview" dist/sitemap*.xml
If matches appear, your generator is using a per-deploy host. Replace it with the hardcoded SITE:
import { SITE } from "./site";
const urls = posts.map((p) => `${SITE}${p.url}`);
A preview that emits no sitemap (or one that lists only canonical-domain URLs) removes a strong indexing signal that can otherwise override noindex.
Step 6: Disable preview comments on public GitHub PRs
If the repo is public, the auto-posted preview link is the main discovery path:
Vercel → Project → Settings → Git → toggle “Comments on Pull Requests” off.
Netlify → Site configuration → Build & deploy → Deploy notifications → remove the GitHub PR comment integration.
This stops Google from harvesting new preview URLs via GitHub crawl. Old PR comments stay in Google’s history, so step 4 remains necessary for those.
Step 7: Serve a preview-specific robots.txt
Belt-and-suspenders. Detect the host and disallow on non-production:
// src/pages/robots.txt.ts
export const GET = ({ request }: { request: Request }) => {
const url = new URL(request.url);
const isProd = url.hostname === "your-site.com";
const body = isProd
? "User-agent: *\nAllow: /\nSitemap: https://your-site.com/sitemap.xml\n"
: "User-agent: *\nDisallow: /\n";
return new Response(body, { headers: { "content-type": "text/plain" } });
};
Important: keep the X-Robots-Tag header from step 1 too. robots.txt only blocks crawling — it does not evict a URL Google already knows about, and a Disallow-ed page with inbound links can still show as a bare URL listing. The header is what actually de-indexes. See robots txt not working for related debug paths.
How to confirm it’s fixed
curl -I https://<the-indexed-host>/returnsx-robots-tag: noindex, nofollow.curl https://<preview-host>/sitemap.xmlreturns 404 or contains only canonical-domain URLs.- View-source on a preview deploy shows
<meta name="robots" content="noindex, nofollow">and a canonical pointing at production. - Opening a preview URL in incognito prompts for a Vercel login / password (if Deployment Protection is on).
- Search Console → Removals shows submitted URLs as Approved.
- After ~1-2 weeks, the
site:vercel.app your-projectcount drops (Removals hides immediately; permanent de-index follows the next crawl).
Long-term prevention
- Bake
noindexfor non-production hosts intovercel.json/netlify.toml, committed to the repo, keyed off an allow-list of production hosts — never a runtime-only flag. - Always set canonical from a single hardcoded
SITEconstant, never derived from a runtime env var likeVERCEL_URL. - Keep Deployment Protection at Standard (or stricter) for all non-production deploys, and lock the team-wide default so new projects inherit it.
- Add a CI check that runs
curl -Iagainst the deployed preview URL and fails ifx-robots-tag: noindexis missing. - In Search Console, claim the production domain only — never add a
*.vercel.appproperty (it signals Google to keep crawling those hosts). - Audit
site:vercel.app your-projectquarterly; one indexed preview can cannibalize brand search.
Common pitfalls
- Assuming you “forgot the noindex header” when Vercel/Netlify add it automatically — the real gap is usually a custom-domain preview or a sitemap/canonical that contradicts it.
- Adding
noindexonly in a<meta>tag, not the response header — Google honors both, but a Removals request plus the header is what evicts fast. - Relying on
robots.txtalone — Google does not de-index already-known URLs from aDisallow; the header does. - Keying the rule off the preview suffix instead of the production host — a new suffix slips through. Allow-list production instead.
- Setting
noindexglobally and accidentally hitting production during a refactor — always re-test prod after the change. - PR previews remaining indexable for months after merge because the deployment URL stays alive. See duplicate domain versions indexed.
FAQ
Q: I never touched headers — why is my preview indexed if Vercel adds noindex automatically?
Because the automatic header has documented gaps. Vercel omits X-Robots-Tag: noindex when a custom domain is assigned to a non-production branch, and Netlify keeps the latest branch deploy indexable. Run curl -I against the exact host Google indexed; if the header is missing, you’re in one of those gaps and need step 1.
Q: How long until indexed preview URLs drop from Google after I fix this?
Search Console Removals hides them within hours (temporary, about 6 months). Permanent de-index needs the noindex header plus Google’s next crawl — usually 1-4 weeks per URL. Old, low-traffic URLs can persist longer.
Q: Should I 301 redirect preview URLs to production?
For custom-domain staging hosts that you control, yes — a 301 to the canonical equivalent passes link equity and removes the duplicate. For generated *.vercel.app / *.netlify.app hosts you can’t add per-path redirects to, rely on noindex plus Removals instead.
Q: Will noindex on a preview break the platform’s deployment check?
No. Vercel and Netlify deployment checks read the HTTP status (a 200), not robots directives. The deploy bot ignores X-Robots-Tag.
Q: My custom branch alias (e.g. staging.your-site.com) is leaking — same fix?
Yes, and it’s the most common 2026 case. A custom domain on a non-production branch is exactly the scenario where Vercel does not add noindex. Treat it like a preview: explicit noindex header + Deployment Protection, or 301 it to production.