| 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