| 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;
}