GuideTechnical SEO
v0 app SEO: the Next.js metadata v0 leaves out
v0 builds Next.js apps, so SEO comes from the metadata API. The files to add: metadataBase, generateMetadata, sitemap.ts, robots.ts, OG image, icons.

A v0 app is a Next.js App Router project, so its SEO comes from Next.js's metadata API: a metadataBase and title template in the root layout, generateMetadata on each route, app/sitemap.ts, app/robots.ts, an opengraph-image and icon files. v0 gives you pages that render on the server; these files are what you check for and add, then you make sure only the production deployment gets shared, because Vercel marks previews noindex.
v0's docs say new chats start with a Next.js app using shadcn/ui and Tailwind CSS, and that full-stack v0 apps use React Server Components and App Router conventions. So everything below is standard Next.js, and you can ask v0 to write each file in the chat, then compare what it wrote with the examples here.
What does a v0 project need for SEO?
Seven files and settings cover almost everything a search engine or link preview reads. Each is a Next.js file convention or a metadata field, and each has one job. Check your project for all seven; the table says where each lives and what it produces.
| What | Where | What it produces |
|---|---|---|
metadataBase, default title and template, default description | app/layout.tsx | Absolute URLs for every metadata field; a title on every page |
| Per-route title, description, canonical | metadata or generateMetadata in each page.tsx | Distinct <title>, description and <link rel="canonical"> per URL |
| Sitemap | app/sitemap.ts | /sitemap.xml |
| Robots rules | app/robots.ts | /robots.txt |
| Social preview image | app/opengraph-image.png (or .tsx), optionally per route | og:image with its type, width and height |
| Icons | app/favicon.ico, app/icon.png or .svg, app/apple-icon.png | <link rel="icon"> and apple-touch-icon tags |
| Production domain | Vercel project settings | The one URL that can be indexed |
How do you set metadataBase and per-route metadata?
Set metadataBase once in the root layout, with a title template and a default, then give each page its own metadata export, or generateMetadata when the title depends on data. Next.js composes relative paths against metadataBase, so canonicals and image URLs come out absolute.
// app/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
metadataBase: new URL('https://www.yourdomain.com'),
title: { template: '%s | YourApp', default: 'YourApp' },
description: 'What YourApp does, in one sentence a searcher would recognise.',
}
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
export async function generateMetadata(
{ params }: { params: Promise<{ slug: string }> }
): Promise<Metadata> {
const { slug } = await params
const post = await getPost(slug)
return {
title: post.title,
description: post.summary,
alternates: { canonical: `/blog/${slug}` },
}
}
Rules from the Next.js docs that matter here:
- A relative URL in a metadata field without a
metadataBasecauses a build error, so a missing base shows up immediately. - A
title.templateneeds atitle.default; child routes that settitle: 'Pricing'renderPricing | YourApp. - An absolute URL in a field ignores
metadataBase, which is how a hard-coded preview URL slips into production canonicals. Search the project forvercel.appbefore launch. - When
generateMetadatafetches data, Next.js can stream the metadata: for browsers it is appended to the<body>once resolved, while HTML-limited bots such asfacebookexternalhitget it in the<head>. Next.js says it has verified bots that run JavaScript, such as Googlebot, interpret streamed metadata correctly.
That last point changes how you test. curl without a bot user agent may show your title near the end of the page rather than in the <head>. That is streaming, not a bug.
How do you add sitemap.ts and robots.ts?
Create both in the app directory. app/sitemap.ts returns an array of entries and Next.js serves it as /sitemap.xml; app/robots.ts returns rules and the sitemap URL, served as /robots.txt. Both are special route handlers that Next.js caches by default unless they use request-time data.
// app/sitemap.ts
import type { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getAllPosts()
return [
{ url: 'https://www.yourdomain.com', lastModified: new Date() },
{ url: 'https://www.yourdomain.com/pricing', lastModified: new Date() },
...posts.map((p) => ({
url: `https://www.yourdomain.com/blog/${p.slug}`,
lastModified: p.updatedAt,
})),
]
}
// app/robots.ts
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: '*', allow: '/', disallow: '/dashboard/' },
sitemap: 'https://www.yourdomain.com/sitemap.xml',
}
}
Use your production domain in both, never a vercel.app address. Each sitemap entry can also carry alternates.languages if you publish more than one language. Once deployed, submit /sitemap.xml in Search Console.
How do the opengraph-image and icon files work?
Drop an image file with the conventional name into a route folder and Next.js writes the tags for you. An opengraph-image.png in app/ sets the social preview image for the whole site; one inside app/pricing/ overrides it for that route. The same pattern works for twitter-image, and favicon.ico, icon and apple-icon in app/ set your icons.
What the Next.js docs specify:
opengraph-imageandtwitter-imageaccept.jpg,.jpeg,.pngand.gif, and Next.js addsog:image,og:image:type,og:image:widthandog:image:height.- An
opengraph-image.alt.txtfile beside the image sets its alt text. - The build fails if a
twitter-imageexceeds 5 MB or anopengraph-imageexceeds 8 MB. - You can generate the image with code instead: an
opengraph-image.tsxthat returns an image, cached and generated at build time unless it uses request-time data. favicon.icocan only live in the rootapp/folder;iconaccepts.ico,.jpg,.jpeg,.pngand.svg;apple-iconaccepts.jpg,.jpegand.png.
These tags resolve against metadataBase, which is the usual reason a v0 app's preview image breaks when shared: the image URL points at a preview deployment or is relative. The Next.js Open Graph image guide covers those failures in detail.
What else belongs on a Next.js SEO checklist for a v0 app?
Three items sit outside the metadata files and are easy to miss in generated code: structured data, missing-page handling and moved-page redirects. Each has a documented Next.js pattern, so ask v0 to follow it rather than inventing its own.
- Structured data. Next.js recommends rendering JSON-LD as a
<script type="application/ld+json">tag inlayout.tsxorpage.tsx. Its guide warns thatJSON.stringifydoes not sanitize malicious strings, and its example replaces every<with\u003cbefore writing the payload. Use a schema.org type that matches the page, such asOrganizationon the homepage orProducton a product page. - Missing pages. Call
notFound()when a record doesn't exist. Next.js renders the 404 page and injects<meta name="robots" content="noindex">. The docs flag a trade-off: if the check runs inside aSuspenseboundary, the response has already started streaming as a 200, so thenoindexis what keeps the soft 404 out of search results. For a real 404 status, check the record before the response streams. - Moved pages. Use
permanentRedirect()(a 308) in server code, orredirects()withpermanent: trueinnext.config.js, when a route's URL changes.redirect()alone returns a 307, which search engines treat as temporary.
Finally, search the project for a robots field in any metadata export. Next.js turns robots: { index: false } into <meta name="robots" content="noindex">, and the field can be set in a layout, where it silently applies to every route below it. A robots field is only needed when a page must stay out of search; public pages can leave it out entirely, because indexing is the default.
Why aren't v0 preview deployments indexed?
Because Vercel tells search engines not to index them. Vercel says every deployment from a non-production branch carries X-Robots-Tag: noindex by default, and v0 publishes through Vercel, with preview deployments built from working branches and production deployments serving your domains. Only production can rank.
Three consequences:
- Share and link only the production URL. A preview link in a launch post sends people, and crawlers, to a page marked
noindex. - When you promote a new production deployment, Vercel adds
noindexto the previous one, so old production URLs drop out on their own. - The exception runs the other way: Vercel omits the header when a custom domain is assigned to a non-production branch. A
staging.yourdomain.compointed at a branch can be indexed unless you add the header yourself.
Check any URL with curl -sI https://staging.yourdomain.com | grep -i x-robots-tag. The reverse mistake, a noindex that follows you into production, is covered in the guide to noindex shipped to production.
To use your own domain, v0's docs point to Publish, then Customize Domain (or the project menu, Settings, Domains), where you can pick an available vercel.app domain or configure your own through the Vercel dashboard.
Do Client Components hurt a v0 app's SEO?
Not by default. In the App Router, Next.js uses Client Components together with the Server Component payload to prerender HTML on the server, then hydrates it in the browser. A 'use client' file's markup is in the HTML crawlers receive. What isn't is anything that only appears after an effect runs.
The patterns to look for in a v0-generated component:
- Data fetched inside
useEffectand rendered when it arrives. The server HTML contains the loading state, not the data. - Content shown only after a
mountedflag flips in an effect, a pattern sometimes used to silence hydration warnings. - Text rendered from
windoworlocalStoragevalues.
For public pages, move that data fetching into a Server Component (an async page or component) and pass the result down as props. This matters most for AI crawlers: Vercel found that OpenAI's crawlers, ClaudeBot and PerplexityBot don't execute JavaScript, so effect-loaded content never reaches them.
Test each public route, including at least one dynamic route such as a blog post, with curl -s https://www.yourdomain.com/pricing | grep -i "a sentence from the page". If the sentence is missing, find the component that loads it on the client. The same checks apply to other AI-built projects; the AI-built app indexing guide covers Lovable, Bolt and Cursor alongside v0.
To check a production v0 app from outside, run the free scan on LaunchScaler. It needs only your URL and no account, and its free checks read your title, description, canonical, robots.txt, sitemap and any noindex header or tag, check that canonicals and sitemap entries resolve to 200 without redirecting, and compare the raw HTML with a real browser render to flag content that only appears after JavaScript runs, among 156 checks across 6 of its 7 categories.
Questions, answered
What people ask about this
01Is a v0 app good for SEO?
- It starts from a good base: v0's docs say new chats start with a Next.js app, which renders pages on the server. What you still need to add is the metadata: titles, descriptions, canonicals, a sitemap, robots.txt, Open Graph images and icons.
02Why isn't my v0 preview showing up on Google?
- Vercel adds X-Robots-Tag: noindex to every deployment from a non-production branch, so previews are never indexed. Only the production deployment on your domain can rank; publish to production and share that URL.
03What is metadataBase in Next.js?
- It is the base URL, set in the root layout, that Next.js uses to turn relative paths in metadata into absolute URLs, for canonicals, Open Graph images and hreflang links. Using a relative URL in metadata without it causes a build error.
04Do Client Components hurt SEO in a v0 app?
- Not by themselves. In the Next.js App Router, Client Components are prerendered to HTML on the server. Content loaded inside useEffect is the exception: it only appears after JavaScript runs, so crawlers that skip JavaScript never see it.
05How do I add a sitemap to a v0 project?
- Add app/sitemap.ts exporting a function that returns an array of URLs, and app/robots.ts returning your rules and the sitemap URL. Next.js serves them at /sitemap.xml and /robots.txt. You can ask v0 in the chat to create both.
Sources
- v0 docs: Design Systems (starter app)v0.app
- v0 docs: Full-stack appsv0.app
- v0 docs: Deploymentsv0.app
- v0 docs: Custom domainv0.app
- Next.js: generateMetadata and the Metadata objectnextjs.org
- Next.js: How to implement JSON-LDnextjs.org
- Next.js: notFoundnextjs.org
- Next.js: redirect functionnextjs.org
- Next.js: sitemap.xml file conventionnextjs.org
- Next.js: robots.txt file conventionnextjs.org
- Next.js: opengraph-image and twitter-imagenextjs.org
- Next.js: favicon, icon, and apple-iconnextjs.org
- Next.js: Server and Client Componentsnextjs.org
- Vercel: Are Vercel preview deployments indexed by search engines?vercel.com
- Vercel: The rise of the AI crawlervercel.com


