Deploy Preview URLs Got Indexed by Google

Vercel/Netlify preview URLs showing in Google's index — usually a custom-domain preview, an indexable latest branch deploy, or a sitemap/canonical that overrides the auto noindex. Here's the fast fix.

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?

SymptomLikely causeJump to
Indexed host is *.vercel.app / *.netlify.appHeader was stripped, or sitemap/canonical overrides itCause 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 hereCause 1
Only your most recent branch deploy is indexedNetlify keeps the latest branch deploy indexable by defaultCause 1
Previews open with no login prompt in incognitoDeployment Protection set to NoneCause 5
Indexed previews map 1:1 to PRs in a public repoPreview link harvested from public GitHubCause 3
Old deployments from months ago still indexedURLs still resolve and sit in Google’s crawl queueCause 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 set X-Robots-Tag: noindex. The automatic protection only covers the generated *.vercel.app URLs.
  • Netlify’s latest branch deploy. Netlify adds noindex to 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 (a staging branch), 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.

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.

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.

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-project and site: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.app host, 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.URL resolves 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 -I against the exact indexed host: check for x-robots-tag, x-vercel-deployment-url (Vercel) or x-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.mjs or framework-auto-generated).
  • The provider’s Deployment Protection settings (and the team-wide default).
  • Search Console “Pages” report filtered by vercel.app or netlify.app to 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:

  1. Google Search Console → RemovalsNew RequestRemove all URLs with this prefixhttps://your-site-abc123.vercel.app/.
  2. 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.
  3. For permanence, pair it with step 1’s noindex header so Google drops the URL on its next crawl, or return 404/410 from 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>/ returns x-robots-tag: noindex, nofollow.
  • curl https://<preview-host>/sitemap.xml returns 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-project count drops (Removals hides immediately; permanent de-index follows the next crawl).

Long-term prevention

  • Bake noindex for non-production hosts into vercel.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 SITE constant, never derived from a runtime env var like VERCEL_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 -I against the deployed preview URL and fails if x-robots-tag: noindex is missing.
  • In Search Console, claim the production domain only — never add a *.vercel.app property (it signals Google to keep crawling those hosts).
  • Audit site:vercel.app your-project quarterly; 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 noindex only 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.txt alone — Google does not de-index already-known URLs from a Disallow; 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 noindex globally 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.

Tags: #Troubleshooting #SEO #preview #Vercel #netlify