nextjs-app-router
**Scope**: App Router patterns for portfolio sites: Server vs Client components, metadata API, static generation, data fetching. **Version range**: Next.js 13.4+ (App Router stable), 14+ recommended **Generated**: 2026-04-12
$ npx -y skills add notque/vexjoy-agent --agent claude-codeHow it fires
How this agent gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
Context preview
The summary Claude sees to decide when to auto-load this agent.
**Scope**: App Router patterns for portfolio sites: Server vs Client components, metadata API, static generation, data fetching. **Version range**: Next.js 13.4+ (App Router stable), 14+ recommended **Generated**: 2026-04-12
Agent definition
nextjs-app-router.mdNext.js App Router Reference
> **Scope**: App Router patterns for portfolio sites: Server vs Client components, metadata API, static generation, data fetching. > **Version range**: Next.js 13.4+ (App Router stable), 14+ recommended > **Generated**: 2026-04-12
---
Pattern Table
| Pattern | Version | Use When | Avoid When | |---------|---------|----------|------------| | `generateStaticParams` | 13.4+ | Gallery pages with known slugs | Dynamic user input | | `generateMetadata` | 13.4+ | Per-artwork SEO/OG images | Static metadata for all pages | | `export const dynamic = 'force-static'` | 13.4+ | Page must never run server-side | Page reads cookies/headers | | `<Suspense>` with `loading.tsx` | 13.4+ | Data fetching in Server Components | Non-async content | | `unstable_cache` | 14.0+ | Repeated CMS/API fetches | One-off requests |
---
Correct Patterns
Server Component for Gallery Grid
Gallery grids render same content for all visitors. No `'use client'`, fetch directly, zero bundle cost.
// app/gallery/page.tsx — Server Component
export default async function GalleryPage() {
const artworks = await getArtworks()
return <GalleryGrid artworks={artworks} />
}---
Isolate Interactivity at the Leaf
Push `'use client'` as deep as possible to preserve static generation.
// components/ArtworkCard.tsx — Server Component
import { LightboxTrigger } from './LightboxTrigger' // Client Component
export function ArtworkCard({ artwork }: { artwork: Artwork }) {
return (
<article>
<Image src={artwork.src} alt={artwork.alt} width={600} height={400} />
<h2>{artwork.title}</h2>
<LightboxTrigger artworkId={artwork.id} />
</article>
)
}---
Per-Artwork Metadata with generateMetadata
// app/gallery/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const artwork = await getArtworkBySlug(params.slug)
return {
title: `${artwork.title} — Portfolio`,
description: artwork.description,
openGraph: { images: [{ url: artwork.src, width: 1200, height: 630 }] },
}
}
export async function generateStaticParams() {
const artworks = await getArtworkBySlug('*')
return artworks.map(art => ({ slug: art.slug }))
}---
Pattern Catalog
Keep Pages as Server Components
**Detection**:
grep -rn "'use client'" app/ --include="*.tsx" | grep "page.tsx"
Entire page as Client Component disables static generation, ships all metadata as JS. Extract `useState` into a small Client Component leaf.
---
Sync Filter State with URL Search Params
**Detection**:
rg "setFilter\|activeFilter" --type tsx
In-memory filter state disappears on refresh, breaks sharing/back button.
**Preferred action**: Use `useSearchParams` + `useRouter`:
const active = searchParams.get('category') ?? 'all'
const setFilter = (cat: string) => {
const params = new URLSearchParams(searchParams)
if (cat === 'all') params.delete('category')
else params.set('category', cat)
router.push(`?${params.toString()}`, { scroll: false })
}**Note**: `useSearchParams` requires `<Suspense>` in Next.js 14+.
---
Fetch Data in Async Server Components
**Detection**:
grep -rn "getServerSideProps\|getStaticProps" app/ --include="*.tsx"
These are Pages Router APIs — silently ignored in App Router. Fetch in the async component body:
export default async function GalleryPage() {
const artworks = await fetch('...').then(r => r.json())
return <GalleryGrid artworks={artworks} />
}---
Error-Fix Mappings
| Error Message | Root Cause | Fix | |---------------|------------|-----| | `useState is not a function` in Server Component | Missing `'use client'` | Add directive or extract to Client Component | | `useSearchParams() should be wrapped in suspense` | Next.js 14 requirement | Wrap with `<Suspense>` | | `Each child should have a unique "key" prop` | Missing key on mapped elements | Add `key={artwork.id}` | | `Image detected as LCP` without `priority` | Hero not marked priority | Add `priority` prop | | `Un-configured Host` | External domain not in config | Add to `images.remotePatterns` |
---
Version-Specific Notes
| Version | Change | Impact | |---------|--------|--------| | 13.4 | App Router stable | Migrate from Pages Router | | 14.0 | `useSearchParams` requires Suspense | Wrap filter components | | 14.1 | `unstable_cache` for CMS dedup | Use for Sanity/Contentful | | 15.0 | Partial Prerendering experimental | Galleries with dynamic sections |
---
Detection Commands Reference
grep -rn "'use client'" app/ --include="*.tsx" | grep "page.tsx"
grep -rn "getServerSideProps\|getStaticProps" app/ --include="*.tsx"
rg "useState.*category|useState.*filter" --type tsx
grep -rn "useSearchParams" --include="*.tsx" | grep -v "Suspense"
---
See Also
- `image-optimization.md` — next/image props, WebP/AVIF, blur placeholders
- `performance.md` — Core Web Vitals, LCP, bundle analysis
Read more
Next.js App Router Reference
> **Scope**: App Router patterns for portfolio sites: Server vs Client components, metadata API, static generation, data fetching. > **Version range**: Next.js 13.4+ (App Router stable), 14+ recommended > **Generated**: 2026-04-12
---
Pattern Table
| Pattern | Version | Use When | Avoid When | |---------|---------|----------|------------| | `generateStaticParams` | 13.4+ | Gallery pages with known slugs | Dynamic user input | | `generateMetadata` | 13.4+ | Per-artwork SEO/OG images | Static metadata for all pages | | `export const dynamic = 'force-static'` | 13.4+ | Page must never run server-side | Page reads cookies/headers | | `<Suspense>` with `loading.tsx` | 13.4+ | Data fetching in Server Components | Non-async content | | `unstable_cache` | 14.0+ | Repeated CMS/API fetches | One-off requests |
---
Correct Patterns
Server Component for Gallery Grid
Gallery grids render same content for all visitors. No `'use client'`, fetch directly, zero bundle cost.
// app/gallery/page.tsx — Server Component
export default async function GalleryPage() {
const artworks = await getArtworks()
return <GalleryGrid artworks={artworks} />
}---
Isolate Interactivity at the Leaf
Push `'use client'` as deep as possible to preserve static generation.
// components/ArtworkCard.tsx — Server Component
import { LightboxTrigger } from './LightboxTrigger' // Client Component
export function ArtworkCard({ artwork }: { artwork: Artwork }) {
return (
<article>
<Image src={artwork.src} alt={artwork.alt} width={600} height={400} />
<h2>{artwork.title}</h2>
<LightboxTrigger artworkId={artwork.id} />
</article>
)
}---
Per-Artwork Metadata with generateMetadata
// app/gallery/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const artwork = await getArtworkBySlug(params.slug)
return {
title: `${artwork.title} — Portfolio`,
description: artwork.description,
openGraph: { images: [{ url: artwork.src, width: 1200, height: 630 }] },
}
}
export async function generateStaticParams() {
const artworks = await getArtworkBySlug('*')
return artworks.map(art => ({ slug: art.slug }))
}---
Pattern Catalog
Keep Pages as Server Components
**Detection**:
grep -rn "'use client'" app/ --include="*.tsx" | grep "page.tsx"
Entire page as Client Component disables static generation, ships all metadata as JS. Extract `useState` into a small Client Component leaf.
---
Sync Filter State with URL Search Params
**Detection**:
rg "setFilter\|activeFilter" --type tsx
In-memory filter state disappears on refresh, breaks sharing/back button.
**Preferred action**: Use `useSearchParams` + `useRouter`:
const active = searchParams.get('category') ?? 'all'
const setFilter = (cat: string) => {
const params = new URLSearchParams(searchParams)
if (cat === 'all') params.delete('category')
else params.set('category', cat)
router.push(`?${params.toString()}`, { scroll: false })
}**Note**: `useSearchParams` requires `<Suspense>` in Next.js 14+.
---
Fetch Data in Async Server Components
**Detection**:
grep -rn "getServerSideProps\|getStaticProps" app/ --include="*.tsx"
These are Pages Router APIs — silently ignored in App Router. Fetch in the async component body:
export default async function GalleryPage() {
const artworks = await fetch('...').then(r => r.json())
return <GalleryGrid artworks={artworks} />
}---
Error-Fix Mappings
| Error Message | Root Cause | Fix | |---------------|------------|-----| | `useState is not a function` in Server Component | Missing `'use client'` | Add directive or extract to Client Component | | `useSearchParams() should be wrapped in suspense` | Next.js 14 requirement | Wrap with `<Suspense>` | | `Each child should have a unique "key" prop` | Missing key on mapped elements | Add `key={artwork.id}` | | `Image detected as LCP` without `priority` | Hero not marked priority | Add `priority` prop | | `Un-configured Host` | External domain not in config | Add to `images.remotePatterns` |
---
Version-Specific Notes
| Version | Change | Impact | |---------|--------|--------| | 13.4 | App Router stable | Migrate from Pages Router | | 14.0 | `useSearchParams` requires Suspense | Wrap filter components | | 14.1 | `unstable_cache` for CMS dedup | Use for Sanity/Contentful | | 15.0 | Partial Prerendering experimental | Galleries with dynamic sections |
---
Detection Commands Reference
grep -rn "'use client'" app/ --include="*.tsx" | grep "page.tsx" grep -rn "getServerSideProps\|getStaticProps" app/ --include="*.tsx" rg "useState.*category|useState.*filter" --type tsx grep -rn "useSearchParams" --include="*.tsx" | grep -v "Suspense"
---
See Also
- `image-optimization.md` — next/image props, WebP/AVIF, blur placeholders
- `performance.md` — Core Web Vitals, LCP, bundle analysis
Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.
Repo: notque/vexjoy-agent
Other agents on vexjoy-agent.
- ansible-automation-engineer
Ansible automation: playbooks, roles, collections, Molecule testing, Vault security.
Open agent - modules
**Scope**: Module selection patterns, builtin vs command/shell decisions, collection modules, and version-specific module changes **Version range**: ansible-core 2.14+ / Ansible Collections (community.general 7.0+) **Generated**: 2026-04-04 — verify against current Ansible
Open agent - testing
**Scope**: Molecule test scenarios, ansible-lint rules, idempotency validation, and check-mode patterns **Version range**: Molecule 6.0+ / ansible-lint 6.0+ / ansible-core 2.14+ **Generated**: 2026-04-04 — verify against current Molecule and ansible-lint documentation
Open agent - base-instructions
Universal operational rules injected by /do at agent dispatch. Domain-specific rules live in each agent's .md file.
Open agent - communication-patterns
**Scope**: Failure modes in agent output style — over-reporting, self-congratulation, verbose narration, and hedging. Covers what to detect and how to fix each. **Version range**: all versions **Generated**: 2026-05-11
Open agent - combat-effects-upgrade
Zero-dependency combat visual upgrades: CSS particle replacement, Framer Motion combat juice, CSS 3D card transforms.
Open agent

