Agent skill
next-best-practices-frankxai-arcanea
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/next-best-practices-frankxai-arcanea
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.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?