-
-// Good: Use useId hook
-import { useId } from 'react'
-
-function Input() {
- const id = useId()
- return
-}
-```
-
-### Invalid HTML Nesting
-
-```tsx
-// Bad: Invalid - div inside p
-
Content
-
-// Bad: Invalid - p inside p
-
Nested
-
-// Good: Valid nesting
-
-```
-
-### Third-party Scripts
-
-Scripts that modify DOM during hydration.
-
-```tsx
-// Good: Use next/script with afterInteractive
-import Script from 'next/script'
-
-export default function Page() {
- return (
-
- )
-}
-```
diff --git a/.agents/skills/next-best-practices/image.md b/.agents/skills/next-best-practices/image.md
deleted file mode 100644
index aa9d28db..00000000
--- a/.agents/skills/next-best-practices/image.md
+++ /dev/null
@@ -1,173 +0,0 @@
-# Image Optimization
-
-Use `next/image` for automatic image optimization.
-
-## Always Use next/image
-
-```tsx
-// Bad: Avoid native img
-
-
-// Good: Use next/image
-import Image from 'next/image'
-
-```
-
-## Required Props
-
-Images need explicit dimensions to prevent layout shift:
-
-```tsx
-// Local images - dimensions inferred automatically
-import heroImage from './hero.png'
-
-
-// Remote images - must specify width/height
-
-
-// Or use fill for parent-relative sizing
-
-
-
-```
-
-## Remote Images Configuration
-
-Remote domains must be configured in `next.config.js`:
-
-```js
-// next.config.js
-module.exports = {
- images: {
- remotePatterns: [
- {
- protocol: 'https',
- hostname: 'example.com',
- pathname: '/images/**',
- },
- {
- protocol: 'https',
- hostname: '*.cdn.com', // Wildcard subdomain
- },
- ],
- },
-}
-```
-
-## Responsive Images
-
-Use `sizes` to tell the browser which size to download:
-
-```tsx
-// Full-width hero
-
-
-// Responsive grid (3 columns on desktop, 1 on mobile)
-
-
-// Fixed sidebar image
-
-```
-
-## Blur Placeholder
-
-Prevent layout shift with placeholders:
-
-```tsx
-// Local images - automatic blur hash
-import heroImage from './hero.png'
-
-
-// Remote images - provide blurDataURL
-
-
-// Or use color placeholder
-
-```
-
-## Priority Loading
-
-Use `priority` for above-the-fold images (LCP):
-
-```tsx
-// Hero image - loads immediately
-
-
-// Below-fold images - lazy loaded by default (no priority needed)
-
-```
-
-## Common Mistakes
-
-```tsx
-// Bad: Missing sizes with fill - downloads largest image
-
-
-// Good: Add sizes for proper responsive behavior
-
-
-// Bad: Using width/height for aspect ratio only
-
-
-// Good: Use actual display dimensions or fill with sizes
-
-
-// Bad: Remote image without config
-
-// Error: Invalid src prop, hostname not configured
-
-// Good: Add hostname to next.config.js remotePatterns
-```
-
-## Static Export
-
-When using `output: 'export'`, use `unoptimized` or custom loader:
-
-```tsx
-// Option 1: Disable optimization
-
-
-// Option 2: Global config
-// next.config.js
-module.exports = {
- output: 'export',
- images: { unoptimized: true },
-}
-
-// Option 3: Custom loader (Cloudinary, Imgix, etc.)
-const cloudinaryLoader = ({ src, width, quality }) => {
- return `https://res.cloudinary.com/demo/image/upload/w_${width},q_${quality || 75}/${src}`
-}
-
-
-```
diff --git a/.agents/skills/next-best-practices/metadata.md b/.agents/skills/next-best-practices/metadata.md
deleted file mode 100644
index 5a0f6551..00000000
--- a/.agents/skills/next-best-practices/metadata.md
+++ /dev/null
@@ -1,301 +0,0 @@
-# Metadata
-
-Add SEO metadata to Next.js pages using the Metadata API.
-
-## Important: Server Components Only
-
-The `metadata` object and `generateMetadata` function are **only supported in Server Components**. They cannot be used in Client Components.
-
-If the target page has `'use client'`:
-1. Remove `'use client'` if possible, move client logic to child components
-2. Or extract metadata to a parent Server Component layout
-3. Or split the file: Server Component with metadata imports Client Components
-
-## Static Metadata
-
-```tsx
-import type { Metadata } from 'next'
-
-export const metadata: Metadata = {
- title: 'Page Title',
- description: 'Page description for search engines',
-}
-```
-
-## Dynamic Metadata
-
-```tsx
-import type { Metadata } from 'next'
-
-type Props = { params: Promise<{ slug: string }> }
-
-export async function generateMetadata({ params }: Props): Promise
{
- const { slug } = await params
- const post = await getPost(slug)
- return { title: post.title, description: post.description }
-}
-```
-
-## Avoid Duplicate Fetches
-
-Use React `cache()` when the same data is needed for both metadata and page:
-
-```tsx
-import { cache } from 'react'
-
-export const getPost = cache(async (slug: string) => {
- return await db.posts.findFirst({ where: { slug } })
-})
-```
-
-## Viewport
-
-Separate from metadata for streaming support:
-
-```tsx
-import type { Viewport } from 'next'
-
-export const viewport: Viewport = {
- width: 'device-width',
- initialScale: 1,
- themeColor: '#000000',
-}
-
-// Or dynamic
-export function generateViewport({ params }): Viewport {
- return { themeColor: getThemeColor(params) }
-}
-```
-
-## Title Templates
-
-In root layout for consistent naming:
-
-```tsx
-export const metadata: Metadata = {
- title: { default: 'Site Name', template: '%s | Site Name' },
-}
-```
-
-## Metadata File Conventions
-
-Reference: https://nextjs.org/docs/app/getting-started/project-structure#metadata-file-conventions
-
-Place these files in `app/` directory (or route segments):
-
-| File | Purpose |
-|------|---------|
-| `favicon.ico` | Favicon |
-| `icon.png` / `icon.svg` | App icon |
-| `apple-icon.png` | Apple app icon |
-| `opengraph-image.png` | OG image |
-| `twitter-image.png` | Twitter card image |
-| `sitemap.ts` / `sitemap.xml` | Sitemap (use `generateSitemaps` for multiple) |
-| `robots.ts` / `robots.txt` | Robots directives |
-| `manifest.ts` / `manifest.json` | Web app manifest |
-
-## SEO Best Practice: Static Files Are Often Enough
-
-For most sites, **static metadata files provide excellent SEO coverage**:
-
-```
-app/
-├── favicon.ico
-├── opengraph-image.png # Works for both OG and Twitter
-├── sitemap.ts
-├── robots.ts
-└── layout.tsx # With title/description metadata
-```
-
-**Tips:**
-- A single `opengraph-image.png` covers both Open Graph and Twitter (Twitter falls back to OG)
-- Static `title` and `description` in layout metadata is sufficient for most pages
-- Only use dynamic `generateMetadata` when content varies per page
-
----
-
-# OG Image Generation
-
-Generate dynamic Open Graph images using `next/og`.
-
-## Important Rules
-
-1. **Use `next/og`** - not `@vercel/og` (it's built into Next.js)
-2. **No searchParams** - OG images can't access search params, use route params instead
-3. **Avoid Edge runtime** - Use default Node.js runtime
-
-```tsx
-// Good
-import { ImageResponse } from 'next/og'
-
-// Bad
-// import { ImageResponse } from '@vercel/og'
-// export const runtime = 'edge'
-```
-
-## Basic OG Image
-
-```tsx
-// app/opengraph-image.tsx
-import { ImageResponse } from 'next/og'
-
-export const alt = 'Site Name'
-export const size = { width: 1200, height: 630 }
-export const contentType = 'image/png'
-
-export default function Image() {
- return new ImageResponse(
- (
-
- Hello World
-
- ),
- { ...size }
- )
-}
-```
-
-## Dynamic OG Image
-
-```tsx
-// app/blog/[slug]/opengraph-image.tsx
-import { ImageResponse } from 'next/og'
-
-export const alt = 'Blog Post'
-export const size = { width: 1200, height: 630 }
-export const contentType = 'image/png'
-
-type Props = { params: Promise<{ slug: string }> }
-
-export default async function Image({ params }: Props) {
- const { slug } = await params
- const post = await getPost(slug)
-
- return new ImageResponse(
- (
-
-
{post.title}
-
{post.description}
-
- ),
- { ...size }
- )
-}
-```
-
-## Custom Fonts
-
-```tsx
-import { ImageResponse } from 'next/og'
-import { join } from 'path'
-import { readFile } from 'fs/promises'
-
-export default async function Image() {
- const fontPath = join(process.cwd(), 'assets/fonts/Inter-Bold.ttf')
- const fontData = await readFile(fontPath)
-
- return new ImageResponse(
- (
-
- Custom Font Text
-
- ),
- {
- width: 1200,
- height: 630,
- fonts: [{ name: 'Inter', data: fontData, style: 'normal' }],
- }
- )
-}
-```
-
-## File Naming
-
-- `opengraph-image.tsx` - Open Graph (Facebook, LinkedIn)
-- `twitter-image.tsx` - Twitter/X cards (optional, falls back to OG)
-
-## Styling Notes
-
-ImageResponse uses Flexbox layout:
-- Use `display: 'flex'`
-- No CSS Grid support
-- Styles must be inline objects
-
-## Multiple OG Images
-
-Use `generateImageMetadata` for multiple images per route:
-
-```tsx
-// app/blog/[slug]/opengraph-image.tsx
-import { ImageResponse } from 'next/og'
-
-export async function generateImageMetadata({ params }) {
- const images = await getPostImages(params.slug)
- return images.map((img, idx) => ({
- id: idx,
- alt: img.alt,
- size: { width: 1200, height: 630 },
- contentType: 'image/png',
- }))
-}
-
-export default async function Image({ params, id }) {
- const images = await getPostImages(params.slug)
- const image = images[id]
- return new ImageResponse(/* ... */)
-}
-```
-
-## Multiple Sitemaps
-
-Use `generateSitemaps` for large sites:
-
-```tsx
-// app/sitemap.ts
-import type { MetadataRoute } from 'next'
-
-export async function generateSitemaps() {
- // Return array of sitemap IDs
- return [{ id: 0 }, { id: 1 }, { id: 2 }]
-}
-
-export default async function sitemap({
- id,
-}: {
- id: number
-}): Promise {
- const start = id * 50000
- const end = start + 50000
- const products = await getProducts(start, end)
-
- return products.map((product) => ({
- url: `https://example.com/product/${product.id}`,
- lastModified: product.updatedAt,
- }))
-}
-```
-
-Generates `/sitemap/0.xml`, `/sitemap/1.xml`, etc.
diff --git a/.agents/skills/next-best-practices/parallel-routes.md b/.agents/skills/next-best-practices/parallel-routes.md
deleted file mode 100644
index 51e270d8..00000000
--- a/.agents/skills/next-best-practices/parallel-routes.md
+++ /dev/null
@@ -1,287 +0,0 @@
-# Parallel & Intercepting Routes
-
-Parallel routes render multiple pages in the same layout. Intercepting routes show a different UI when navigating from within your app vs direct URL access. Together they enable modal patterns.
-
-## File Structure
-
-```
-app/
-├── @modal/ # Parallel route slot
-│ ├── default.tsx # Required! Returns null
-│ ├── (.)photos/ # Intercepts /photos/*
-│ │ └── [id]/
-│ │ └── page.tsx # Modal content
-│ └── [...]catchall/ # Optional: catch unmatched
-│ └── page.tsx
-├── photos/
-│ └── [id]/
-│ └── page.tsx # Full page (direct access)
-├── layout.tsx # Renders both children and @modal
-└── page.tsx
-```
-
-## Step 1: Root Layout with Slot
-
-```tsx
-// app/layout.tsx
-export default function RootLayout({
- children,
- modal,
-}: {
- children: React.ReactNode;
- modal: React.ReactNode;
-}) {
- return (
-
-
- {children}
- {modal}
-
-
- );
-}
-```
-
-## Step 2: Default File (Critical!)
-
-**Every parallel route slot MUST have a `default.tsx`** to prevent 404s on hard navigation.
-
-```tsx
-// app/@modal/default.tsx
-export default function Default() {
- return null;
-}
-```
-
-Without this file, refreshing any page will 404 because Next.js can't determine what to render in the `@modal` slot.
-
-## Step 3: Intercepting Route (Modal)
-
-The `(.)` prefix intercepts routes at the same level.
-
-```tsx
-// app/@modal/(.)photos/[id]/page.tsx
-import { Modal } from '@/components/modal';
-
-export default async function PhotoModal({
- params
-}: {
- params: Promise<{ id: string }>
-}) {
- const { id } = await params;
- const photo = await getPhoto(id);
-
- return (
-
-
-
- );
-}
-```
-
-## Step 4: Full Page (Direct Access)
-
-```tsx
-// app/photos/[id]/page.tsx
-export default async function PhotoPage({
- params
-}: {
- params: Promise<{ id: string }>
-}) {
- const { id } = await params;
- const photo = await getPhoto(id);
-
- return (
-
-
-
{photo.title}
-
- );
-}
-```
-
-## Step 5: Modal Component with Correct Closing
-
-**Critical: Use `router.back()` to close modals, NOT `router.push()` or ` `.**
-
-```tsx
-// components/modal.tsx
-'use client';
-
-import { useRouter } from 'next/navigation';
-import { useCallback, useEffect, useRef } from 'react';
-
-export function Modal({ children }: { children: React.ReactNode }) {
- const router = useRouter();
- const overlayRef = useRef(null);
-
- // Close on escape key
- useEffect(() => {
- function onKeyDown(e: KeyboardEvent) {
- if (e.key === 'Escape') {
- router.back(); // Correct
- }
- }
- document.addEventListener('keydown', onKeyDown);
- return () => document.removeEventListener('keydown', onKeyDown);
- }, [router]);
-
- // Close on overlay click
- const handleOverlayClick = useCallback((e: React.MouseEvent) => {
- if (e.target === overlayRef.current) {
- router.back(); // Correct
- }
- }, [router]);
-
- return (
-
-
- router.back()} // Correct!
- className="absolute top-4 right-4"
- >
- Close
-
- {children}
-
-
- );
-}
-```
-
-### Why NOT `router.push('/')` or ` `?
-
-Using `push` or `Link` to "close" a modal:
-1. Adds a new history entry (back button shows modal again)
-2. Doesn't properly clear the intercepted route
-3. Can cause the modal to flash or persist unexpectedly
-
-`router.back()` correctly:
-1. Removes the intercepted route from history
-2. Returns to the previous page
-3. Properly unmounts the modal
-
-## Route Matcher Reference
-
-Matchers match **route segments**, not filesystem paths:
-
-| Matcher | Matches | Example |
-|---------|---------|---------|
-| `(.)` | Same level | `@modal/(.)photos` intercepts `/photos` |
-| `(..)` | One level up | `@modal/(..)settings` from `/dashboard/@modal` intercepts `/settings` |
-| `(..)(..)` | Two levels up | Rarely used |
-| `(...)` | From root | `@modal/(...)photos` intercepts `/photos` from anywhere |
-
-**Common mistake**: Thinking `(..)` means "parent folder" - it means "parent route segment".
-
-## Handling Hard Navigation
-
-When users directly visit `/photos/123` (bookmark, refresh, shared link):
-- The intercepting route is bypassed
-- The full `photos/[id]/page.tsx` renders
-- Modal doesn't appear (expected behavior)
-
-If you want the modal to appear on direct access too, you need additional logic:
-
-```tsx
-// app/photos/[id]/page.tsx
-import { Modal } from '@/components/modal';
-
-export default async function PhotoPage({ params }) {
- const { id } = await params;
- const photo = await getPhoto(id);
-
- // Option: Render as modal on direct access too
- return (
-
-
-
- );
-}
-```
-
-## Common Gotchas
-
-### 1. Missing `default.tsx` → 404 on Refresh
-
-Every `@slot` folder needs a `default.tsx` that returns `null` (or appropriate content).
-
-### 2. Modal Persists After Navigation
-
-You're using `router.push()` instead of `router.back()`.
-
-### 3. Nested Parallel Routes Need Defaults Too
-
-If you have `@modal` inside a route group, each level needs its own `default.tsx`:
-
-```
-app/
-├── (marketing)/
-│ ├── @modal/
-│ │ └── default.tsx # Needed!
-│ └── layout.tsx
-└── layout.tsx
-```
-
-### 4. Intercepted Route Shows Wrong Content
-
-Check your matcher:
-- `(.)photos` intercepts `/photos` from the same route level
-- If your `@modal` is in `app/dashboard/@modal`, use `(.)photos` to intercept `/dashboard/photos`, not `/photos`
-
-### 5. TypeScript Errors with `params`
-
-In Next.js 15+, `params` is a Promise:
-
-```tsx
-// Correct
-export default async function Page({ params }: { params: Promise<{ id: string }> }) {
- const { id } = await params;
-}
-```
-
-## Complete Example: Photo Gallery Modal
-
-```
-app/
-├── @modal/
-│ ├── default.tsx
-│ └── (.)photos/
-│ └── [id]/
-│ └── page.tsx
-├── photos/
-│ ├── page.tsx # Gallery grid
-│ └── [id]/
-│ └── page.tsx # Full photo page
-├── layout.tsx
-└── page.tsx
-```
-
-Links in the gallery:
-
-```tsx
-// app/photos/page.tsx
-import Link from 'next/link';
-
-export default async function Gallery() {
- const photos = await getPhotos();
-
- return (
-
- {photos.map(photo => (
-
-
-
- ))}
-
- );
-}
-```
-
-Clicking a photo → Modal opens (intercepted)
-Direct URL → Full page renders
-Refresh while modal open → Full page renders
diff --git a/.agents/skills/next-best-practices/route-handlers.md b/.agents/skills/next-best-practices/route-handlers.md
deleted file mode 100644
index 25e6f4d7..00000000
--- a/.agents/skills/next-best-practices/route-handlers.md
+++ /dev/null
@@ -1,146 +0,0 @@
-# Route Handlers
-
-Create API endpoints with `route.ts` files.
-
-## Basic Usage
-
-```tsx
-// app/api/users/route.ts
-export async function GET() {
- const users = await getUsers()
- return Response.json(users)
-}
-
-export async function POST(request: Request) {
- const body = await request.json()
- const user = await createUser(body)
- return Response.json(user, { status: 201 })
-}
-```
-
-## Supported Methods
-
-`GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`
-
-## GET Handler Conflicts with page.tsx
-
-**A `route.ts` and `page.tsx` cannot coexist in the same folder.**
-
-```
-app/
-├── api/
-│ └── users/
-│ └── route.ts # /api/users
-└── users/
- ├── page.tsx # /users (page)
- └── route.ts # Warning: Conflicts with page.tsx!
-```
-
-If you need both a page and an API at the same path, use different paths:
-
-```
-app/
-├── users/
-│ └── page.tsx # /users (page)
-└── api/
- └── users/
- └── route.ts # /api/users (API)
-```
-
-## Environment Behavior
-
-Route handlers run in a **Server Component-like environment**:
-
-- Yes: Can use `async/await`
-- Yes: Can access `cookies()`, `headers()`
-- Yes: Can use Node.js APIs
-- No: Cannot use React hooks
-- No: Cannot use React DOM APIs
-- No: Cannot use browser APIs
-
-```tsx
-// Bad: This won't work - no React DOM in route handlers
-import { renderToString } from 'react-dom/server'
-
-export async function GET() {
- const html = renderToString( ) // Error!
- return new Response(html)
-}
-```
-
-## Dynamic Route Handlers
-
-```tsx
-// app/api/users/[id]/route.ts
-export async function GET(
- request: Request,
- { params }: { params: Promise<{ id: string }> }
-) {
- const { id } = await params
- const user = await getUser(id)
-
- if (!user) {
- return Response.json({ error: 'Not found' }, { status: 404 })
- }
-
- return Response.json(user)
-}
-```
-
-## Request Helpers
-
-```tsx
-export async function GET(request: Request) {
- // URL and search params
- const { searchParams } = new URL(request.url)
- const query = searchParams.get('q')
-
- // Headers
- const authHeader = request.headers.get('authorization')
-
- // Cookies (Next.js helper)
- const cookieStore = await cookies()
- const token = cookieStore.get('token')
-
- return Response.json({ query, token })
-}
-```
-
-## Response Helpers
-
-```tsx
-// JSON response
-return Response.json({ data })
-
-// With status
-return Response.json({ error: 'Not found' }, { status: 404 })
-
-// With headers
-return Response.json(data, {
- headers: {
- 'Cache-Control': 'max-age=3600',
- },
-})
-
-// Redirect
-return Response.redirect(new URL('/login', request.url))
-
-// Stream
-return new Response(stream, {
- headers: { 'Content-Type': 'text/event-stream' },
-})
-```
-
-## When to Use Route Handlers vs Server Actions
-
-| Use Case | Route Handlers | Server Actions |
-|----------|----------------|----------------|
-| Form submissions | No | Yes |
-| Data mutations from UI | No | Yes |
-| Third-party webhooks | Yes | No |
-| External API consumption | Yes | No |
-| Public REST API | Yes | No |
-| File uploads | Both work | Both work |
-
-**Prefer Server Actions** for mutations triggered from your UI.
-**Use Route Handlers** for external integrations and public APIs.
diff --git a/.agents/skills/next-best-practices/rsc-boundaries.md b/.agents/skills/next-best-practices/rsc-boundaries.md
deleted file mode 100644
index 0c2208d3..00000000
--- a/.agents/skills/next-best-practices/rsc-boundaries.md
+++ /dev/null
@@ -1,159 +0,0 @@
-# RSC Boundaries
-
-Detect and prevent invalid patterns when crossing Server/Client component boundaries.
-
-## Detection Rules
-
-### 1. Async Client Components Are Invalid
-
-Client components **cannot** be async functions. Only Server Components can be async.
-
-**Detect:** File has `'use client'` AND component is `async function` or returns `Promise`
-
-```tsx
-// Bad: async client component
-'use client'
-export default async function UserProfile() {
- const user = await getUser() // Cannot await in client component
- return {user.name}
-}
-
-// Good: Remove async, fetch data in parent server component
-// page.tsx (server component - no 'use client')
-export default async function Page() {
- const user = await getUser()
- return
-}
-
-// UserProfile.tsx (client component)
-'use client'
-export function UserProfile({ user }: { user: User }) {
- return {user.name}
-}
-```
-
-```tsx
-// Bad: async arrow function client component
-'use client'
-const Dashboard = async () => {
- const data = await fetchDashboard()
- return {data}
-}
-
-// Good: Fetch in server component, pass data down
-```
-
-### 2. Non-Serializable Props to Client Components
-
-Props passed from Server → Client must be JSON-serializable.
-
-**Detect:** Server component passes these to a client component:
-- Functions (except Server Actions with `'use server'`)
-- `Date` objects
-- `Map`, `Set`, `WeakMap`, `WeakSet`
-- Class instances
-- `Symbol` (unless globally registered)
-- Circular references
-
-```tsx
-// Bad: Function prop
-// page.tsx (server)
-export default function Page() {
- const handleClick = () => console.log('clicked')
- return
-}
-
-// Good: Define function inside client component
-// ClientButton.tsx
-'use client'
-export function ClientButton() {
- const handleClick = () => console.log('clicked')
- return Click
-}
-```
-
-```tsx
-// Bad: Date object (silently becomes string, then crashes)
-// page.tsx (server)
-export default async function Page() {
- const post = await getPost()
- return // Date object
-}
-
-// PostCard.tsx (client) - will crash on .getFullYear()
-'use client'
-export function PostCard({ createdAt }: { createdAt: Date }) {
- return {createdAt.getFullYear()} // Runtime error!
-}
-
-// Good: Serialize to string on server
-// page.tsx (server)
-export default async function Page() {
- const post = await getPost()
- return
-}
-
-// PostCard.tsx (client)
-'use client'
-export function PostCard({ createdAt }: { createdAt: string }) {
- const date = new Date(createdAt)
- return {date.getFullYear()}
-}
-```
-
-```tsx
-// Bad: Class instance
-const user = new UserModel(data)
- // Methods will be stripped
-
-// Good: Pass plain object
-const user = await getUser()
-
-```
-
-```tsx
-// Bad: Map/Set
-
-
-// Good: Convert to array/object
-
-
-```
-
-### 3. Server Actions Are the Exception
-
-Functions marked with `'use server'` CAN be passed to client components.
-
-```tsx
-// Valid: Server Action can be passed
-// actions.ts
-'use server'
-export async function submitForm(formData: FormData) {
- // server-side logic
-}
-
-// page.tsx (server)
-import { submitForm } from './actions'
-export default function Page() {
- return // OK!
-}
-
-// ClientForm.tsx (client)
-'use client'
-export function ClientForm({ onSubmit }: { onSubmit: (data: FormData) => Promise }) {
- return
-}
-```
-
-## Quick Reference
-
-| Pattern | Valid? | Fix |
-|---------|--------|-----|
-| `'use client'` + `async function` | No | Fetch in server parent, pass data |
-| Pass `() => {}` to client | No | Define in client or use server action |
-| Pass `new Date()` to client | No | Use `.toISOString()` |
-| Pass `new Map()` to client | No | Convert to object/array |
-| Pass class instance to client | No | Pass plain object |
-| Pass server action to client | Yes | - |
-| Pass `string/number/boolean` | Yes | - |
-| Pass plain object/array | Yes | - |
diff --git a/.agents/skills/next-best-practices/runtime-selection.md b/.agents/skills/next-best-practices/runtime-selection.md
deleted file mode 100644
index cec960d7..00000000
--- a/.agents/skills/next-best-practices/runtime-selection.md
+++ /dev/null
@@ -1,39 +0,0 @@
-# Runtime Selection
-
-## Use Node.js Runtime by Default
-
-Use the default Node.js runtime for new routes and pages. Only use Edge runtime if the project already uses it or there's a specific requirement.
-
-```tsx
-// Good: Default - no runtime config needed (uses Node.js)
-export default function Page() { ... }
-
-// Caution: Only if already used in project or specifically required
-export const runtime = 'edge'
-```
-
-## When to Use Each
-
-### Node.js Runtime (Default)
-
-- Full Node.js API support
-- File system access (`fs`)
-- Full `crypto` support
-- Database connections
-- Most npm packages work
-
-### Edge Runtime
-
-- Only for specific edge-location latency requirements
-- Limited API (no `fs`, limited `crypto`)
-- Smaller cold start
-- Geographic distribution needs
-
-## Detection
-
-**Before adding `runtime = 'edge'`**, check:
-1. Does the project already use Edge runtime?
-2. Is there a specific latency requirement?
-3. Are all dependencies Edge-compatible?
-
-If unsure, use Node.js runtime.
diff --git a/.agents/skills/next-best-practices/scripts.md b/.agents/skills/next-best-practices/scripts.md
deleted file mode 100644
index 4eeb744c..00000000
--- a/.agents/skills/next-best-practices/scripts.md
+++ /dev/null
@@ -1,141 +0,0 @@
-# Scripts
-
-Loading third-party scripts in Next.js.
-
-## Use next/script
-
-Always use `next/script` instead of native `
-
-// Good: Next.js Script component
-import Script from 'next/script'
-
-
-```
-
-## Inline Scripts Need ID
-
-Inline scripts require an `id` attribute for Next.js to track them.
-
-```tsx
-// Bad: Missing id
-
-
-// Good: Has id
-
-
-// Good: Inline with id
-
-```
-
-## Don't Put Script in Head
-
-`next/script` should not be placed inside `next/head`. It handles its own positioning.
-
-```tsx
-// Bad: Script inside Head
-import Head from 'next/head'
-import Script from 'next/script'
-
-
-
-
-
-// Good: Script outside Head
-
- Page
-
-
-```
-
-## Loading Strategies
-
-```tsx
-// afterInteractive (default) - Load after page is interactive
-
-
-// lazyOnload - Load during idle time
-
-
-// beforeInteractive - Load before page is interactive (use sparingly)
-// Only works in app/layout.tsx or pages/_document.js
-
-
-// worker - Load in web worker (experimental)
-
-```
-
-## Google Analytics
-
-Use `@next/third-parties` instead of inline GA scripts.
-
-```tsx
-// Bad: Inline GA script
-
-
-
-// Good: Next.js component
-import { GoogleAnalytics } from '@next/third-parties/google'
-
-export default function Layout({ children }) {
- return (
-
- {children}
-
-
- )
-}
-```
-
-## Google Tag Manager
-
-```tsx
-import { GoogleTagManager } from '@next/third-parties/google'
-
-export default function Layout({ children }) {
- return (
-
-
- {children}
-
- )
-}
-```
-
-## Other Third-Party Scripts
-
-```tsx
-// YouTube embed
-import { YouTubeEmbed } from '@next/third-parties/google'
-
-
-
-// Google Maps
-import { GoogleMapsEmbed } from '@next/third-parties/google'
-
-
-```
-
-## Quick Reference
-
-| Pattern | Issue | Fix |
-|---------|-------|-----|
-| `
-
-// Good: Next.js Script component
-import Script from 'next/script'
-
-
-```
-
-## Inline Scripts Need ID
-
-Inline scripts require an `id` attribute for Next.js to track them.
-
-```tsx
-// Bad: Missing id
-
-
-// Good: Has id
-
-
-// Good: Inline with id
-
-```
-
-## Don't Put Script in Head
-
-`next/script` should not be placed inside `next/head`. It handles its own positioning.
-
-```tsx
-// Bad: Script inside Head
-import Head from 'next/head'
-import Script from 'next/script'
-
-
-
-
-
-// Good: Script outside Head
-
- Page
-
-
-```
-
-## Loading Strategies
-
-```tsx
-// afterInteractive (default) - Load after page is interactive
-
-
-// lazyOnload - Load during idle time
-
-
-// beforeInteractive - Load before page is interactive (use sparingly)
-// Only works in app/layout.tsx or pages/_document.js
-
-
-// worker - Load in web worker (experimental)
-
-```
-
-## Google Analytics
-
-Use `@next/third-parties` instead of inline GA scripts.
-
-```tsx
-// Bad: Inline GA script
-
-
-
-// Good: Next.js component
-import { GoogleAnalytics } from '@next/third-parties/google'
-
-export default function Layout({ children }) {
- return (
-
- {children}
-
-
- )
-}
-```
-
-## Google Tag Manager
-
-```tsx
-import { GoogleTagManager } from '@next/third-parties/google'
-
-export default function Layout({ children }) {
- return (
-
-
- {children}
-
- )
-}
-```
-
-## Other Third-Party Scripts
-
-```tsx
-// YouTube embed
-import { YouTubeEmbed } from '@next/third-parties/google'
-
-
-
-// Google Maps
-import { GoogleMapsEmbed } from '@next/third-parties/google'
-
-
-```
-
-## Quick Reference
-
-| Pattern | Issue | Fix |
-|---------|-------|-----|
-| `
-
-// Good: Next.js Script component
-import Script from 'next/script'
-
-
-```
-
-## Inline Scripts Need ID
-
-Inline scripts require an `id` attribute for Next.js to track them.
-
-```tsx
-// Bad: Missing id
-
-
-// Good: Has id
-
-
-// Good: Inline with id
-
-```
-
-## Don't Put Script in Head
-
-`next/script` should not be placed inside `next/head`. It handles its own positioning.
-
-```tsx
-// Bad: Script inside Head
-import Head from 'next/head'
-import Script from 'next/script'
-
-
-
-
-
-// Good: Script outside Head
-
- Page
-
-
-```
-
-## Loading Strategies
-
-```tsx
-// afterInteractive (default) - Load after page is interactive
-
-
-// lazyOnload - Load during idle time
-
-
-// beforeInteractive - Load before page is interactive (use sparingly)
-// Only works in app/layout.tsx or pages/_document.js
-
-
-// worker - Load in web worker (experimental)
-
-```
-
-## Google Analytics
-
-Use `@next/third-parties` instead of inline GA scripts.
-
-```tsx
-// Bad: Inline GA script
-
-
-
-// Good: Next.js component
-import { GoogleAnalytics } from '@next/third-parties/google'
-
-export default function Layout({ children }) {
- return (
-
- {children}
-
-
- )
-}
-```
-
-## Google Tag Manager
-
-```tsx
-import { GoogleTagManager } from '@next/third-parties/google'
-
-export default function Layout({ children }) {
- return (
-
-
- {children}
-
- )
-}
-```
-
-## Other Third-Party Scripts
-
-```tsx
-// YouTube embed
-import { YouTubeEmbed } from '@next/third-parties/google'
-
-
-
-// Google Maps
-import { GoogleMapsEmbed } from '@next/third-parties/google'
-
-
-```
-
-## Quick Reference
-
-| Pattern | Issue | Fix |
-|---------|-------|-----|
-| `
-
-// Good: Next.js Script component
-import Script from 'next/script'
-
-
-```
-
-## Inline Scripts Need ID
-
-Inline scripts require an `id` attribute for Next.js to track them.
-
-```tsx
-// Bad: Missing id
-
-
-// Good: Has id
-
-
-// Good: Inline with id
-
-```
-
-## Don't Put Script in Head
-
-`next/script` should not be placed inside `next/head`. It handles its own positioning.
-
-```tsx
-// Bad: Script inside Head
-import Head from 'next/head'
-import Script from 'next/script'
-
-
-
-
-
-// Good: Script outside Head
-
- Page
-
-
-```
-
-## Loading Strategies
-
-```tsx
-// afterInteractive (default) - Load after page is interactive
-
-
-// lazyOnload - Load during idle time
-
-
-// beforeInteractive - Load before page is interactive (use sparingly)
-// Only works in app/layout.tsx or pages/_document.js
-
-
-// worker - Load in web worker (experimental)
-
-```
-
-## Google Analytics
-
-Use `@next/third-parties` instead of inline GA scripts.
-
-```tsx
-// Bad: Inline GA script
-
-
-
-// Good: Next.js component
-import { GoogleAnalytics } from '@next/third-parties/google'
-
-export default function Layout({ children }) {
- return (
-
- {children}
-
-
- )
-}
-```
-
-## Google Tag Manager
-
-```tsx
-import { GoogleTagManager } from '@next/third-parties/google'
-
-export default function Layout({ children }) {
- return (
-
-
- {children}
-
- )
-}
-```
-
-## Other Third-Party Scripts
-
-```tsx
-// YouTube embed
-import { YouTubeEmbed } from '@next/third-parties/google'
-
-
-
-// Google Maps
-import { GoogleMapsEmbed } from '@next/third-parties/google'
-
-
-```
-
-## Quick Reference
-
-| Pattern | Issue | Fix |
-|---------|-------|-----|
-| `