V0 uses Next.js App Router where routes are defined by the folder structure under /app. To add a new static page, create a directory with a page.tsx file. For dynamic routes like /blog/my-post, use bracket notation like [slug]/page.tsx. V0's AI can generate new pages via prompts, but you can also create them manually in the code editor. Each route can have its own layout.tsx, loading.tsx, and error.tsx for shared UI, loading states, and error handling.
Why adding pages requires understanding App Router file conventions
V0 generates Next.js App Router projects where routing is entirely file-based — there is no routes configuration file. Each folder under /app that contains a page.tsx becomes a route. Dynamic segments use bracket notation like [id] or [slug], and catch-all routes use [...slug]. This differs from adding individual pages because you need to understand the file conventions to create routes that work with layouts, loading states, and metadata. V0's AI can add pages via prompts, but it sometimes creates them in the wrong location or misses the page.tsx file convention, resulting in 404 errors.
- New directory created under /app but missing the page.tsx file, so the route returns 404
- Dynamic route uses incorrect bracket notation (parentheses or curly braces instead of square brackets)
- Page component is not the default export, which Next.js App Router requires
- Route group folders using (groupName) syntax are confused with dynamic route folders
- V0 generates a page inside the wrong directory level in the /app folder structure
Error messages you might see
404: This page could not be found.The route does not have a page.tsx file in the correct directory. Check that the file exists at the exact path matching the URL: /app/about/page.tsx for the /about route.
Error: Page "/products/[id]" is missing "generateStaticParams()" so it cannot be used with "output: export".When using static export mode, every dynamic route needs a generateStaticParams function that returns all valid parameter values at build time.
Error: The default export is not a React Component in page: "/app/about/page"Next.js App Router requires the page.tsx file to have a default export that is a React component. Named exports are not recognized as the page component.
Before you start
- A V0 project where you need to add new pages or dynamic routes
- Access to the V0 code editor to create files and directories
- Understanding of URL structure and how it maps to file paths
How to fix it
Create a new static page by adding a directory with page.tsx
In Next.js App Router, each route corresponds to a directory under /app that contains a page.tsx file. The directory name becomes the URL segment.
Create a new static page by adding a directory with page.tsx
In Next.js App Router, each route corresponds to a directory under /app that contains a page.tsx file. The directory name becomes the URL segment.
In the V0 file explorer, right-click on the /app directory and create a new folder (e.g., 'about'). Inside that folder, create a page.tsx file with a default-exported React component.
// No /about route exists// app/// page.tsx (home page only)// app/about/page.tsx — creates the /about routeexport default function AboutPage() { return ( <div className="max-w-4xl mx-auto px-6 py-16"> <h1 className="text-3xl font-bold">About Us</h1> <p className="mt-4 text-muted-foreground"> Learn more about our team and mission. </p> </div> );}Expected result: Navigating to /about displays the new About page.
Add a dynamic route with bracket notation for parameters
Dynamic routes like /blog/my-first-post use a directory named with square brackets [slug] to capture the URL parameter. The parameter value is available as a prop in the page component.
Add a dynamic route with bracket notation for parameters
Dynamic routes like /blog/my-first-post use a directory named with square brackets [slug] to capture the URL parameter. The parameter value is available as a prop in the page component.
Create a directory with square brackets in the name (e.g., [slug]) inside the parent route directory. Add a page.tsx that receives the params prop.
// Only a static /blog page exists// app/blog/page.tsx// app/blog/[slug]/page.tsx — creates /blog/:slug routesexport default async function BlogPost({ params,}: { params: Promise<{ slug: string }>;}) { const { slug } = await params; return ( <article className="max-w-3xl mx-auto px-6 py-16"> <h1 className="text-3xl font-bold capitalize"> {slug.replace(/-/g, " ")} </h1> </article> );}Expected result: Navigating to /blog/my-first-post displays the page with 'my first post' as the heading.
Add metadata and loading states to new pages
Each route in App Router can export a metadata object for SEO and a loading.tsx file for skeleton screens during data fetching. Adding these improves user experience and search engine visibility.
Add metadata and loading states to new pages
Each route in App Router can export a metadata object for SEO and a loading.tsx file for skeleton screens during data fetching. Adding these improves user experience and search engine visibility.
Export a metadata object from your page.tsx for static metadata, or use generateMetadata for dynamic metadata based on route parameters. Create a loading.tsx file in the same directory for a loading skeleton.
// app/blog/[slug]/page.tsx — no metadata or loading stateexport default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; return <h1>{slug}</h1>;}// app/blog/[slug]/page.tsx — with dynamic metadataimport type { Metadata } from "next";export async function generateMetadata({ params,}: { params: Promise<{ slug: string }>;}): Promise<Metadata> { const { slug } = await params; return { title: slug.replace(/-/g, " "), description: `Read our blog post about ${slug.replace(/-/g, " ")}`, };}export default async function BlogPost({ params,}: { params: Promise<{ slug: string }>;}) { const { slug } = await params; return ( <article className="max-w-3xl mx-auto px-6 py-16"> <h1 className="text-3xl font-bold capitalize"> {slug.replace(/-/g, " ")} </h1> </article> );}Expected result: The browser tab shows the dynamic page title, and a loading skeleton displays while data is fetched.
Create a catch-all route for flexible URL patterns
Catch-all routes use [...slug] syntax to match multiple URL segments. This is useful for documentation pages, nested category structures, or any URL pattern with variable depth.
Create a catch-all route for flexible URL patterns
Catch-all routes use [...slug] syntax to match multiple URL segments. This is useful for documentation pages, nested category structures, or any URL pattern with variable depth.
Create a directory named [...slug] and access the segments as an array in the page component. Use optional catch-all [[...slug]] to also match the parent route.
// Only matches /docs — not /docs/getting-started or /docs/api/auth// app/docs/page.tsx// app/docs/[...slug]/page.tsx — matches /docs/anything/at/any/depthexport default async function DocsPage({ params,}: { params: Promise<{ slug: string[] }>;}) { const { slug } = await params; const path = slug.join("/"); return ( <div className="max-w-4xl mx-auto px-6 py-16"> <p className="text-sm text-muted-foreground">docs/{path}</p> <h1 className="text-3xl font-bold mt-2"> {slug[slug.length - 1].replace(/-/g, " ")} </h1> </div> );}Expected result: URLs like /docs/getting-started, /docs/api/auth, and /docs/guides/deployment all render correctly.
Complete code example
1import type { Metadata } from "next";2import { notFound } from "next/navigation";3import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";4import { Badge } from "@/components/ui/badge";56interface Product {7 id: string;8 name: string;9 description: string;10 price: number;11 category: string;12}1314async function getProduct(id: string): Promise<Product | null> {15 try {16 const res = await fetch(`${process.env.API_URL}/products/${id}`);17 if (!res.ok) return null;18 return res.json();19 } catch {20 return null;21 }22}2324export async function generateMetadata({25 params,26}: {27 params: Promise<{ id: string }>;28}): Promise<Metadata> {29 const { id } = await params;30 const product = await getProduct(id);31 return {32 title: product?.name ?? "Product Not Found",33 description: product?.description ?? "",34 };35}3637export default async function ProductPage({38 params,39}: {40 params: Promise<{ id: string }>;41}) {42 const { id } = await params;43 const product = await getProduct(id);4445 if (!product) notFound();4647 return (48 <div className="max-w-3xl mx-auto px-6 py-16">49 <Card>50 <CardHeader>51 <div className="flex items-center justify-between">52 <CardTitle>{product.name}</CardTitle>53 <Badge>{product.category}</Badge>54 </div>55 </CardHeader>56 <CardContent>57 <p className="text-muted-foreground">{product.description}</p>58 <p className="text-2xl font-bold mt-4">${product.price}</p>59 </CardContent>60 </Card>61 </div>62 );63}Best practices to prevent this
- Always use default exports for page components — Next.js App Router does not recognize named exports as pages
- Name dynamic route directories with square brackets like [id] or [slug] — not curly braces or parentheses
- Use route groups with parentheses (marketing) for organizational folders that should not create URL segments
- Add generateMetadata to every page for proper SEO — V0 often generates pages without metadata
- Create loading.tsx alongside page.tsx for any route that fetches data to show a skeleton during loading
- Call notFound() from the page component when a dynamic route parameter does not match any data
- For complex routing structures, consider having RapidDev architect the route hierarchy before generating pages with V0
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I need to add a dynamic product detail page at /products/[id] in my V0 Next.js App Router project. It should fetch product data, show a 404 if the product is not found, and have dynamic metadata for SEO. How do I set up the file structure?
Frequently asked questions
How do I add a new page to my V0 project?
Create a new directory under /app with the route name and add a page.tsx file inside it. For example, to create /about, add app/about/page.tsx with a default-exported React component. The page appears immediately in the V0 preview.
How do dynamic routes work in V0 Next.js projects?
Create a directory with square brackets like [id] or [slug] under the parent route. The bracket name becomes a parameter available in the page component via the params prop. For example, app/products/[id]/page.tsx handles /products/1, /products/2, etc.
What is the difference between [slug] and [...slug] in App Router?
Single brackets [slug] match one URL segment (/blog/my-post). Spread brackets [...slug] match multiple segments at any depth (/docs/api/auth/session). Optional catch-all [[...slug]] also matches the parent route (/docs).
Why does my new V0 page return a 404?
The most common cause is a missing page.tsx file. The directory exists but does not contain a page.tsx with a default export. Check the file name spelling and ensure the component is exported as default, not as a named export.
Can I ask V0 to generate new pages automatically?
Yes. Prompt V0 with specific instructions like 'Create a /pricing page with a three-tier pricing table using shadcn/ui Card components.' V0 creates the correct file structure and component. Review the result to ensure the file is in the right directory.
Can RapidDev help architect the routing structure for my V0 app?
Yes. RapidDev can design the complete route hierarchy including dynamic routes, route groups, parallel routes, and intercepting routes — then implement them in your V0 project with proper layouts, loading states, and error handling.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your issue.
Book a free consultation