Complete Next.js App Router fundamentals system. PROACTIVELY activate for: (1) App directory structure and conventions, (2) Layouts and templates, (3) Loading and error boundaries, (4) Route groups and organization, (5) Parallel routes setup, (6) Metadata and SEO, (7) Server vs Client Components, (8) Navigation patterns. Provides: File conventions, layout nesting, streaming UI, error handling, metadata templates. Ensures correct App Router architecture with proper component boundaries.
/plugin marketplace add JosiahSiegel/claude-plugin-marketplace/plugin install nextjs-master@claude-plugin-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
| Convention | File | Purpose |
|---|---|---|
| Page | page.tsx | Route UI (required for route) |
| Layout | layout.tsx | Shared UI (persists across navigations) |
| Loading | loading.tsx | Loading UI with Suspense |
| Error | error.tsx | Error boundary (must be 'use client') |
| Not Found | not-found.tsx | 404 UI |
| Template | template.tsx | Re-renders on navigation |
| Pattern | Syntax | Example |
|---|---|---|
| Dynamic Route | [slug] | /blog/[slug]/page.tsx |
| Catch-all | [...slug] | /docs/[...slug]/page.tsx |
| Optional Catch-all | [[...slug]] | /shop/[[...slug]]/page.tsx |
| Route Group | (name) | /(marketing)/about/page.tsx |
| Parallel Route | @slot | /@analytics/page.tsx |
Use for App Router fundamentals:
Related skills:
nextjs-data-fetchingnextjs-routing-advancednextjs-cachingapp/
├── layout.tsx # Root layout (required)
├── page.tsx # Home page (/)
├── loading.tsx # Loading UI
├── error.tsx # Error boundary
├── not-found.tsx # 404 page
├── global-error.tsx # Global error boundary
├── template.tsx # Re-renders on navigation
│
├── (marketing)/ # Route group (no URL impact)
│ ├── about/
│ │ └── page.tsx # /about
│ └── contact/
│ └── page.tsx # /contact
│
├── dashboard/
│ ├── layout.tsx # Dashboard layout
│ ├── page.tsx # /dashboard
│ ├── loading.tsx # Dashboard loading
│ ├── @analytics/ # Parallel route
│ │ └── page.tsx
│ ├── @team/ # Parallel route
│ │ └── page.tsx
│ └── settings/
│ └── page.tsx # /dashboard/settings
│
├── blog/
│ ├── page.tsx # /blog
│ └── [slug]/ # Dynamic route
│ ├── page.tsx # /blog/:slug
│ └── opengraph-image.tsx
│
├── products/
│ └── [...slug]/ # Catch-all route
│ └── page.tsx # /products/*
│
├── shop/
│ └── [[...slug]]/ # Optional catch-all
│ └── page.tsx # /shop or /shop/*
│
└── api/
└── users/
└── route.ts # API route handler
// app/layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: {
template: '%s | My App',
default: 'My App',
},
description: 'My application description',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
<header>
<nav>{/* Navigation */}</nav>
</header>
<main>{children}</main>
<footer>{/* Footer */}</footer>
</body>
</html>
);
}
// app/dashboard/layout.tsx
import { Sidebar } from '@/components/sidebar';
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard">
<Sidebar />
<div className="dashboard-content">{children}</div>
</div>
);
}
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<div className="dashboard">
<div className="main">{children}</div>
<div className="sidebar">
{analytics}
{team}
</div>
</div>
);
}
// app/page.tsx
export default function HomePage() {
return (
<div>
<h1>Welcome to My App</h1>
<p>This is the home page.</p>
</div>
);
}
// app/posts/page.tsx
import { db } from '@/lib/db';
async function getPosts() {
return db.posts.findMany({
orderBy: { createdAt: 'desc' },
});
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<div>
<h1>Blog Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<a href={`/posts/${post.slug}`}>{post.title}</a>
</li>
))}
</ul>
</div>
);
}
// app/posts/[slug]/page.tsx
import { notFound } from 'next/navigation';
import { db } from '@/lib/db';
interface PageProps {
params: Promise<{ slug: string }>;
}
export async function generateMetadata({ params }: PageProps) {
const { slug } = await params;
const post = await db.posts.findUnique({ where: { slug } });
if (!post) {
return { title: 'Post Not Found' };
}
return {
title: post.title,
description: post.excerpt,
};
}
export async function generateStaticParams() {
const posts = await db.posts.findMany({ select: { slug: true } });
return posts.map((post) => ({ slug: post.slug }));
}
export default async function PostPage({ params }: PageProps) {
const { slug } = await params;
const post = await db.posts.findUnique({ where: { slug } });
if (!post) {
notFound();
}
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// app/dashboard/loading.tsx
export default function Loading() {
return (
<div className="loading-container">
<div className="spinner" />
<p>Loading dashboard...</p>
</div>
);
}
// app/posts/loading.tsx
export default function PostsLoading() {
return (
<div className="posts-skeleton">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="post-skeleton">
<div className="skeleton-title" />
<div className="skeleton-excerpt" />
</div>
))}
</div>
);
}
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { Analytics } from './analytics';
import { RecentSales } from './recent-sales';
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<AnalyticsSkeleton />}>
<Analytics />
</Suspense>
<Suspense fallback={<SalesSkeleton />}>
<RecentSales />
</Suspense>
</div>
);
}
// app/dashboard/error.tsx
'use client';
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div className="error-container">
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
// app/global-error.tsx
'use client';
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
);
}
// app/not-found.tsx
import Link from 'next/link';
export default function NotFound() {
return (
<div className="not-found">
<h2>Page Not Found</h2>
<p>Could not find the requested resource.</p>
<Link href="/">Return Home</Link>
</div>
);
}
// app/posts/[slug]/page.tsx
import { notFound } from 'next/navigation';
export default async function PostPage({ params }: PageProps) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) {
notFound();
}
return <article>{/* ... */}</article>;
}
// app/about/page.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'About Us',
description: 'Learn more about our company',
openGraph: {
title: 'About Us',
description: 'Learn more about our company',
images: ['/og-about.jpg'],
},
};
export default function AboutPage() {
return <div>About content</div>;
}
// app/products/[id]/page.tsx
import type { Metadata, ResolvingMetadata } from 'next';
interface PageProps {
params: Promise<{ id: string }>;
}
export async function generateMetadata(
{ params }: PageProps,
parent: ResolvingMetadata
): Promise<Metadata> {
const { id } = await params;
const product = await getProduct(id);
const previousImages = (await parent).openGraph?.images || [];
return {
title: product.name,
description: product.description,
openGraph: {
images: [product.image, ...previousImages],
},
};
}
// app/layout.tsx
export const metadata: Metadata = {
title: {
template: '%s | My Store',
default: 'My Store',
},
metadataBase: new URL('https://mystore.com'),
};
// app/products/page.tsx
export const metadata: Metadata = {
title: 'Products', // Results in "Products | My Store"
};
// app/dashboard/template.tsx
// Template re-mounts on navigation (state resets)
export default function DashboardTemplate({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<nav>Dashboard Navigation</nav>
{children}
</div>
);
}
// (marketing)/about/page.tsx → /about
// (marketing)/contact/page.tsx → /contact
// (shop)/products/page.tsx → /products
// (shop)/cart/page.tsx → /cart
// Different layouts for different sections
// app/(marketing)/layout.tsx - Marketing layout
// app/(shop)/layout.tsx - Shop layout
// components/Counter.tsx
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
</div>
);
}
// app/page.tsx (Server Component)
import { Counter } from '@/components/Counter';
import { db } from '@/lib/db';
export default async function Page() {
const initialData = await db.getData();
return (
<div>
<h1>Server rendered title</h1>
<Counter />
<ClientDataDisplay data={initialData} />
</div>
);
}
import Link from 'next/link';
export function Navigation() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/blog" prefetch={false}>Blog</Link>
<Link href="/dashboard" replace>Dashboard</Link>
</nav>
);
}
'use client';
import { useRouter } from 'next/navigation';
export function NavigateButton() {
const router = useRouter();
return (
<button onClick={() => router.push('/dashboard')}>
Go to Dashboard
</button>
);
}
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
export function CurrentPath() {
const pathname = usePathname();
const searchParams = useSearchParams();
const query = searchParams.get('q');
return (
<div>
<p>Current path: {pathname}</p>
{query && <p>Search query: {query}</p>}
</div>
);
}
| Practice | Description |
|---|---|
| Default to Server Components | Only use 'use client' when needed |
| Colocate related files | Keep page, loading, error together |
| Use route groups | Organize without affecting URL |
| Implement loading states | Use loading.tsx or Suspense |
| Handle errors gracefully | Use error.tsx boundaries |
| Optimize metadata | Use generateMetadata for dynamic pages |
Use when working with Payload CMS projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.