This skill should be used when the user asks to 'create a page', 'add a route', 'create a layout', 'add metadata', or 'set up a dynamic route'. Provides guidance for Next.js 15 App Router pages, layouts, and route handlers in app/**/*.tsx.
app/**/page.tsx - Next.js page componentsapp/**/layout.tsx - Layout componentsapp/**/not-found.tsx - Not found pagesapp/**/loading.tsx - Loading statesapp/**/error.tsx - Error boundariesapp/app/[route-name]/page.tsx: Export default async functionmetadata object or generateMetadata functionapp/[param]/ or app/[...slug]/params: Promise<{ param: string }>const { param } = await params;generateStaticParams: For static generationgenerateMetadata: For dynamic meta tagsmetadata objectgenerateMetadata functionenv.PROJECT_BASE_TITLE, etc.layout.tsx in route folderchildren propimport type { Metadata } from "next";
import { Container } from "@/components/layout/container";
import { PageIntro } from "@/components/layout/page-intro";
export const metadata: Metadata = {
title: "Page Title",
description: "Page description for SEO",
};
export default function PageName() {
return (
<Container className="mt-16">
<PageIntro title="Page Title">
<p>Page content description</p>
</PageIntro>
{/* Page content */}
</Container>
);
}
import type { Metadata } from "next";
import { notFound } from "next/navigation";
type Props = {
params: Promise<{ slug: string }>;
};
export async function generateStaticParams() {
// Return array of param objects for static generation
return [{ slug: "example-1" }, { slug: "example-2" }];
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
// Fetch data and return metadata
return {
title: `Dynamic Title for ${slug}`,
description: "Dynamic description",
};
}
export default async function Page({ params }: Props) {
const { slug } = await params;
// Fetch data
const data = getData(slug);
if (!data) {
notFound();
}
return (
<div>
<h1>{data.title}</h1>
</div>
);
}
import type { Metadata } from "next";
export const metadata: Metadata = {
title: {
template: "%s | Section Name",
default: "Section Name",
},
};
export default function SectionLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="section-wrapper">
{children}
</div>
);
}
import { NotFound } from "@/components/shared/not-found";
export default function NotFoundPage() {
return <NotFound message="Page not found" />;
}
import type { Metadata } from "next";
import { getCldImageUrl } from "next-cloudinary";
import { env } from "@/lib/config/env";
import { withCloudinaryCloudName } from "@/lib/utils/withCloudinaryCloudName";
const ogImageUrl = getCldImageUrl({
width: 1200,
height: 630,
src: withCloudinaryCloudName("path/to/image"),
});
export const metadata: Metadata = {
title: "Page Title",
description: "Page description",
openGraph: {
title: "Page Title",
description: "Page description",
url: "/page-path",
images: [ogImageUrl],
type: "website",
locale: "en_GB",
siteName: env.PROJECT_BASE_TITLE,
},
};
await params in Next.js 15 (params is now a Promise)"use client" on pages (should be server components)generateStaticParams for dynamic routes (breaks static export)notFound() for missing dataAfter changes, run:
.claude/skills/page-layer/scripts/validate-page-patterns.sh <file>
pnpm build # Full build validates routes
pnpm typecheck # TypeScript validation
app/
├── layout.tsx # Root layout (required)
├── page.tsx # Home page (/)
├── not-found.tsx # Global 404
├── about/
│ └── page.tsx # /about
├── articles/
│ ├── page.tsx # /articles
│ └── [slug]/
│ ├── page.tsx # /articles/[slug]
│ └── not-found.tsx # Article 404
├── contact/
│ └── page.tsx # /contact
└── projects/
└── page.tsx # /projects
Params are now Promises:
// Next.js 14 (old)
export default function Page({ params }: { params: { slug: string } }) {
const { slug } = params;
}
// Next.js 15 (current)
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
}