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.
LLaunchScaler·Published ·8 min read
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
Questions, answered
What people ask about this
01
Is 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.
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.',
}
A relative URL in a metadata field without a metadataBase causes a build error, so a missing base shows up immediately.
A title.template needs a title.default; child routes that set title: 'Pricing' render Pricing | YourApp.
An absolute URL in a field ignores metadataBase, which is how a hard-coded preview URL slips into production canonicals. Search the project for vercel.app before launch.
When generateMetadata fetches data, Next.js can stream the metadata: for browsers it is appended to the <body> once resolved, while HTML-limited bots such as facebookexternalhit get 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-image and twitter-image accept .jpg, .jpeg, .png and .gif, and Next.js adds og:image, og:image:type, og:image:width and og:image:height.
An opengraph-image.alt.txt file beside the image sets its alt text.
The build fails if a twitter-image exceeds 5 MB or an opengraph-image exceeds 8 MB.
You can generate the image with code instead: an opengraph-image.tsx that returns an image, cached and generated at build time unless it uses request-time data.
favicon.ico can only live in the root app/ folder; icon accepts .ico, .jpg, .jpeg, .png and .svg; apple-icon accepts .jpg, .jpeg and .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 in layout.tsx or page.tsx. Its guide warns that JSON.stringify does not sanitize malicious strings, and its example replaces every < with \u003c before writing the payload. Use a schema.org type that matches the page, such as Organization on the homepage or Product on 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 a Suspense boundary, the response has already started streaming as a 200, so the noindex is 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, or redirects() with permanent: true in next.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 noindex to 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.com pointed 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 useEffect and rendered when it arrives. The server HTML contains the loading state, not the data.
Content shown only after a mounted flag flips in an effect, a pattern sometimes used to silence hydration warnings.
Text rendered from window or localStorage values.
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.
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.
03
What 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.
04
Do 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.
05
How 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.