SEO for Vercel and Next.js sites in 2026

TL;DR

  • Vercel hosts the site and your framework decides what Google and the AI crawlers read when they arrive, which on a Next.js project comes down to two files, one header, and a handful of settings you can check in an afternoon
  • Vercel adds an X-Robots-Tag: noindex header to preview deployments and to outdated production deployments, so duplicate copies of your site stay out of search without any work from you
  • A custom domain assigned to a non-production branch is the one preview case that gets no automatic noindex header
  • Deployment Protection set to All Deployments puts a login wall in front of your production domain as well, and a crawler cannot log in
  • Next.js gives you app/robots.ts and app/sitemap.ts, so write both, verify the site in Google Search Console, and submit the sitemap
  • Google says a page needs no special markup to appear in AI Overviews or AI Mode, only that it is indexed and eligible to show with a snippet
  • Vercel's bot protection and AI bots rulesets ship switched off, so turn them on deliberately, and never behind another proxy

Why "Vercel SEO" is really Next.js SEO

When we ran "vercel seo" through Google's results on 30 September 2026, the top organic result was an agency template site hosted on vercel.app. The AI Overview above it drew on Vercel's own knowledge base, a GitHub discussion, and a Reddit thread.

The demand is small. Google Ads Keyword Planner puts "nextjs seo" at roughly 1,600 searches a month worldwide and "vercel seo" at 210. This guide is for the person who shipped a site from v0 or Claude Code last week and wants to know whether Google can see it.

What Vercel does and what your Next.js app does

JobWho does itWhere to check
Keep previews and outdated production deployments out of searchVercel, automaticallycurl -I on a preview URL
Serve a robots.txtYour Next.js appyourdomain.com/robots.txt
Serve a sitemapYour Next.js appyourdomain.com/sitemap.xml
Titles, descriptions, canonical URLsYour Next.js appView source on any page
Structured dataYour Next.js appGoogle's Rich Results Test
Prove you own the site to GoogleYou, in Search ConsoleSearch Console property list
Filter bot trafficYou, in the Vercel Firewall (optional)Firewall tab in your project

Previews, old deployments and the custom domain trap

Vercel's knowledge base is clear on the default: every deployment from a non-production branch carries an X-Robots-Tag noindex header, and so does the previous production deployment once you promote a new one. You can check it yourself.

curl -I https://your-preview-url.vercel.app

Look for x-robots-tag: noindex in the response. If it is there, the copy stays out of Google.

The exception is a custom domain on a non-production branch. Vercel leaves the header off in that case, so a domain like staging.example.com carries no noindex. Search engines can then index it, and you end up with two copies of your site competing for the same queries. Set the header yourself in next.config.js.

module.exports = {
  async headers() {
    if (process.env.VERCEL_ENV !== 'preview') return []
    return [
      {
        source: '/:path*',
        headers: [{ key: 'X-Robots-Tag', value: 'noindex' }],
      },
    ]
  },
}

Two more points from the same page. The header is a request to search engines, and anyone with the URL can still open the page. And if your production .vercel.app address gets indexed next to your custom domain, point search engines at a single canonical address.

When Deployment Protection blocks Google

Deployment Protection decides who can open your preview and production URLs, and it demands authentication for every request. Standard Protection covers everything except production domains, and Vercel recommends it for most projects. All Deployments covers production as well, and Vercel describes it as shutting public access off completely.

That setting suits a client review. It ruins a live marketing site, because Googlebot has no login. It also breaks the HTML tag and HTML file methods of Search Console verification, which need a homepage that logged-out visitors can reach.

If you publish from v0, the first-publish flow includes a visibility step where you choose who can access the deployed app, and the options depend on your plan and protection settings. Read that step slowly. Then open your production URL in a private browser window and confirm it loads without a login.

robots.txt and sitemap.xml in Next.js

Next.js reads app/robots.ts and app/sitemap.ts and serves them at /robots.txt and /sitemap.xml. A minimal pair looks like this.

// app/robots.ts
import type { MetadataRoute } from 'next'

export default function robots(): MetadataRoute.Robots {
  return {
    rules: [{ userAgent: '*', allow: '/' }],
    sitemap: 'https://yourdomain.com/sitemap.xml',
  }
}
// app/sitemap.ts
import type { MetadataRoute } from 'next'
import { getAllPosts } from '@/lib/posts'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getAllPosts()
  return [
    { url: 'https://yourdomain.com', lastModified: '2026-09-01' },
    ...posts.map((post) => ({
      url: `https://yourdomain.com/blog/${post.slug}`,
      lastModified: post.updatedAt,
    })),
  ]
}

Two details trip people up. The example in the Next.js docs sets changeFrequency and priority, and Google ignores both. The same example stamps lastModified with new Date(), which gives every URL the moment the file was generated. Google uses lastmod only when it is consistently and verifiably accurate, so feed it the real updated date from your CMS or your database.

The rest of Google's sitemap rules are short. Use absolute URLs, list the canonical version of each page, keep each file under 50MB or 50,000 URLs, and host it at the root. Submitting a sitemap is a hint and does not guarantee Google will use it.

Metadata, structured data and the raw HTML test

Every route needs its own title, its own description, a canonical URL on your production domain, and one h1. In the App Router you set the first three through the Metadata API, with a static metadata export or a generateMetadata function for dynamic routes.

The canonical is the fix for the .vercel.app problem from earlier. Set metadataBase once in your root layout, and any relative canonical you write afterwards is joined to it, so every route points at your custom domain. Forget metadataBase and a relative URL in a metadata field causes a build error, which is at least loud.

// app/layout.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  metadataBase: new URL('https://yourdomain.com'),
  alternates: { canonical: '/' },
}

A canonical of '/' set in the root layout applies to every route below it, which is wrong for your blog posts. Override alternates.canonical in each page or in generateMetadata so the post at /blog/your-post declares itself, not the homepage. Ask v0 or Claude Code to list the canonical each route ends up with, then read that list yourself.

For structured data, the Next.js docs recommend rendering JSON-LD as a script tag in your layout or page component, and they warn that JSON.stringify does not sanitise malicious strings, so escape the less-than character in the output. Google adds one rule of its own: the markup must match the visible text on the page.

Then run a quick test for rendering problems.

curl -s https://yourdomain.com/blog/your-post | grep -i "<h1"

If your headline appears in the raw response, any crawler that skips JavaScript can read it. Google renders JavaScript, though its own guidance says SEO on a JavaScript framework is generally more complex than on other sites. We found no documentation stating that every AI crawler renders JavaScript, so treat finished HTML as the safe default.

Search Console: verify, submit, check the AI control

  1. Add your site as a property. A Domain property covers every protocol and subdomain and is verified through your domain provider's DNS. A URL-prefix property can be verified with an HTML tag in your homepage head or an HTML file at the root, as Google's verification guide explains.
  2. Open the Sitemaps report and submit your sitemap URL. You can also add a Sitemap: line to robots.txt, which the Next.js file above already does.
  3. Give it a few days. Data starts collecting when the property is added and takes a few days to accrue.
  4. Open Settings, then Search generative AI. The control rolled out to all sites on 31 August 2026 and defaults to including your site in AI Overviews, AI Mode, and generative AI features in Discover. Leave it on include unless you have a reason not to.
  5. Come back in a couple of weeks and open the generative AI performance report, which shows your impressions in those features. It stays hidden until your site has enough of them.

Google's guide to generative AI features is short on magic. A page must be indexed and eligible to show with a snippet, the site must be included in the Search Console control, and existing SEO practice does the rest. Google also lists llms.txt files and rewriting content just for AI systems among the things you can ignore for Google Search.

Bot rules on Vercel

Vercel's bot protection ruleset challenges non-browser traffic and excludes verified bots automatically. It is available on all plans and inactive by default, with log and challenge modes. It does not work with a reverse proxy such as Cloudflare in front of your deployment.

The AI bots ruleset covers crawlers that collect training data, build search indexes, or fetch pages on a user's behalf, and Vercel keeps the list current. It is also off by default, with log and deny modes. A blanket deny therefore shuts out the bots that would put you in ChatGPT and Claude answers, along with the ones that train on you. To block training bots only, write a custom firewall rule that matches those user agents. Start in log mode and read the traffic before you deny anything.

What to ask v0 or Claude Code to build

Paste these one at a time and read the diff. A site with less pages has less to go wrong, but it still needs every line below.

Add app/robots.ts that allows all crawlers on /, disallows only the paths I name,
and lists https://yourdomain.com/sitemap.xml as the sitemap.
Add app/sitemap.ts listing every public page with absolute URLs on my production
domain. Use each page's real last-updated date for lastModified. Leave out
changeFrequency and priority.
Give every route a unique title and meta description through the Metadata API,
a canonical URL on my production domain, and exactly one h1.
Add Article JSON-LD to blog posts. It must match the visible headline, author and
date, and it must escape the less-than character in the JSON output.
Search the project for any noindex meta tag or X-Robots-Tag header that applies to
production, and list every route affected.

The launch checklist

CheckHowPass looks like
Production is publicPrivate browser window on the production URLPage loads with no login
Deployment Protection scopeProject settings, Deployment ProtectionStandard Protection, or none on production
Previews stay outcurl -I on a preview URLx-robots-tag: noindex
Staging domain on a branchSame curl on the staging domainHeader present because you set it
robots.txtOpen /robots.txtLoads and names the sitemap
SitemapOpen /sitemap.xmlAbsolute canonical URLs, honest lastmod
Raw HTMLcurl and grep for the h1Headline in the response
Search ConsoleProperty list, Sitemaps reportVerified, sitemap read without errors
Generative AI controlSettings, Search generative AIInclude
Bot rulesetsFirewall rulesSet on purpose, log mode first

Launch day, in order

The checklist above tells you what to verify. The order below keeps you from verifying the wrong thing first.

  1. Point your custom domain at the production deployment and open it in a private window. If a login appears, fix Deployment Protection before anything else.
  2. Deploy robots.ts, sitemap.ts, and the canonical setup, then open /robots.txt and /sitemap.xml in the browser.
  3. Run the curl -I check on one preview URL and on any staging domain attached to a branch.
  4. Verify the property in Search Console. If you used a Domain property, expect to wait on DNS.
  5. Submit the sitemap, then leave it for a few days. The submission is only a hint, so Google decides the timing.
  6. Decide on the bot rules. Switch the rulesets to log mode and read a week of traffic before choosing to deny anything.

What to look at a week later

Open the Sitemaps report and confirm Google read the file without errors. Then check the list of pages Search Console knows about against your real page count. A large gap is worth chasing. Look for a noindex you forgot about, a canonical pointing somewhere unexpected, or a route the sitemap never listed. Once the generative AI performance report has enough impressions to show, it tells you whether AI Overviews and AI Mode are surfacing your site at all, and that report is the nearest thing Google offers to a scoreboard for AI visibility.

The launch checks make your pages eligible to appear, and that is all they do. In our own Search Console data, the topic area this guide covers had close to zero clicks for Contengi when we looked, with only a few CMS queries showing up at all. Eligibility comes first, then the writing.

Where the blog posts live

None of the above gives you somewhere to write. Our guides on a CMS for Vercel websites and on choosing a CMS for a v0 website cover the options, from markdown in the repo to a headless CMS. Contengi connects to v0 sites through a signed route handler that v0 adds from a single prompt, so posts written and reviewed there publish back to the same URLs. For the on-page side, our guide to an agent-friendly website covers semantic HTML and schema.

Frequently asked questions

Is Vercel good for SEO?

Vercel gives you the hosting and some useful defaults, such as a noindex header on previews and outdated production deployments. Whether your pages rank depends on what your Next.js app serves: titles, canonical URLs, a sitemap, and content worth ranking. We did not test Vercel's hosting speed for this guide, so we make no claim about it.

Will Google index my Vercel preview deployments?

By default no. Vercel adds an X-Robots-Tag noindex header to preview deployments and to outdated production deployments. The exception is a custom domain assigned to a non-production branch, which gets no automatic header, so add one yourself in next.config.js using VERCEL_ENV.

Why does my vercel.app URL show up in Google next to my custom domain?

Vercel's knowledge base says the production .vercel.app URL can be indexed alongside your custom domain. Set metadataBase in your root layout and give each page a canonical on your custom domain, so search engines know which address to keep.

Does noindex keep a preview deployment private?

No. The header is a request to search engines, and anyone with the URL can still open the page. If a preview needs to stay private, use Deployment Protection, which requires authentication for every request.

Do I need to submit a sitemap?

Google treats a submitted sitemap as a hint and gives no guarantee it will use it. Next.js builds one from a single app/sitemap.ts file, so it takes a few minutes, and the Sitemaps report in Search Console shows whether Google could read it.

Do I need an llms.txt file on Vercel?

Google's guide to generative AI features lists llms.txt among the things you can ignore for Google Search. The OpenAI, Perplexity and Google crawler pages we read do not say the file affects crawling or citation. Keep one if another tool you use asks for it.

How do I see whether AI Overviews mention my site?

Open the generative AI performance report in Search Console. It covers AI Overviews and AI Mode, and it stays hidden until your site has enough impressions to report.

Content like this, in your own voice

The workflows behind this blog run inside Contengi. A strategist sets up your knowledge base, and the agents write to your rules.