page-layer
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.
When & Why to Use This Skill
This Claude skill streamlines Next.js 15 development by providing expert guidance and ready-to-use templates for the App Router architecture. It automates the creation of pages, layouts, and dynamic routes while ensuring full compatibility with Next.js 15 breaking changes, such as asynchronous parameters. By integrating SEO best practices directly into the workflow, it helps developers implement robust metadata and OpenGraph configurations efficiently.
Use Cases
- Rapidly scaffolding new website structures, including nested layouts and page components within the Next.js app directory.
- Implementing complex dynamic routing for blogs or e-commerce sites with correct TypeScript typing for asynchronous route parameters.
- Enhancing search engine visibility and social media presence by generating static and dynamic metadata, including OpenGraph tags.
- Building resilient user interfaces by correctly setting up loading states, error boundaries, and custom 404 pages using Next.js specialized files.
- Migrating existing projects to Next.js 15 by following updated patterns for params and searchParams handling.
| name | page-layer |
|---|---|
| description | "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." |
Page Layer Skill
Scope
app/**/page.tsx- Next.js page componentsapp/**/layout.tsx- Layout componentsapp/**/not-found.tsx- Not found pagesapp/**/loading.tsx- Loading statesapp/**/error.tsx- Error boundaries
Decision Tree
Creating a new page?
- Determine route: Map URL to folder structure in
app/ - Create folder:
app/[route-name]/ - Create
page.tsx: Export default async function - Add metadata: Export
metadataobject orgenerateMetadatafunction - Use layout components: Container, PageIntro, etc.
Creating a dynamic route?
- Create folder with brackets:
app/[param]/orapp/[...slug]/ - Type params as Promise:
params: Promise<{ param: string }> - Await params:
const { param } = await params; - Add
generateStaticParams: For static generation - Add
generateMetadata: For dynamic meta tags
Adding page metadata?
- Static metadata: Export
metadataobject - Dynamic metadata: Export async
generateMetadatafunction - Include OpenGraph: title, description, images, type
- Use env variables:
env.PROJECT_BASE_TITLE, etc.
Adding a layout?
- Create
layout.tsxin route folder - Accept
childrenprop - Wrap with structural components
- Export metadata if needed (inherited by child pages)
Quick Templates
Basic Page
import 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>
);
}
Dynamic Route Page (Next.js 15)
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>
);
}
Layout
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>
);
}
Not Found Page
import { NotFound } from "@/components/shared/not-found";
export default function NotFoundPage() {
return <NotFound message="Page not found" />;
}
Metadata with OpenGraph
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,
},
};
Mistakes
- ❌ Missing
await paramsin Next.js 15 (params is now a Promise) - ❌
"use client"on pages (should be server components) - ❌ Missing
generateStaticParamsfor dynamic routes (breaks static export) - ❌ Not calling
notFound()for missing data - ❌ Hardcoding URLs instead of using route config
- ❌ Missing metadata/OpenGraph tags
Validation
After changes, run:
.claude/skills/page-layer/scripts/validate-page-patterns.sh <file>
pnpm build # Full build validates routes
pnpm typecheck # TypeScript validation
Route Structure
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
Next.js 15 Breaking Changes
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;
}