@malamute/ai-rules 1.0.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +272 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/_shared/rules/conventions/documentation.md +324 -0
- package/configs/_shared/rules/conventions/git.md +265 -0
- package/configs/_shared/rules/conventions/npm.md +80 -0
- package/configs/_shared/{.claude/rules → rules/conventions}/performance.md +1 -1
- package/configs/_shared/rules/conventions/principles.md +334 -0
- package/configs/_shared/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/rules/devops/docker.md +275 -0
- package/configs/_shared/rules/devops/nx.md +194 -0
- package/configs/_shared/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/rules/lang/python/async.md +337 -0
- package/configs/_shared/rules/lang/python/celery.md +476 -0
- package/configs/_shared/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/rules/lang/python/python.md +172 -0
- package/configs/_shared/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/rules/quality/error-handling.md +48 -0
- package/configs/_shared/rules/quality/logging.md +45 -0
- package/configs/_shared/rules/quality/observability.md +240 -0
- package/configs/_shared/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/rules/security/secrets-management.md +222 -0
- package/configs/_shared/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/{.claude/commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/{.claude/commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/{.claude/commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/angular/{.claude/rules → rules/core}/components.md +69 -15
- package/configs/angular/rules/core/resource.md +285 -0
- package/configs/angular/rules/core/signals.md +323 -0
- package/configs/angular/rules/http.md +338 -0
- package/configs/angular/rules/routing.md +291 -0
- package/configs/angular/rules/ssr.md +312 -0
- package/configs/angular/rules/state/signal-store.md +408 -0
- package/configs/angular/{.claude/rules → rules/state}/state.md +2 -2
- package/configs/angular/{.claude/rules → rules}/testing.md +7 -7
- package/configs/angular/rules/ui/aria.md +422 -0
- package/configs/angular/rules/ui/forms.md +424 -0
- package/configs/angular/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/{.claude/settings.json → settings.json} +3 -0
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/dotnet/rules/background-services.md +552 -0
- package/configs/dotnet/rules/configuration.md +426 -0
- package/configs/dotnet/rules/ddd.md +447 -0
- package/configs/dotnet/rules/dependency-injection.md +343 -0
- package/configs/dotnet/rules/mediatr.md +320 -0
- package/configs/dotnet/rules/middleware.md +489 -0
- package/configs/dotnet/rules/result-pattern.md +363 -0
- package/configs/dotnet/rules/validation.md +388 -0
- package/configs/dotnet/settings.json +29 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/fastapi/rules/background-tasks.md +254 -0
- package/configs/fastapi/rules/dependencies.md +170 -0
- package/configs/{python/.claude → fastapi}/rules/fastapi.md +61 -1
- package/configs/fastapi/rules/lifespan.md +274 -0
- package/configs/fastapi/rules/middleware.md +229 -0
- package/configs/fastapi/rules/pydantic.md +433 -0
- package/configs/fastapi/rules/responses.md +251 -0
- package/configs/fastapi/rules/routers.md +202 -0
- package/configs/fastapi/rules/security.md +222 -0
- package/configs/fastapi/rules/testing.md +251 -0
- package/configs/fastapi/rules/websockets.md +298 -0
- package/configs/fastapi/settings.json +35 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/flask/rules/blueprints.md +208 -0
- package/configs/flask/rules/cli.md +285 -0
- package/configs/flask/rules/configuration.md +281 -0
- package/configs/flask/rules/context.md +238 -0
- package/configs/flask/rules/error-handlers.md +278 -0
- package/configs/flask/rules/extensions.md +278 -0
- package/configs/flask/rules/flask.md +171 -0
- package/configs/flask/rules/marshmallow.md +206 -0
- package/configs/flask/rules/security.md +267 -0
- package/configs/flask/rules/testing.md +284 -0
- package/configs/flask/settings.json +35 -0
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nestjs/rules/common-patterns.md +300 -0
- package/configs/nestjs/rules/filters.md +376 -0
- package/configs/nestjs/rules/interceptors.md +317 -0
- package/configs/nestjs/rules/middleware.md +321 -0
- package/configs/nestjs/{.claude/rules → rules}/modules.md +26 -0
- package/configs/nestjs/rules/pipes.md +351 -0
- package/configs/nestjs/rules/websockets.md +451 -0
- package/configs/nestjs/settings.json +31 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/configs/nextjs/rules/api-routes.md +358 -0
- package/configs/nextjs/rules/authentication.md +355 -0
- package/configs/nextjs/{.claude/rules → rules}/components.md +52 -0
- package/configs/nextjs/rules/data-fetching.md +249 -0
- package/configs/nextjs/rules/database.md +400 -0
- package/configs/nextjs/rules/middleware.md +303 -0
- package/configs/nextjs/rules/routing.md +324 -0
- package/configs/nextjs/rules/seo.md +350 -0
- package/configs/nextjs/rules/server-actions.md +353 -0
- package/configs/nextjs/{.claude/rules → rules}/state/zustand.md +6 -6
- package/configs/nextjs/{.claude/settings.json → settings.json} +7 -0
- package/package.json +24 -9
- package/src/cli.js +218 -0
- package/src/config.js +63 -0
- package/src/index.js +4 -0
- package/src/installer.js +414 -0
- package/src/merge.js +109 -0
- package/src/tech-config.json +45 -0
- package/src/utils.js +88 -0
- package/configs/dotnet/.claude/settings.json +0 -9
- package/configs/nestjs/.claude/settings.json +0 -15
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/{.claude/rules → rules/domain/frontend}/accessibility.md +0 -0
- /package/configs/_shared/{.claude/rules → rules/security}/security.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/debug/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/learning/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/spec/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/git}/review/SKILL.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/api.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/architecture.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/database/efcore.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/testing.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/auth.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/database/prisma.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/database/typeorm.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/testing.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/validation.md +0 -0
- /package/configs/nextjs/{.claude/rules → rules}/state/redux-toolkit.md +0 -0
- /package/configs/nextjs/{.claude/rules → rules}/testing.md +0 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "middleware.ts"
|
|
4
|
+
- "src/middleware.ts"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Next.js Middleware
|
|
8
|
+
|
|
9
|
+
## Basic Structure
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// middleware.ts
|
|
13
|
+
import { NextResponse } from 'next/server';
|
|
14
|
+
import type { NextRequest } from 'next/server';
|
|
15
|
+
|
|
16
|
+
export function middleware(request: NextRequest) {
|
|
17
|
+
// Runs on every matched request
|
|
18
|
+
return NextResponse.next();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const config = {
|
|
22
|
+
matcher: [
|
|
23
|
+
// Match all paths except static files
|
|
24
|
+
'/((?!_next/static|_next/image|favicon.ico).*)',
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Common Patterns
|
|
30
|
+
|
|
31
|
+
### Authentication
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { NextResponse } from 'next/server';
|
|
35
|
+
import type { NextRequest } from 'next/server';
|
|
36
|
+
import { getToken } from 'next-auth/jwt';
|
|
37
|
+
|
|
38
|
+
export async function middleware(request: NextRequest) {
|
|
39
|
+
const token = await getToken({ req: request });
|
|
40
|
+
const { pathname } = request.nextUrl;
|
|
41
|
+
|
|
42
|
+
// Public paths
|
|
43
|
+
const publicPaths = ['/login', '/register', '/api/auth'];
|
|
44
|
+
if (publicPaths.some(path => pathname.startsWith(path))) {
|
|
45
|
+
return NextResponse.next();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Protected paths
|
|
49
|
+
if (!token) {
|
|
50
|
+
const loginUrl = new URL('/login', request.url);
|
|
51
|
+
loginUrl.searchParams.set('callbackUrl', pathname);
|
|
52
|
+
return NextResponse.redirect(loginUrl);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return NextResponse.next();
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Role-Based Access
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
export async function middleware(request: NextRequest) {
|
|
63
|
+
const token = await getToken({ req: request });
|
|
64
|
+
const { pathname } = request.nextUrl;
|
|
65
|
+
|
|
66
|
+
// Admin routes
|
|
67
|
+
if (pathname.startsWith('/admin')) {
|
|
68
|
+
if (token?.role !== 'admin') {
|
|
69
|
+
return NextResponse.redirect(new URL('/forbidden', request.url));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// API admin routes
|
|
74
|
+
if (pathname.startsWith('/api/admin')) {
|
|
75
|
+
if (token?.role !== 'admin') {
|
|
76
|
+
return NextResponse.json(
|
|
77
|
+
{ error: 'Forbidden' },
|
|
78
|
+
{ status: 403 }
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return NextResponse.next();
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Internationalization (i18n)
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { NextResponse } from 'next/server';
|
|
91
|
+
import type { NextRequest } from 'next/server';
|
|
92
|
+
import { match } from '@formatjs/intl-localematcher';
|
|
93
|
+
import Negotiator from 'negotiator';
|
|
94
|
+
|
|
95
|
+
const locales = ['en', 'fr', 'de'];
|
|
96
|
+
const defaultLocale = 'en';
|
|
97
|
+
|
|
98
|
+
function getLocale(request: NextRequest): string {
|
|
99
|
+
const headers = { 'accept-language': request.headers.get('accept-language') || '' };
|
|
100
|
+
const languages = new Negotiator({ headers }).languages();
|
|
101
|
+
return match(languages, locales, defaultLocale);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function middleware(request: NextRequest) {
|
|
105
|
+
const { pathname } = request.nextUrl;
|
|
106
|
+
|
|
107
|
+
// Check if pathname has locale
|
|
108
|
+
const pathnameHasLocale = locales.some(
|
|
109
|
+
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if (pathnameHasLocale) return NextResponse.next();
|
|
113
|
+
|
|
114
|
+
// Redirect to locale
|
|
115
|
+
const locale = getLocale(request);
|
|
116
|
+
request.nextUrl.pathname = `/${locale}${pathname}`;
|
|
117
|
+
return NextResponse.redirect(request.nextUrl);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const config = {
|
|
121
|
+
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
|
|
122
|
+
};
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Rate Limiting
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import { NextResponse } from 'next/server';
|
|
129
|
+
import type { NextRequest } from 'next/server';
|
|
130
|
+
import { Ratelimit } from '@upstash/ratelimit';
|
|
131
|
+
import { Redis } from '@upstash/redis';
|
|
132
|
+
|
|
133
|
+
const ratelimit = new Ratelimit({
|
|
134
|
+
redis: Redis.fromEnv(),
|
|
135
|
+
limiter: Ratelimit.slidingWindow(10, '10 s'),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
export async function middleware(request: NextRequest) {
|
|
139
|
+
if (request.nextUrl.pathname.startsWith('/api')) {
|
|
140
|
+
const ip = request.ip ?? '127.0.0.1';
|
|
141
|
+
const { success, limit, remaining, reset } = await ratelimit.limit(ip);
|
|
142
|
+
|
|
143
|
+
if (!success) {
|
|
144
|
+
return NextResponse.json(
|
|
145
|
+
{ error: 'Too many requests' },
|
|
146
|
+
{
|
|
147
|
+
status: 429,
|
|
148
|
+
headers: {
|
|
149
|
+
'X-RateLimit-Limit': limit.toString(),
|
|
150
|
+
'X-RateLimit-Remaining': remaining.toString(),
|
|
151
|
+
'X-RateLimit-Reset': reset.toString(),
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return NextResponse.next();
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Geolocation Redirect
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
export function middleware(request: NextRequest) {
|
|
166
|
+
const country = request.geo?.country || 'US';
|
|
167
|
+
|
|
168
|
+
// Redirect EU users to EU subdomain
|
|
169
|
+
const euCountries = ['DE', 'FR', 'IT', 'ES', 'NL'];
|
|
170
|
+
if (euCountries.includes(country) && !request.nextUrl.hostname.includes('eu.')) {
|
|
171
|
+
return NextResponse.redirect(
|
|
172
|
+
new URL(request.nextUrl.pathname, 'https://eu.example.com')
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return NextResponse.next();
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Security Headers
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
export function middleware(request: NextRequest) {
|
|
184
|
+
const response = NextResponse.next();
|
|
185
|
+
|
|
186
|
+
// Security headers
|
|
187
|
+
response.headers.set('X-Frame-Options', 'DENY');
|
|
188
|
+
response.headers.set('X-Content-Type-Options', 'nosniff');
|
|
189
|
+
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
190
|
+
response.headers.set(
|
|
191
|
+
'Content-Security-Policy',
|
|
192
|
+
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';"
|
|
193
|
+
);
|
|
194
|
+
response.headers.set(
|
|
195
|
+
'Permissions-Policy',
|
|
196
|
+
'camera=(), microphone=(), geolocation=()'
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
return response;
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Request/Response Modification
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
export function middleware(request: NextRequest) {
|
|
207
|
+
// Add custom header to request
|
|
208
|
+
const requestHeaders = new Headers(request.headers);
|
|
209
|
+
requestHeaders.set('x-request-id', crypto.randomUUID());
|
|
210
|
+
|
|
211
|
+
// Rewrite URL
|
|
212
|
+
if (request.nextUrl.pathname === '/old-path') {
|
|
213
|
+
return NextResponse.rewrite(new URL('/new-path', request.url));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Pass headers to server components
|
|
217
|
+
const response = NextResponse.next({
|
|
218
|
+
request: {
|
|
219
|
+
headers: requestHeaders,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Add header to response
|
|
224
|
+
response.headers.set('x-request-id', requestHeaders.get('x-request-id')!);
|
|
225
|
+
|
|
226
|
+
return response;
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### A/B Testing
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
export function middleware(request: NextRequest) {
|
|
234
|
+
const bucket = request.cookies.get('ab-bucket')?.value;
|
|
235
|
+
|
|
236
|
+
if (!bucket) {
|
|
237
|
+
// Assign to bucket
|
|
238
|
+
const newBucket = Math.random() > 0.5 ? 'a' : 'b';
|
|
239
|
+
const response = NextResponse.next();
|
|
240
|
+
response.cookies.set('ab-bucket', newBucket, {
|
|
241
|
+
httpOnly: true,
|
|
242
|
+
maxAge: 60 * 60 * 24 * 30, // 30 days
|
|
243
|
+
});
|
|
244
|
+
return response;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Rewrite based on bucket
|
|
248
|
+
if (request.nextUrl.pathname === '/landing') {
|
|
249
|
+
return NextResponse.rewrite(
|
|
250
|
+
new URL(`/landing-${bucket}`, request.url)
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return NextResponse.next();
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Matcher Patterns
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
export const config = {
|
|
262
|
+
matcher: [
|
|
263
|
+
// Match all paths except static
|
|
264
|
+
'/((?!_next/static|_next/image|favicon.ico).*)',
|
|
265
|
+
|
|
266
|
+
// Match specific paths
|
|
267
|
+
'/dashboard/:path*',
|
|
268
|
+
'/api/:path*',
|
|
269
|
+
|
|
270
|
+
// Match with regex
|
|
271
|
+
'/(api|admin)/:path*',
|
|
272
|
+
|
|
273
|
+
// Exclude specific paths
|
|
274
|
+
'/((?!api/public)api/:path*)',
|
|
275
|
+
],
|
|
276
|
+
};
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Anti-patterns
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
// BAD: Heavy computation in middleware (runs on every request)
|
|
283
|
+
export async function middleware(request: NextRequest) {
|
|
284
|
+
await heavyDatabaseQuery(); // Blocks all requests!
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// GOOD: Keep middleware lightweight
|
|
288
|
+
export async function middleware(request: NextRequest) {
|
|
289
|
+
// Only quick checks, use Edge-compatible code
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// BAD: Using Node.js APIs (middleware runs on Edge)
|
|
293
|
+
import fs from 'fs'; // Won't work!
|
|
294
|
+
|
|
295
|
+
// GOOD: Use Edge-compatible APIs
|
|
296
|
+
import { Redis } from '@upstash/redis'; // Edge-compatible
|
|
297
|
+
|
|
298
|
+
// BAD: Modifying response body
|
|
299
|
+
return new Response('Modified body'); // Loses Next.js features
|
|
300
|
+
|
|
301
|
+
// GOOD: Use rewrite or redirect
|
|
302
|
+
return NextResponse.rewrite(new URL('/new-path', request.url));
|
|
303
|
+
```
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "apps/**/app/**/*.tsx"
|
|
4
|
+
- "app/**/*.tsx"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Next.js App Router Patterns
|
|
8
|
+
|
|
9
|
+
## Route Structure
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
app/
|
|
13
|
+
├── page.tsx # / (home)
|
|
14
|
+
├── layout.tsx # Root layout
|
|
15
|
+
├── loading.tsx # Root loading
|
|
16
|
+
├── error.tsx # Root error boundary
|
|
17
|
+
├── not-found.tsx # 404 page
|
|
18
|
+
│
|
|
19
|
+
├── (marketing)/ # Route group (not in URL)
|
|
20
|
+
│ ├── layout.tsx # Shared marketing layout
|
|
21
|
+
│ ├── about/page.tsx # /about
|
|
22
|
+
│ └── pricing/page.tsx # /pricing
|
|
23
|
+
│
|
|
24
|
+
├── (app)/ # Route group for app
|
|
25
|
+
│ ├── layout.tsx # App layout (with auth)
|
|
26
|
+
│ ├── dashboard/page.tsx # /dashboard
|
|
27
|
+
│ └── settings/page.tsx # /settings
|
|
28
|
+
│
|
|
29
|
+
├── blog/
|
|
30
|
+
│ ├── page.tsx # /blog
|
|
31
|
+
│ └── [slug]/ # Dynamic segment
|
|
32
|
+
│ ├── page.tsx # /blog/my-post
|
|
33
|
+
│ └── opengraph-image.tsx # OG image generation
|
|
34
|
+
│
|
|
35
|
+
├── docs/
|
|
36
|
+
│ └── [...slug]/ # Catch-all segment
|
|
37
|
+
│ └── page.tsx # /docs/a/b/c
|
|
38
|
+
│
|
|
39
|
+
└── api/ # API routes
|
|
40
|
+
└── webhooks/
|
|
41
|
+
└── stripe/route.ts # /api/webhooks/stripe
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Dynamic Routes
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
// app/blog/[slug]/page.tsx
|
|
48
|
+
interface Props {
|
|
49
|
+
params: Promise<{ slug: string }>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default async function BlogPost({ params }: Props) {
|
|
53
|
+
const { slug } = await params;
|
|
54
|
+
const post = await getPost(slug);
|
|
55
|
+
|
|
56
|
+
if (!post) {
|
|
57
|
+
notFound();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return <article>{post.content}</article>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Generate static params for SSG
|
|
64
|
+
export async function generateStaticParams() {
|
|
65
|
+
const posts = await getPosts();
|
|
66
|
+
return posts.map((post) => ({ slug: post.slug }));
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Catch-All Routes
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
// app/docs/[...slug]/page.tsx
|
|
74
|
+
interface Props {
|
|
75
|
+
params: Promise<{ slug: string[] }>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default async function DocsPage({ params }: Props) {
|
|
79
|
+
const { slug } = await params;
|
|
80
|
+
// slug = ['guide', 'getting-started'] for /docs/guide/getting-started
|
|
81
|
+
|
|
82
|
+
const path = slug.join('/');
|
|
83
|
+
const doc = await getDoc(path);
|
|
84
|
+
|
|
85
|
+
return <DocContent doc={doc} />;
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Route Groups
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
// (marketing) and (app) don't appear in URL
|
|
93
|
+
// but allow different layouts
|
|
94
|
+
|
|
95
|
+
// app/(marketing)/layout.tsx
|
|
96
|
+
export default function MarketingLayout({ children }) {
|
|
97
|
+
return (
|
|
98
|
+
<>
|
|
99
|
+
<MarketingNav />
|
|
100
|
+
<main>{children}</main>
|
|
101
|
+
<Footer />
|
|
102
|
+
</>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// app/(app)/layout.tsx
|
|
107
|
+
export default function AppLayout({ children }) {
|
|
108
|
+
return (
|
|
109
|
+
<>
|
|
110
|
+
<Sidebar />
|
|
111
|
+
<main>{children}</main>
|
|
112
|
+
</>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Parallel Routes
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
// app/layout.tsx with slots
|
|
121
|
+
export default function Layout({
|
|
122
|
+
children,
|
|
123
|
+
modal, // @modal slot
|
|
124
|
+
analytics, // @analytics slot
|
|
125
|
+
}: {
|
|
126
|
+
children: React.ReactNode;
|
|
127
|
+
modal: React.ReactNode;
|
|
128
|
+
analytics: React.ReactNode;
|
|
129
|
+
}) {
|
|
130
|
+
return (
|
|
131
|
+
<html>
|
|
132
|
+
<body>
|
|
133
|
+
{children}
|
|
134
|
+
{modal}
|
|
135
|
+
{analytics}
|
|
136
|
+
</body>
|
|
137
|
+
</html>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// app/@modal/login/page.tsx - Intercepted modal
|
|
142
|
+
// app/@analytics/page.tsx - Analytics slot
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Intercepting Routes
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
// Intercept /photo/123 when navigating from gallery
|
|
149
|
+
// app/gallery/@modal/(.)photo/[id]/page.tsx
|
|
150
|
+
|
|
151
|
+
// (.) - Same level
|
|
152
|
+
// (..) - One level up
|
|
153
|
+
// (..)(..) - Two levels up
|
|
154
|
+
// (...) - Root
|
|
155
|
+
|
|
156
|
+
export default function PhotoModal({ params }: { params: Promise<{ id: string }> }) {
|
|
157
|
+
return (
|
|
158
|
+
<Modal>
|
|
159
|
+
<Photo id={(await params).id} />
|
|
160
|
+
</Modal>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Navigation
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
import Link from 'next/link';
|
|
169
|
+
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
|
|
170
|
+
|
|
171
|
+
// Declarative navigation
|
|
172
|
+
<Link href="/blog/my-post">Read Post</Link>
|
|
173
|
+
<Link href={{ pathname: '/blog', query: { sort: 'date' } }}>Blog</Link>
|
|
174
|
+
|
|
175
|
+
// Prefetch on hover (default)
|
|
176
|
+
<Link href="/dashboard" prefetch={true}>Dashboard</Link>
|
|
177
|
+
|
|
178
|
+
// No prefetch
|
|
179
|
+
<Link href="/admin" prefetch={false}>Admin</Link>
|
|
180
|
+
|
|
181
|
+
// Programmatic navigation
|
|
182
|
+
function NavigationExample() {
|
|
183
|
+
const router = useRouter();
|
|
184
|
+
const pathname = usePathname();
|
|
185
|
+
const searchParams = useSearchParams();
|
|
186
|
+
|
|
187
|
+
function navigate() {
|
|
188
|
+
router.push('/dashboard');
|
|
189
|
+
router.replace('/login'); // No history entry
|
|
190
|
+
router.back();
|
|
191
|
+
router.forward();
|
|
192
|
+
router.refresh(); // Re-fetch server components
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Current URL info
|
|
196
|
+
console.log(pathname); // /blog
|
|
197
|
+
console.log(searchParams.get('page')); // 1
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Middleware
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// middleware.ts (root level)
|
|
205
|
+
import { NextResponse } from 'next/server';
|
|
206
|
+
import type { NextRequest } from 'next/server';
|
|
207
|
+
|
|
208
|
+
export function middleware(request: NextRequest) {
|
|
209
|
+
const { pathname } = request.nextUrl;
|
|
210
|
+
|
|
211
|
+
// Auth check
|
|
212
|
+
const token = request.cookies.get('token');
|
|
213
|
+
if (pathname.startsWith('/dashboard') && !token) {
|
|
214
|
+
return NextResponse.redirect(new URL('/login', request.url));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Rewrite
|
|
218
|
+
if (pathname.startsWith('/api/v1')) {
|
|
219
|
+
return NextResponse.rewrite(new URL('/api/v2' + pathname.slice(7), request.url));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Add headers
|
|
223
|
+
const response = NextResponse.next();
|
|
224
|
+
response.headers.set('x-custom-header', 'value');
|
|
225
|
+
return response;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export const config = {
|
|
229
|
+
matcher: ['/dashboard/:path*', '/api/:path*'],
|
|
230
|
+
};
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Metadata
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
// Static metadata
|
|
237
|
+
export const metadata = {
|
|
238
|
+
title: 'My Blog',
|
|
239
|
+
description: 'A blog about things',
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// Dynamic metadata
|
|
243
|
+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
244
|
+
const { slug } = await params;
|
|
245
|
+
const post = await getPost(slug);
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
title: post.title,
|
|
249
|
+
description: post.excerpt,
|
|
250
|
+
openGraph: {
|
|
251
|
+
title: post.title,
|
|
252
|
+
images: [post.image],
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Route Handlers (API)
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
// app/api/users/route.ts
|
|
262
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
263
|
+
|
|
264
|
+
export async function GET(request: NextRequest) {
|
|
265
|
+
const searchParams = request.nextUrl.searchParams;
|
|
266
|
+
const page = searchParams.get('page') || '1';
|
|
267
|
+
|
|
268
|
+
const users = await getUsers({ page: parseInt(page) });
|
|
269
|
+
return NextResponse.json(users);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export async function POST(request: NextRequest) {
|
|
273
|
+
const body = await request.json();
|
|
274
|
+
const user = await createUser(body);
|
|
275
|
+
return NextResponse.json(user, { status: 201 });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// app/api/users/[id]/route.ts
|
|
279
|
+
export async function GET(
|
|
280
|
+
request: NextRequest,
|
|
281
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
282
|
+
) {
|
|
283
|
+
const { id } = await params;
|
|
284
|
+
const user = await getUser(id);
|
|
285
|
+
|
|
286
|
+
if (!user) {
|
|
287
|
+
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return NextResponse.json(user);
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Loading & Error Boundaries
|
|
295
|
+
|
|
296
|
+
```tsx
|
|
297
|
+
// app/dashboard/loading.tsx
|
|
298
|
+
export default function Loading() {
|
|
299
|
+
return <DashboardSkeleton />;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// app/dashboard/error.tsx
|
|
303
|
+
'use client';
|
|
304
|
+
|
|
305
|
+
export default function Error({
|
|
306
|
+
error,
|
|
307
|
+
reset,
|
|
308
|
+
}: {
|
|
309
|
+
error: Error & { digest?: string };
|
|
310
|
+
reset: () => void;
|
|
311
|
+
}) {
|
|
312
|
+
return (
|
|
313
|
+
<div>
|
|
314
|
+
<h2>Something went wrong!</h2>
|
|
315
|
+
<button onClick={reset}>Try again</button>
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// app/dashboard/not-found.tsx
|
|
321
|
+
export default function NotFound() {
|
|
322
|
+
return <div>Dashboard not found</div>;
|
|
323
|
+
}
|
|
324
|
+
```
|