feat: biblioteca inteligente libs/ + 5 novas skills (20 skills total)
NOVAS SKILLS: - next-best-practices v0.1.0 (CLEAN) — Next.js App Router, RSC, caching, data - nextjs-patterns v1.0.0 (CLEAN) — Next.js 15: Server Actions, route handlers - vite v1.0.0 (CLEAN) — env vars, aliases, proxy, CJS compat - uncle-bob v1.0.0 (CLEAN) — Clean Code, SOLID, Clean Architecture - clean-code-review v1.0.0 (CLEAN) — naming, guard clauses, anti-patterns, refactoring - vue v1.0.0 (CLEAN) — Vue framework - vue-composition-api-best-practices v1.0.0 (CLEAN) — composables, Pinia, reactivity BIBLIOTECA INTELIGENTE libs/ (10 dominios, 11 arquivos): - typescript/ — TS safe + generics gotchas - react/ — Next.js App Router + Vite config - vue/ — Composition API + Pinia - linux/ — System diagnostic cheatsheet - database/ — PostgreSQL + MySQL patterns - browser/ — Chromium CLI + E2E testing - security/ — SAST audit (OWASP Top 10) - best-practices/ — Clean Code + SOLID + Clean Architecture - deploy/ — Docker multi-stack + OpenClaw ops - + INDEX.md como guia de navegacao .learnings/ — LRN-20260519-003 criado (biblioteca compartilhada)
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"version": 1,
|
||||
"registry": "https://clawhub.ai",
|
||||
"slug": "nextjs-patterns",
|
||||
"installedVersion": "1.0.0",
|
||||
"installedAt": 1779235115388,
|
||||
"fingerprint": "bc26850b7f1dac7a77618c50aab5563296d26699e6b00b8a31bab25e950849b4"
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
---
|
||||
name: nextjs-patterns
|
||||
description: >
|
||||
Apply Next.js 15 best practices and modern patterns including App Router,
|
||||
Server Components, Server Actions, caching strategies, and performance
|
||||
optimization. Use when building or reviewing Next.js 15 applications to
|
||||
ensure idiomatic, production-ready code.
|
||||
---
|
||||
|
||||
# Next.js 15 Best Practices
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. **Default to Server Components** — only add `"use client"` when you need interactivity or browser APIs
|
||||
2. **Colocate by feature** — keep components, hooks, and utils near the routes that use them
|
||||
3. **Type everything** — leverage TypeScript with strict mode enabled
|
||||
4. **Cache deliberately** — understand the four caching layers and opt in/out explicitly
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
app/
|
||||
(marketing)/ # route group: no URL segment
|
||||
page.tsx
|
||||
(dashboard)/
|
||||
layout.tsx
|
||||
[id]/
|
||||
page.tsx
|
||||
api/
|
||||
route.ts
|
||||
components/
|
||||
ui/ # shared, "dumb" UI components
|
||||
features/ # feature-specific components
|
||||
lib/
|
||||
db.ts # database client (singleton)
|
||||
auth.ts # auth helpers
|
||||
utils.ts
|
||||
```
|
||||
|
||||
## Server vs. Client Components
|
||||
|
||||
```tsx
|
||||
// ✅ Server Component (default) — runs on server, no JS sent to client
|
||||
export default async function ProductList() {
|
||||
const products = await db.product.findMany()
|
||||
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>
|
||||
}
|
||||
|
||||
// ✅ Client Component — only when needed
|
||||
"use client"
|
||||
import { useState } from "react"
|
||||
export function Counter() {
|
||||
const [n, setN] = useState(0)
|
||||
return <button onClick={() => setN(n + 1)}>{n}</button>
|
||||
}
|
||||
```
|
||||
|
||||
## Data Fetching Patterns
|
||||
|
||||
```tsx
|
||||
// Parallel fetching (avoid sequential waterfalls)
|
||||
export default async function Dashboard() {
|
||||
const [user, stats] = await Promise.all([
|
||||
fetchUser(),
|
||||
fetchStats(),
|
||||
])
|
||||
return <View user={user} stats={stats} />
|
||||
}
|
||||
|
||||
// fetch with cache control
|
||||
const data = await fetch("https://api.example.com/data", {
|
||||
next: { revalidate: 60 }, // ISR: revalidate every 60s
|
||||
// cache: "no-store" // always fresh
|
||||
// cache: "force-cache" // static, until manual revalidation
|
||||
})
|
||||
```
|
||||
|
||||
## Server Actions
|
||||
|
||||
```tsx
|
||||
// app/actions.ts
|
||||
"use server"
|
||||
import { revalidatePath } from "next/cache"
|
||||
|
||||
export async function createPost(formData: FormData) {
|
||||
const title = formData.get("title") as string
|
||||
await db.post.create({ data: { title } })
|
||||
revalidatePath("/posts")
|
||||
}
|
||||
|
||||
// app/posts/new/page.tsx
|
||||
import { createPost } from "../actions"
|
||||
export default function NewPost() {
|
||||
return (
|
||||
<form action={createPost}>
|
||||
<input name="title" />
|
||||
<button type="submit">Create</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Metadata & SEO
|
||||
|
||||
```tsx
|
||||
// Static metadata
|
||||
export const metadata = {
|
||||
title: "My App",
|
||||
description: "...",
|
||||
}
|
||||
|
||||
// Dynamic metadata
|
||||
export async function generateMetadata({ params }) {
|
||||
const post = await fetchPost(params.slug)
|
||||
return { title: post.title, description: post.excerpt }
|
||||
}
|
||||
```
|
||||
|
||||
## Error & Loading States
|
||||
|
||||
```tsx
|
||||
// app/posts/loading.tsx — automatic Suspense boundary
|
||||
export default function Loading() {
|
||||
return <Skeleton />
|
||||
}
|
||||
|
||||
// app/posts/error.tsx — automatic error boundary
|
||||
"use client"
|
||||
export default function Error({ error, reset }) {
|
||||
return <button onClick={reset}>Retry: {error.message}</button>
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Checklist
|
||||
|
||||
- [ ] Images use `next/image` with explicit `width`/`height`
|
||||
- [ ] Fonts use `next/font` (zero layout shift)
|
||||
- [ ] Dynamic imports for heavy client components: `dynamic(() => import(...))`
|
||||
- [ ] `generateStaticParams` for known dynamic routes
|
||||
- [ ] Bundle analyzer run: `ANALYZE=true next build`
|
||||
- [ ] Partial Prerendering (PPR) considered for mixed static/dynamic pages
|
||||
|
||||
## References
|
||||
|
||||
See `references/` folder for routing patterns, caching deep-dive, and migration guide.
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "kn753sxg7hag86tcm2dtgzyhns84chhv",
|
||||
"slug": "nextjs-patterns",
|
||||
"version": "1.0.0",
|
||||
"publishedAt": 1776047751623
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
# Next.js 15 Caching Deep Dive
|
||||
|
||||
## The Four Caching Layers
|
||||
|
||||
### 1. Request Memoization (in-memory, per request)
|
||||
Automatically deduplicates identical `fetch()` calls within a single render tree.
|
||||
|
||||
```tsx
|
||||
// Both components call fetchUser(id) — only ONE network request is made
|
||||
async function Avatar({ id }) { const u = await fetchUser(id); return <img src={u.avatar} /> }
|
||||
async function Name({ id }) { const u = await fetchUser(id); return <span>{u.name}</span> }
|
||||
```
|
||||
|
||||
### 2. Data Cache (persistent, cross-request)
|
||||
`fetch()` responses are stored on the server and reused across requests and deployments.
|
||||
|
||||
```tsx
|
||||
// Static — cached indefinitely
|
||||
fetch(url)
|
||||
fetch(url, { cache: "force-cache" })
|
||||
|
||||
// Time-based revalidation (ISR)
|
||||
fetch(url, { next: { revalidate: 3600 } }) // revalidate every hour
|
||||
|
||||
// Always fresh — no caching
|
||||
fetch(url, { cache: "no-store" })
|
||||
fetch(url, { next: { revalidate: 0 } })
|
||||
```
|
||||
|
||||
### 3. Full Route Cache (build-time static rendering)
|
||||
Pages rendered at build time are stored as static HTML+RSC payload.
|
||||
|
||||
```tsx
|
||||
// Force dynamic rendering for this route
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
// Custom revalidation period for the whole route
|
||||
export const revalidate = 60
|
||||
```
|
||||
|
||||
### 4. Router Cache (client-side, per session)
|
||||
Browser caches RSC payloads for instant back/forward navigation.
|
||||
- Static routes: cached for 5 minutes
|
||||
- Dynamic routes: cached for 30 seconds
|
||||
|
||||
## On-Demand Revalidation
|
||||
|
||||
```ts
|
||||
// Revalidate by path
|
||||
import { revalidatePath } from "next/cache"
|
||||
revalidatePath("/blog")
|
||||
revalidatePath("/blog/[slug]", "page")
|
||||
|
||||
// Revalidate by cache tag
|
||||
import { revalidateTag } from "next/cache"
|
||||
revalidateTag("posts")
|
||||
|
||||
// Tagging fetches
|
||||
fetch(url, { next: { tags: ["posts"] } })
|
||||
```
|
||||
|
||||
## unstable_cache for Non-Fetch Data
|
||||
|
||||
```ts
|
||||
import { unstable_cache } from "next/cache"
|
||||
|
||||
const getCachedUser = unstable_cache(
|
||||
async (id: string) => db.user.findUnique({ where: { id } }),
|
||||
["user"],
|
||||
{ revalidate: 300, tags: ["users"] }
|
||||
)
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
| Mistake | Fix |
|
||||
|---------|-----|
|
||||
| Using `fetch` inside `useEffect` | Move to Server Component or use React Query |
|
||||
| Forgetting `revalidatePath` after mutations | Call in Server Action after every write |
|
||||
| Over-caching user-specific data | Add `cache: "no-store"` or check `cookies()`/`headers()` |
|
||||
| Sequential awaits | Use `Promise.all()` for parallel fetching |
|
||||
@@ -0,0 +1,104 @@
|
||||
# Next.js 15 Migration & Configuration Guide
|
||||
|
||||
## Upgrading to Next.js 15
|
||||
|
||||
```bash
|
||||
npx @next/codemod@canary upgrade latest
|
||||
# or manual:
|
||||
npm install next@latest react@latest react-dom@latest
|
||||
```
|
||||
|
||||
## Key Breaking Changes (14 → 15)
|
||||
|
||||
### 1. Async Request APIs (Breaking)
|
||||
`cookies()`, `headers()`, `params`, `searchParams` are now async:
|
||||
|
||||
```tsx
|
||||
// Before (Next.js 14)
|
||||
export default function Page({ params }) {
|
||||
const { id } = params
|
||||
}
|
||||
|
||||
// After (Next.js 15)
|
||||
export default async function Page({ params }) {
|
||||
const { id } = await params
|
||||
}
|
||||
|
||||
// cookies and headers
|
||||
import { cookies, headers } from "next/headers"
|
||||
const cookieStore = await cookies()
|
||||
const headersList = await headers()
|
||||
```
|
||||
|
||||
### 2. Caching Defaults Changed
|
||||
`fetch()` no longer caches by default (was `force-cache`, now `no-store`).
|
||||
Add `{ cache: "force-cache" }` or set route-level `export const fetchCache = "default-cache"` to restore.
|
||||
|
||||
### 3. React 19 Compatibility
|
||||
Next.js 15 supports React 19. New hooks available:
|
||||
- `useActionState` (replaces `useFormState`)
|
||||
- `useFormStatus`
|
||||
- `use()` for reading promises/context
|
||||
|
||||
## next.config.ts (TypeScript config)
|
||||
|
||||
```ts
|
||||
import type { NextConfig } from "next"
|
||||
|
||||
const config: NextConfig = {
|
||||
experimental: {
|
||||
ppr: "incremental", // Partial Prerendering
|
||||
reactCompiler: true, // React Compiler (auto-memoization)
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{ protocol: "https", hostname: "images.example.com" },
|
||||
],
|
||||
},
|
||||
logging: {
|
||||
fetches: { fullUrl: true }, // Log all fetch calls in dev
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
# .env.local
|
||||
DATABASE_URL="postgresql://..."
|
||||
NEXT_PUBLIC_API_URL="https://api.example.com" # exposed to browser
|
||||
```
|
||||
|
||||
```ts
|
||||
// Access server-side
|
||||
process.env.DATABASE_URL
|
||||
|
||||
// Access client-side (only NEXT_PUBLIC_ prefix)
|
||||
process.env.NEXT_PUBLIC_API_URL
|
||||
```
|
||||
|
||||
## Turbopack (Default in Dev)
|
||||
|
||||
```bash
|
||||
next dev # uses Turbopack by default in Next.js 15
|
||||
next dev --turbopack # explicit flag (same behavior)
|
||||
next build # Webpack still default for production builds
|
||||
```
|
||||
|
||||
## TypeScript Path Aliases
|
||||
|
||||
```json
|
||||
// tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"],
|
||||
"@/components/*": ["./components/*"],
|
||||
"@/lib/*": ["./lib/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,122 @@
|
||||
# Next.js 15 Routing Patterns
|
||||
|
||||
## App Router Fundamentals
|
||||
|
||||
| File | Purpose |
|
||||
|---------------|----------------------------------------------|
|
||||
| `page.tsx` | Unique UI for a route, makes it publicly accessible |
|
||||
| `layout.tsx` | Shared UI; does NOT re-render on navigation |
|
||||
| `template.tsx`| Like layout but re-renders on navigation |
|
||||
| `loading.tsx` | Instant loading state (React Suspense) |
|
||||
| `error.tsx` | Error UI boundary (must be Client Component) |
|
||||
| `not-found.tsx`| 404 UI |
|
||||
| `route.ts` | API endpoint (GET, POST, etc.) |
|
||||
|
||||
## Route Groups
|
||||
|
||||
```
|
||||
app/
|
||||
(auth)/
|
||||
login/page.tsx → /login
|
||||
register/page.tsx → /register
|
||||
(app)/
|
||||
layout.tsx ← shared auth-required layout
|
||||
dashboard/page.tsx → /dashboard
|
||||
```
|
||||
|
||||
Route groups `(name)` organize routes without affecting the URL.
|
||||
|
||||
## Dynamic Routes
|
||||
|
||||
```tsx
|
||||
// app/blog/[slug]/page.tsx
|
||||
export default function Post({ params }: { params: { slug: string } }) {
|
||||
return <h1>{params.slug}</h1>
|
||||
}
|
||||
|
||||
// Generate static routes at build time
|
||||
export async function generateStaticParams() {
|
||||
const posts = await fetchAllPosts()
|
||||
return posts.map(p => ({ slug: p.slug }))
|
||||
}
|
||||
```
|
||||
|
||||
## Catch-All & Optional Catch-All
|
||||
|
||||
```
|
||||
app/shop/[...categories]/page.tsx → /shop/a/b/c
|
||||
app/shop/[[...categories]]/page.tsx → /shop AND /shop/a/b/c
|
||||
```
|
||||
|
||||
## Parallel Routes (Advanced)
|
||||
|
||||
```
|
||||
app/
|
||||
@team/page.tsx
|
||||
@analytics/page.tsx
|
||||
layout.tsx ← receives { team, analytics } props
|
||||
```
|
||||
|
||||
```tsx
|
||||
// layout.tsx
|
||||
export default function Layout({ children, team, analytics }) {
|
||||
return (
|
||||
<div>
|
||||
{children}
|
||||
<aside>{team}</aside>
|
||||
<aside>{analytics}</aside>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Intercepting Routes
|
||||
|
||||
```
|
||||
app/
|
||||
feed/page.tsx
|
||||
(..)photo/[id]/page.tsx ← intercepts /photo/[id] when navigated from /feed
|
||||
photo/[id]/page.tsx ← full page on direct URL visit
|
||||
```
|
||||
|
||||
Useful for modals that maintain background context.
|
||||
|
||||
## Middleware
|
||||
|
||||
```ts
|
||||
// middleware.ts (project root)
|
||||
import { NextResponse } from "next/server"
|
||||
import type { NextRequest } from "next/server"
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const token = request.cookies.get("token")
|
||||
if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
|
||||
return NextResponse.redirect(new URL("/login", request.url))
|
||||
}
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/dashboard/:path*", "/api/protected/:path*"],
|
||||
}
|
||||
```
|
||||
|
||||
## API Routes (Route Handlers)
|
||||
|
||||
```ts
|
||||
// app/api/posts/route.ts
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const page = searchParams.get("page") ?? "1"
|
||||
const posts = await fetchPosts(parseInt(page))
|
||||
return NextResponse.json({ posts })
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json()
|
||||
const post = await createPost(body)
|
||||
return NextResponse.json(post, { status: 201 })
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user