worldcrafter-route-creator
Scaffold Next.js App Router routes for simple pages without forms or complex logic. Use when user needs "create a page", "add route", "add about/contact/terms page", "create API endpoint", "add layout", or mentions SSE/webhooks. Generates page, layout, loading, error, and not-found files with boilerplate. Best for static content, read-only pages, and API endpoints (REST, SSE, webhooks). Do NOT use for forms with validation (use worldcrafter-feature-builder), complete features with database (use worldcrafter-feature-builder), database-only changes (use worldcrafter-database-setup), or comprehensive testing (use worldcrafter-test-generator).
When & Why to Use This Skill
The WorldCrafter Route Creator is a specialized developer tool designed to automate the scaffolding of Next.js App Router structures. It streamlines the creation of pages, layouts, loading states, and error boundaries, as well as specialized API endpoints like Server-Sent Events (SSE) and webhooks. By generating standardized boilerplate code and following Next.js 15+ best practices, it ensures consistent project architecture and significantly reduces manual setup time for developers.
Use Cases
- Rapidly scaffolding static marketing or informational pages such as 'About Us', 'Privacy Policy', or 'Contact' with integrated loading and error UI.
- Setting up complex API Route Handlers for real-time updates via SSE, secure file uploads to Supabase, or Stripe webhook signature verification.
- Creating dynamic route segments (e.g., /worlds/[slug] or /posts/[id]) with pre-configured metadata generation and server-side data fetching patterns.
- Organizing application architecture using Route Groups to separate public marketing content from protected dashboard areas without affecting the URL structure.
- Implementing nested layouts for shared UI components across specific route segments, ensuring state preservation and consistent navigation.
| name | worldcrafter-route-creator |
|---|---|
| description | Scaffold Next.js App Router routes for simple pages without forms or complex logic. Use when user needs "create a page", "add route", "add about/contact/terms page", "create API endpoint", "add layout", or mentions SSE/webhooks. Generates page, layout, loading, error, and not-found files with boilerplate. Best for static content, read-only pages, and API endpoints (REST, SSE, webhooks). Do NOT use for forms with validation (use worldcrafter-feature-builder), complete features with database (use worldcrafter-feature-builder), database-only changes (use worldcrafter-database-setup), or comprehensive testing (use worldcrafter-test-generator). |
WorldCrafter Route Creator
Version: 2.0.0 Last Updated: 2025-01-15
This skill provides tools and templates for scaffolding Next.js App Router routes with all necessary files and boilerplate code.
Skill Metadata
Related Skills:
worldcrafter-feature-builder- Use instead for complete features with forms and validationworldcrafter-auth-guard- Use to protect routes after creationworldcrafter-test-generator- Use to add tests for created routes
Example Use Cases:
- "Create an about us page" → Generates page.tsx with static content, loading.tsx, and error.tsx at /about
- "Add an API endpoint for users" → Creates route.ts with GET/POST handlers at /api/users
- "Create a dashboard layout" → Generates layout.tsx with navigation and shared UI for /dashboard routes
- "Add a dynamic product page" → Creates /products/[id]/page.tsx with dynamic route handling
When to Use This Skill
Use this skill when:
- Creating new pages/routes in the application
- Setting up route groups or nested routes
- Creating API routes (Route Handlers)
- Adding layouts for route segments
- Setting up protected/authenticated routes
- Creating dynamic routes with parameters
- Adding loading states and error boundaries
Next.js App Router Overview
WorldCrafter uses Next.js 15+ App Router with these file conventions:
page.tsx- UI for a route (makes route publicly accessible)layout.tsx- Shared UI for segment and childrenloading.tsx- Loading UI (Suspense boundary)error.tsx- Error UI (Error boundary)not-found.tsx- 404 UI for not found errorsroute.ts- API endpoint (Route Handler)
Route Scaffolding Process
Phase 1: Plan Route Structure
Consider:
- Route path (e.g.,
/worlds/[slug],/api/upload,/api/sse/activity) - Is it a page or API route?
- Does it need authentication?
- Does it need a custom layout?
- What loading/error states are needed?
WorldCrafter Route Reference: See references/worldcrafter-routes.md for complete application route tree with all patterns:
- World management routes (
/worlds/[slug]/*) - Entity routes (characters, locations, events, items, factions)
- Visualization routes (graph, map, timeline, wiki)
- API routes (upload, export, MCP, SSE, webhooks)
- Auth routes (login, signup, reset-password)
- Marketing routes (landing, about, pricing)
Phase 2: Scaffold Route
Automated Scaffolding:
python .claude/skills/worldcrafter-route-creator/scripts/scaffold_route.py <route-path>
Examples:
# Basic page route
python scaffold_route.py dashboard
# Nested route
python scaffold_route.py dashboard/settings
# Dynamic route
python scaffold_route.py posts/[id]
# Route group
python scaffold_route.py (marketing)/about
# API route
python scaffold_route.py api/users --api
This creates:
src/app/<route-path>/page.tsx(orroute.tsfor API)src/app/<route-path>/loading.tsxsrc/app/<route-path>/error.tsx- Optional:
layout.tsx,not-found.tsx
Manual Approach:
Copy templates from assets/templates/ and customize.
Phase 3: Customize Route Files
Page Component (page.tsx)
export default function DashboardPage() {
return (
<div className="container mx-auto py-8">
<h1 className="text-3xl font-bold">Dashboard</h1>
{/* Page content */}
</div>
)
}
Server Component Features:
- Async data fetching
- Direct database access
- No client-side JavaScript by default
Client Component Features:
- Add
"use client"directive - Interactive elements
- Hooks (useState, useEffect, etc.)
Layout (layout.tsx)
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="dashboard-layout">
<nav>{/* Navigation */}</nav>
<main>{children}</main>
</div>
)
}
Layout Features:
- Shared UI across routes
- Preserved state on navigation
- Can be nested
- Cannot access route parameters (use page)
Loading State (loading.tsx)
export default function Loading() {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="h-12 w-12 animate-spin rounded-full border-4 border-muted border-t-primary" />
</div>
)
}
Features:
- Instant loading state
- Wrapped in Suspense automatically
- Streamed from server
Error Boundary (error.tsx)
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div className="flex min-h-screen flex-col items-center justify-center">
<h2 className="text-2xl font-bold">Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
)
}
Features:
- Must be Client Component
- Catches errors in children
- Provides reset function
- Can be nested
API Route (route.ts)
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
export async function GET() {
const data = await prisma.model.findMany()
return NextResponse.json(data)
}
export async function POST(request: Request) {
const body = await request.json()
const result = await prisma.model.create({ data: body })
return NextResponse.json(result)
}
Supported Methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
Phase 4: Add Metadata
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Dashboard',
description: 'User dashboard page'
}
export default function DashboardPage() {
// ...
}
Dynamic Metadata:
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.id)
return {
title: post.title,
description: post.excerpt
}
}
WorldCrafter API Route Patterns
WorldCrafter uses several specialized API route patterns. See references/worldcrafter-routes.md for complete implementations.
File Upload to Supabase Storage
// src/app/api/upload/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
export async function POST(request: NextRequest) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const formData = await request.formData()
const file = formData.get('file') as File
const { data, error } = await supabase.storage
.from('uploads')
.upload(`${user.id}/${file.name}`, file)
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
}
return NextResponse.json({ url: data.path })
}
Server-Sent Events (SSE) for Real-Time Updates
// src/app/api/sse/activity/route.ts
import { NextRequest } from 'next/server'
import { createClient } from '@/lib/supabase/server'
export async function GET(request: NextRequest) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return new Response('Unauthorized', { status: 401 })
}
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder()
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'connected' })}\n\n`))
const channel = supabase
.channel('activity')
.on('postgres_changes', {
event: '*',
schema: 'public',
table: 'activities',
filter: `user_id=eq.${user.id}`
}, (payload) => {
controller.enqueue(encoder.encode(`data: ${JSON.stringify(payload)}\n\n`))
})
.subscribe()
request.signal.addEventListener('abort', () => {
channel.unsubscribe()
controller.close()
})
}
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
}
})
}
MCP Server (JSON-RPC 2.0)
// src/app/api/mcp/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { prisma } from '@/lib/prisma'
interface JsonRpcRequest {
jsonrpc: '2.0'
method: string
params?: unknown
id: string | number
}
export async function POST(request: NextRequest) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return NextResponse.json({
jsonrpc: '2.0',
error: { code: -32001, message: 'Unauthorized' },
id: null
}, { status: 401 })
}
const rpcRequest: JsonRpcRequest = await request.json()
try {
const result = await handleMcpMethod(rpcRequest.method, rpcRequest.params, user.id)
return NextResponse.json({
jsonrpc: '2.0',
result,
id: rpcRequest.id
})
} catch (error) {
return NextResponse.json({
jsonrpc: '2.0',
error: {
code: -32603,
message: error instanceof Error ? error.message : 'Internal error'
},
id: rpcRequest.id
}, { status: 500 })
}
}
Webhook Handler (with Signature Verification)
// src/app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { headers } from 'next/headers'
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-11-20.acacia'
})
export async function POST(request: NextRequest) {
const body = await request.text()
const signature = (await headers()).get('stripe-signature')!
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
)
} catch (err) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
}
// Handle event types
switch (event.type) {
case 'checkout.session.completed':
// Handle successful checkout
break
case 'customer.subscription.deleted':
// Handle subscription cancellation
break
}
return NextResponse.json({ received: true })
}
Background Job (Export)
// src/app/api/export/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { prisma } from '@/lib/prisma'
export async function POST(request: NextRequest) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { worldId } = await request.json()
// Create export job
const job = await prisma.exportJob.create({
data: {
userId: user.id,
worldId,
status: 'pending'
}
})
// Trigger background processing
// await queueExportJob(job.id)
return NextResponse.json({ jobId: job.id })
}
Common Route Patterns
Basic Page Route
src/app/
└── about/
├── page.tsx # /about
├── loading.tsx
└── error.tsx
Nested Routes
src/app/
└── dashboard/
├── page.tsx # /dashboard
├── layout.tsx
├── settings/
│ └── page.tsx # /dashboard/settings
└── profile/
└── page.tsx # /dashboard/profile
Dynamic Routes
src/app/
└── posts/
├── page.tsx # /posts
└── [id]/
├── page.tsx # /posts/123
├── edit/
│ └── page.tsx # /posts/123/edit
└── not-found.tsx
Access Parameters:
export default function PostPage({ params }: { params: { id: string } }) {
return <div>Post ID: {params.id}</div>
}
WorldCrafter World Management Routes
src/app/
└── worlds/
├── page.tsx # /worlds - List all user's worlds
├── new/
│ └── page.tsx # /worlds/new - Create world
└── [slug]/
├── page.tsx # /worlds/:slug - Dashboard
├── settings/
│ └── page.tsx # /worlds/:slug/settings
├── characters/
│ ├── page.tsx # /worlds/:slug/characters - List
│ ├── new/
│ │ └── page.tsx # /worlds/:slug/characters/new
│ └── [id]/
│ ├── page.tsx # /worlds/:slug/characters/:id - Detail
│ └── edit/
│ └── page.tsx # /worlds/:slug/characters/:id/edit
├── graph/
│ └── page.tsx # /worlds/:slug/graph - Visualization
├── map/
│ └── page.tsx # /worlds/:slug/map - Interactive map
└── timeline/
└── page.tsx # /worlds/:slug/timeline - Events timeline
Pattern Features:
- World slug for human-readable URLs
- Nested entity routes (characters, locations, events, items, factions)
- Consistent CRUD pattern (list, new, [id], [id]/edit)
- Visualization routes (graph, map, timeline)
- All routes protected (auth required)
- World ownership verified in layout
Catch-All Routes
src/app/
└── docs/
└── [...slug]/
└── page.tsx # /docs/a, /docs/a/b, /docs/a/b/c
Optional Catch-All:
└── [[...slug]]/ # Matches /docs too
WorldCrafter Wiki Example:
src/app/
└── worlds/
└── [slug]/
└── wiki/
├── page.tsx # /worlds/:slug/wiki - Home
└── [pageSlug]/
└── page.tsx # /worlds/:slug/wiki/:pageSlug
Route Groups
src/app/
├── (auth)/ # Auth route group (not in URL)
│ ├── layout.tsx # Auth-specific layout
│ ├── login/
│ │ └── page.tsx # /login
│ ├── signup/
│ │ └── page.tsx # /signup
│ └── reset-password/
│ └── page.tsx # /reset-password
└── (marketing)/ # Marketing route group (not in URL)
├── layout.tsx # Marketing layout
├── page.tsx # / (landing)
├── about/
│ └── page.tsx # /about
└── pricing/
└── page.tsx # /pricing
WorldCrafter Use Cases:
- Different layouts for auth vs marketing pages
- Organize routes logically without affecting URLs
- Share navigation and styling within groups
- Separate public and authenticated sections
Parallel Routes
src/app/
└── dashboard/
├── @analytics/
│ └── page.tsx
├── @team/
│ └── page.tsx
└── page.tsx
Layout:
export default function Layout({
children,
analytics,
team,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
return (
<>
{children}
{analytics}
{team}
</>
)
}
Intercepting Routes
src/app/
├── feed/
│ └── page.tsx
├── photo/
│ └── [id]/
│ └── page.tsx # /photo/123
└── @modal/
└── (.)photo/
└── [id]/
└── page.tsx # Intercepts /photo/123
API Routes
src/app/
└── api/
├── upload/
│ └── route.ts # POST /api/upload - File upload
├── export/
│ └── route.ts # POST /api/export - Background job
├── mcp/
│ └── route.ts # POST /api/mcp - MCP server (JSON-RPC)
├── sse/
│ └── activity/
│ └── route.ts # GET /api/sse/activity - Server-Sent Events
└── webhooks/
└── stripe/
└── route.ts # POST /api/webhooks/stripe - Stripe webhook
WorldCrafter API Patterns:
- File upload to Supabase Storage
- Background job creation (export)
- MCP server with JSON-RPC 2.0
- Server-Sent Events for real-time updates
- Webhook handlers with signature verification
Authentication Patterns
Protected Page Route
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
export default async function ProtectedPage() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
redirect('/login')
}
return <div>Protected content for {user.email}</div>
}
Protected API Route
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
export async function GET() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Fetch user-specific data
const data = await prisma.data.findMany({
where: { userId: user.id }
})
return NextResponse.json(data)
}
Protected Layout
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
redirect('/login')
}
return (
<div className="dashboard-layout">
{children}
</div>
)
}
Data Fetching Patterns
Server Component (Recommended)
import { prisma } from '@/lib/prisma'
export default async function PostsPage() {
const posts = await prisma.post.findMany()
return (
<div>
{posts.map(post => (
<article key={post.id}>{post.title}</article>
))}
</div>
)
}
Parallel Data Fetching
async function getData() {
const [users, posts] = await Promise.all([
prisma.user.findMany(),
prisma.post.findMany()
])
return { users, posts }
}
export default async function Page() {
const { users, posts } = await getData()
// ...
}
Sequential Data Fetching
export default async function Page({ params }: { params: { id: string } }) {
const user = await prisma.user.findUnique({
where: { id: params.id }
})
// Waits for user before fetching posts
const posts = await prisma.post.findMany({
where: { authorId: user.id }
})
return <div>{/* ... */}</div>
}
Reference Files
references/worldcrafter-routes.md- Complete WorldCrafter route tree with all API patternsreferences/related-skills.md- How this skill works with other WorldCrafter skillsassets/templates/- All route file templates
Key patterns in worldcrafter-routes.md:
- World management structure (
/worlds/[slug]/*) - Entity CRUD patterns (characters, locations, events, items, factions)
- API routes (file upload, background jobs, MCP server, SSE, webhooks)
- Auth route group patterns
- Nested layouts and route groups
- Dynamic route parameter handling
Skill Orchestration
This skill is best for creating simple pages and routes. For complete features with forms, use worldcrafter-feature-builder instead.
Common Workflows
Simple Static Pages:
- worldcrafter-route-creator (this skill) - Create page structure
- Add content and styling
- Optionally use worldcrafter-test-generator to add tests
API Endpoints:
- worldcrafter-route-creator (this skill) - Create route.ts with handlers
- Add business logic and database operations
- Optionally use worldcrafter-auth-guard to protect endpoints
Protected Pages:
- worldcrafter-route-creator (this skill) - Create page structure
- worldcrafter-auth-guard - Add authentication checks
Dynamic Routes:
- worldcrafter-route-creator (this skill) - Create [id]/page.tsx structure
- Add data fetching and rendering logic
- Add not-found.tsx for invalid IDs
When Claude Should Use Multiple Skills
Claude will orchestrate route-creator with other skills when:
- User wants a simple page that later needs protection (route-creator → auth-guard)
- User wants to create API endpoints (route-creator → auth-guard for protection)
- User wants static pages with comprehensive tests (route-creator → test-generator)
Example orchestration:
User: "Create an about page and a contact page"
Claude's workflow:
1. worldcrafter-route-creator (this skill):
- Create /about page with loading and error states
- Create /contact page with loading and error states
Alternative - Complete Feature:
User: "Create a contact form"
Claude's workflow (DO NOT use route-creator):
1. worldcrafter-feature-builder (use instead):
- Creates /contact route with form
- Adds validation schema
- Creates Server Action
- Includes tests
Skill Selection Guidance
Choose this skill when:
- User wants "simple page", "about page", "static content"
- User mentions "API endpoint" or "route handler"
- User wants layouts or route structure only
- No forms or validation needed
Choose worldcrafter-feature-builder instead when:
- User mentions "form", "submit", "validation", "feature"
- User wants complete functionality with database
- User wants CRUD operations
- User wants comprehensive implementation
Use this skill for:
- Static content pages (about, terms, privacy)
- API endpoints (Route Handlers)
- Layouts and route groups
- Simple read-only pages
- Dynamic routes without forms
Do NOT use this skill for:
- Forms with validation (use feature-builder)
- Complete features (use feature-builder)
- Database operations (use database-setup or feature-builder)
Route Scaffolding Script Reference
# Basic route
python scaffold_route.py dashboard
# With layout
python scaffold_route.py dashboard --with-layout
# API route
python scaffold_route.py api/users --api
# Dynamic route
python scaffold_route.py posts/[id]
# Protected route
python scaffold_route.py dashboard --protected
Troubleshooting
Route Not Found
- Ensure
page.tsxexists in route folder - Check file naming (lowercase, no typos)
- Verify folder structure matches URL
Layout Not Applied
- Layout must be named
layout.tsx - Layout applies to children only
- Check layout is in correct folder
Loading State Not Showing
- Add
loading.tsxin route folder - Ensure page has async operations
- Check Suspense boundaries
Error Boundary Not Catching
- Error boundary must be Client Component (
'use client') - Only catches errors in children
- Add error.tsx at appropriate level
Best Practices
- Use Server Components by default - Add
'use client'only when needed - Colocate files - Keep components/utilities with routes
- Use layouts for shared UI - Navigation, headers, footers
- Add loading states - Improve perceived performance
- Handle errors gracefully - Provide reset functionality
- Protect sensitive routes - Check auth in server components
- Optimize metadata - Use generateMetadata for dynamic content
- Use route groups - Organize routes without affecting URLs
Success Criteria
A complete route setup includes:
- ✅ Page component created
- ✅ Loading state added
- ✅ Error boundary added
- ✅ Layout (if shared UI needed)
- ✅ Metadata configured
- ✅ Authentication check (if protected)
- ✅ Not-found page (for dynamic routes)
- ✅ Route accessible at correct URL