Agent skill
next-best-practices
Install this agent skill to your Project
npx add-skill https://github.com/frankxai/arcanea/tree/main/.claude/skills/development/next-best-practices
SKILL.md
Arcanea Next.js 16 Best Practices
"Aiyami guards the Crown Gate at 741 Hz — Enlightenment. Correct App Router patterns are enlightenment. Wrong patterns are Malachar's confusion."
File Conventions
Route Structure
apps/web/app/
├── layout.tsx # Root layout (html + body)
├── page.tsx # Home page
├── (marketing)/ # Route group (no URL segment)
│ ├── about/page.tsx
│ └── pricing/page.tsx
├── lore/
│ ├── layout.tsx # Nested layout
│ ├── page.tsx # /lore
│ ├── [element]/
│ │ └── page.tsx # /lore/fire, /lore/water...
│ └── [...slug]/
│ └── page.tsx # Catch-all: /lore/fire/draconia/...
├── academy/
│ └── gates/
│ └── [gate]/
│ └── page.tsx # /academy/gates/foundation
└── api/
└── guardians/
└── route.ts # GET/POST /api/guardians
Special Files (all must be .tsx for JSX)
| File | Purpose |
|---|---|
layout.tsx |
Shared UI, persists across navigation |
page.tsx |
Unique route UI |
loading.tsx |
Suspense boundary automatically |
error.tsx |
Error boundary (must be 'use client') |
not-found.tsx |
404 handling |
global-error.tsx |
Root error boundary |
route.ts |
API endpoint (no JSX) |
middleware.ts |
App root only (not in app/) |
RSC Boundaries
Server Component (default — no directive needed)
// app/lore/[element]/page.tsx — Server Component by default
import { supabase } from '@/lib/supabase-server'
export default async function ElementPage({ params }: { params: Promise<{ element: string }> }) {
const { element } = await params // NEXT.JS 16: params is now a Promise
const { data } = await supabase.from('elements').select().eq('slug', element).single()
return <ElementProfile data={data} />
}
Client Component — only when needed
// 'use client' required for: useState, useEffect, event handlers, browser APIs
'use client'
import { useState } from 'react'
export function GateQuiz() {
const [selectedGate, setSelectedGate] = useState<string | null>(null)
return <div onClick={() => setSelectedGate('foundation')}>...</div>
}
Invalid Patterns — RSC Errors
// ERROR: async Client Component (not allowed in React 19)
'use client'
export default async function Invalid() { // async + 'use client' = ERROR
const data = await fetch(...)
}
// ERROR: non-serializable props from Server to Client
// Server Component:
<ClientComp fn={() => console.log('hi')} /> // Functions can't cross RSC boundary
// VALID: Serialize data before passing
<ClientComp data={JSON.parse(JSON.stringify(complexObject))} />
Async APIs (Next.js 16 BREAKING CHANGES)
Next.js 16 made these APIs async. Always await them:
// app/academy/page.tsx
import { cookies, headers } from 'next/headers'
export default async function AcademyPage({
params,
searchParams,
}: {
params: Promise<{ slug: string }> // NOW A PROMISE
searchParams: Promise<{ gate?: string }> // NOW A PROMISE
}) {
const { slug } = await params
const { gate } = await searchParams
const cookieStore = await cookies() // NOW ASYNC
const headersList = await headers() // NOW ASYNC
const token = cookieStore.get('arcanea-session')
// ...
}
Server Actions — async params too
'use server'
import { cookies } from 'next/headers'
export async function unlockGate(gate: string) {
const cookieStore = await cookies() // async in Next.js 16
cookieStore.set('gate-unlocked', gate)
}
Error Handling
error.tsx — Must be Client Component
// app/lore/error.tsx
'use client'
import { useEffect } from 'react'
export default function LoreError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error('[Lore Error]', error)
}, [error])
return (
<div className="glass-card text-center">
<h2 className="text-gradient-aurora">The Lore is Veiled</h2>
<p className="text-white/70">{error.message}</p>
<button onClick={reset} className="btn-cosmic">Try Again</button>
</div>
)
}
Redirect and notFound in Server Components
import { redirect, notFound } from 'next/navigation'
export default async function GuardianPage({ params }: { params: Promise<{ gate: string }> }) {
const { gate } = await params
const guardian = await getGuardian(gate)
if (!guardian) notFound() // renders not-found.tsx
if (guardian.deprecated) redirect(`/lore/guardians/${guardian.newSlug}`)
return <GuardianProfile guardian={guardian} />
}
Metadata & SEO
Static Metadata
// app/lore/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Lore | Arcanea',
description: 'The mythology of the Ten Gates and Five Elements',
openGraph: {
title: 'Arcanea Lore',
description: 'Enter the mythology.',
images: ['/og/lore.png'],
},
}
Dynamic Metadata
// app/lore/[element]/page.tsx
export async function generateMetadata({
params,
}: {
params: Promise<{ element: string }>
}): Promise<Metadata> {
const { element } = await params
const data = await getElement(element)
return {
title: `${data.name} | Arcanea Lore`,
description: data.description,
openGraph: {
images: [data.ogImage],
},
}
}
OG Image Generation
// app/lore/[element]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
export const runtime = 'edge'
export const size = { width: 1200, height: 630 }
export default async function OGImage({ params }: { params: Promise<{ element: string }> }) {
const { element } = await params
return new ImageResponse(
<div style={{ background: 'linear-gradient(135deg, #1a0a2e, #0d1b40)', width: '100%', height: '100%' }}>
<h1 style={{ color: '#7fffd4', fontSize: 72 }}>{element}</h1>
</div>
)
}
Image Optimization
Always next/image — Never raw
import Image from 'next/image'
// Local image — width/height required
<Image src="/godbeast/draconis.png" alt="Draconis" width={800} height={600} />
// Remote image — configure in next.config.ts
<Image
src="https://cdn.arcanea.ai/guardians/draconia.jpg"
alt="Draconia"
fill // fills container — parent needs position: relative
sizes="(max-width: 768px) 100vw, 50vw"
priority // add for LCP images (above-the-fold)
/>
Remote images config
// next.config.ts
const config: NextConfig = {
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'cdn.arcanea.ai' },
{ protocol: 'https', hostname: '*.supabase.co' },
],
},
}
Font Optimization
next/font — Always, never link tags
// app/layout.tsx — Arcanea font setup
// MEMORY.md override: Inter everywhere, no Cinzel in new code
import { Inter, JetBrains_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
display: 'swap',
})
const jetbrains = JetBrains_Mono({
subsets: ['latin'],
variable: '--font-code',
display: 'swap',
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${inter.variable} ${jetbrains.variable}`}>
<body>{children}</body>
</html>
)
}
Route Handlers
// app/api/guardians/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { createServerClient } from '@/lib/supabase-server'
// IMPORTANT: GET handler conflicts with page.tsx in same directory
// Use separate directories: app/api/guardians/route.ts (no page.tsx here)
export async function GET(request: NextRequest) {
const supabase = createServerClient()
const { searchParams } = new URL(request.url)
const gate = searchParams.get('gate')
const query = supabase.from('guardians').select()
if (gate) query.eq('gate', gate)
const { data, error } = await query
if (error) return NextResponse.json({ error: error.message }, { status: 500 })
return NextResponse.json(data)
}
export async function POST(request: NextRequest) {
const supabase = createServerClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
const body = await request.json()
const { data, error } = await supabase.from('user_progress').insert(body).select().single()
if (error) return NextResponse.json({ error: error.message }, { status: 500 })
return NextResponse.json(data, { status: 201 })
}
Bundling Gotchas
// Server-incompatible packages — use dynamic import with ssr: false
// fs, path, canvas, sharp — Server only (ok in RSC)
// window, document, navigator — Client only (use dynamic or 'use client')
// CSS — import in JS files, never link tags
import '@/styles/globals.css' // correct
// <link rel="stylesheet"> in layout is WRONG for App Router
// next.config.ts — transpile if needed
const config: NextConfig = {
transpilePackages: ['@arcanea/ui'],
}
Quick Checklist
Before any Next.js 16 page/route in Arcanea:
-
paramsandsearchParamsawaited (they're Promises in Next.js 16) -
cookies()andheaders()awaited -
'use client'only where browser APIs or interactivity needed - No async Client Components
-
generateMetadatareturns proper OG data - Images use
next/imagewith sizes and priority where above fold - Fonts use
next/font— Inter + JetBrains Mono only - Route handlers in
/api/separate from page routes - error.tsx has
'use client'directive
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
luminor-personality-design
Design consistent, memorable AI personalities for Arcanea Luminors. From voice patterns to system prompts, create AI companions that feel magical and alive.
guardian-evolution-system
Design and implement the Guardian AI companion evolution system - from Level 1 Spark to Level 50 Transcendent. XP mechanics, personality adaptation, and player progression.
arcanea-prompt-craft
Master the Arcanean Prompt Language - advanced prompt engineering using mythological frameworks, constraint architecture, and the Centaur Principle for human-AI co-creation
Arcanea Lore Master
Maintains consistency in Arcanea world-building, including academy systems, magical mechanics, character lore, and narrative coherence across the fantasy multiverse platform
Arcanea Creator Academy
The integration of Teacher Team with Arcanea's creator education mission
Arcanea Canon Guardian
Canon consistency enforcement for Arcanea universe - tracks facts, prevents contradictions, maintains timeline, ensures lore integrity
Didn't find tool you were looking for?