@malamute/ai-rules 1.0.0 → 1.2.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 +270 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
- package/configs/_shared/.claude/rules/conventions/git.md +265 -0
- package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
- package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
- package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/.claude/rules/devops/docker.md +275 -0
- package/configs/_shared/.claude/rules/devops/nx.md +194 -0
- package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
- package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
- package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
- package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
- package/configs/_shared/.claude/rules/quality/logging.md +45 -0
- package/configs/_shared/.claude/rules/quality/observability.md +240 -0
- package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
- package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/.claude/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/.claude/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
- package/configs/angular/.claude/rules/core/resource.md +285 -0
- package/configs/angular/.claude/rules/core/signals.md +323 -0
- package/configs/angular/.claude/rules/http.md +338 -0
- package/configs/angular/.claude/rules/routing.md +291 -0
- package/configs/angular/.claude/rules/ssr.md +312 -0
- package/configs/angular/.claude/rules/state/signal-store.md +408 -0
- package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
- package/configs/angular/.claude/rules/testing.md +7 -7
- package/configs/angular/.claude/rules/ui/aria.md +422 -0
- package/configs/angular/.claude/rules/ui/forms.md +424 -0
- package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/.claude/settings.json +1 -0
- package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
- package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/dotnet/.claude/rules/background-services.md +552 -0
- package/configs/dotnet/.claude/rules/configuration.md +426 -0
- package/configs/dotnet/.claude/rules/ddd.md +447 -0
- package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
- package/configs/dotnet/.claude/rules/mediatr.md +320 -0
- package/configs/dotnet/.claude/rules/middleware.md +489 -0
- package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
- package/configs/dotnet/.claude/rules/validation.md +388 -0
- package/configs/dotnet/.claude/settings.json +21 -3
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
- package/configs/fastapi/.claude/rules/dependencies.md +170 -0
- package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
- package/configs/fastapi/.claude/rules/lifespan.md +274 -0
- package/configs/fastapi/.claude/rules/middleware.md +229 -0
- package/configs/fastapi/.claude/rules/pydantic.md +433 -0
- package/configs/fastapi/.claude/rules/responses.md +251 -0
- package/configs/fastapi/.claude/rules/routers.md +202 -0
- package/configs/fastapi/.claude/rules/security.md +222 -0
- package/configs/fastapi/.claude/rules/testing.md +251 -0
- package/configs/fastapi/.claude/rules/websockets.md +298 -0
- package/configs/fastapi/.claude/settings.json +33 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/flask/.claude/rules/blueprints.md +208 -0
- package/configs/flask/.claude/rules/cli.md +285 -0
- package/configs/flask/.claude/rules/configuration.md +281 -0
- package/configs/flask/.claude/rules/context.md +238 -0
- package/configs/flask/.claude/rules/error-handlers.md +278 -0
- package/configs/flask/.claude/rules/extensions.md +278 -0
- package/configs/flask/.claude/rules/flask.md +171 -0
- package/configs/flask/.claude/rules/marshmallow.md +206 -0
- package/configs/flask/.claude/rules/security.md +267 -0
- package/configs/flask/.claude/rules/testing.md +284 -0
- package/configs/flask/.claude/settings.json +33 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
- package/configs/nestjs/.claude/rules/filters.md +376 -0
- package/configs/nestjs/.claude/rules/interceptors.md +317 -0
- package/configs/nestjs/.claude/rules/middleware.md +321 -0
- package/configs/nestjs/.claude/rules/modules.md +26 -0
- package/configs/nestjs/.claude/rules/pipes.md +351 -0
- package/configs/nestjs/.claude/rules/websockets.md +451 -0
- package/configs/nestjs/.claude/settings.json +16 -2
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nextjs/.claude/rules/api-routes.md +358 -0
- package/configs/nextjs/.claude/rules/authentication.md +355 -0
- package/configs/nextjs/.claude/rules/components.md +52 -0
- package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
- package/configs/nextjs/.claude/rules/database.md +400 -0
- package/configs/nextjs/.claude/rules/middleware.md +303 -0
- package/configs/nextjs/.claude/rules/routing.md +324 -0
- package/configs/nextjs/.claude/rules/seo.md +350 -0
- package/configs/nextjs/.claude/rules/server-actions.md +353 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
- package/configs/nextjs/.claude/settings.json +5 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/package.json +23 -9
- package/src/cli.js +220 -0
- package/src/config.js +29 -0
- package/src/index.js +13 -0
- package/src/installer.js +361 -0
- package/src/merge.js +116 -0
- package/src/tech-config.json +29 -0
- package/src/utils.js +96 -0
- 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/{accessibility.md → domain/frontend/accessibility.md} +0 -0
- /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
- /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
|
@@ -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
|
+
```
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/layout.tsx"
|
|
4
|
+
- "**/page.tsx"
|
|
5
|
+
- "app/sitemap.ts"
|
|
6
|
+
- "app/robots.ts"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Next.js SEO
|
|
10
|
+
|
|
11
|
+
## Metadata API
|
|
12
|
+
|
|
13
|
+
### Static Metadata
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// app/page.tsx
|
|
17
|
+
import type { Metadata } from 'next';
|
|
18
|
+
|
|
19
|
+
export const metadata: Metadata = {
|
|
20
|
+
title: 'Home | My App',
|
|
21
|
+
description: 'Welcome to my application',
|
|
22
|
+
keywords: ['next.js', 'react', 'web development'],
|
|
23
|
+
authors: [{ name: 'John Doe' }],
|
|
24
|
+
openGraph: {
|
|
25
|
+
title: 'Home | My App',
|
|
26
|
+
description: 'Welcome to my application',
|
|
27
|
+
url: 'https://example.com',
|
|
28
|
+
siteName: 'My App',
|
|
29
|
+
images: [
|
|
30
|
+
{
|
|
31
|
+
url: 'https://example.com/og-image.png',
|
|
32
|
+
width: 1200,
|
|
33
|
+
height: 630,
|
|
34
|
+
alt: 'My App',
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
locale: 'en_US',
|
|
38
|
+
type: 'website',
|
|
39
|
+
},
|
|
40
|
+
twitter: {
|
|
41
|
+
card: 'summary_large_image',
|
|
42
|
+
title: 'Home | My App',
|
|
43
|
+
description: 'Welcome to my application',
|
|
44
|
+
images: ['https://example.com/og-image.png'],
|
|
45
|
+
},
|
|
46
|
+
robots: {
|
|
47
|
+
index: true,
|
|
48
|
+
follow: true,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Dynamic Metadata
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// app/blog/[slug]/page.tsx
|
|
57
|
+
import type { Metadata } from 'next';
|
|
58
|
+
|
|
59
|
+
type Props = {
|
|
60
|
+
params: Promise<{ slug: string }>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
64
|
+
const { slug } = await params;
|
|
65
|
+
const post = await getPost(slug);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
title: `${post.title} | Blog`,
|
|
69
|
+
description: post.excerpt,
|
|
70
|
+
openGraph: {
|
|
71
|
+
title: post.title,
|
|
72
|
+
description: post.excerpt,
|
|
73
|
+
type: 'article',
|
|
74
|
+
publishedTime: post.publishedAt,
|
|
75
|
+
authors: [post.author.name],
|
|
76
|
+
images: [
|
|
77
|
+
{
|
|
78
|
+
url: post.featuredImage,
|
|
79
|
+
width: 1200,
|
|
80
|
+
height: 630,
|
|
81
|
+
alt: post.title,
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Layout Metadata (Template)
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
// app/layout.tsx
|
|
93
|
+
import type { Metadata } from 'next';
|
|
94
|
+
|
|
95
|
+
export const metadata: Metadata = {
|
|
96
|
+
metadataBase: new URL('https://example.com'),
|
|
97
|
+
title: {
|
|
98
|
+
template: '%s | My App',
|
|
99
|
+
default: 'My App',
|
|
100
|
+
},
|
|
101
|
+
description: 'My application description',
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Child page: title: 'Blog' → renders as 'Blog | My App'
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Sitemap
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// app/sitemap.ts
|
|
111
|
+
import type { MetadataRoute } from 'next';
|
|
112
|
+
|
|
113
|
+
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
114
|
+
const baseUrl = 'https://example.com';
|
|
115
|
+
|
|
116
|
+
// Static pages
|
|
117
|
+
const staticPages = [
|
|
118
|
+
{ url: baseUrl, lastModified: new Date(), changeFrequency: 'daily' as const, priority: 1 },
|
|
119
|
+
{ url: `${baseUrl}/about`, lastModified: new Date(), changeFrequency: 'monthly' as const, priority: 0.8 },
|
|
120
|
+
{ url: `${baseUrl}/contact`, lastModified: new Date(), changeFrequency: 'monthly' as const, priority: 0.5 },
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
// Dynamic pages
|
|
124
|
+
const posts = await db.post.findMany({
|
|
125
|
+
select: { slug: true, updatedAt: true },
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const postPages = posts.map(post => ({
|
|
129
|
+
url: `${baseUrl}/blog/${post.slug}`,
|
|
130
|
+
lastModified: post.updatedAt,
|
|
131
|
+
changeFrequency: 'weekly' as const,
|
|
132
|
+
priority: 0.7,
|
|
133
|
+
}));
|
|
134
|
+
|
|
135
|
+
return [...staticPages, ...postPages];
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Large Sitemap (Multiple Files)
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// app/sitemap/[id]/route.ts
|
|
143
|
+
import { getPostsBatch } from '@/lib/posts';
|
|
144
|
+
|
|
145
|
+
export async function GET(
|
|
146
|
+
request: Request,
|
|
147
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
148
|
+
) {
|
|
149
|
+
const { id } = await params;
|
|
150
|
+
const posts = await getPostsBatch(parseInt(id));
|
|
151
|
+
|
|
152
|
+
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
|
|
153
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
154
|
+
${posts.map(post => `
|
|
155
|
+
<url>
|
|
156
|
+
<loc>https://example.com/blog/${post.slug}</loc>
|
|
157
|
+
<lastmod>${post.updatedAt.toISOString()}</lastmod>
|
|
158
|
+
</url>
|
|
159
|
+
`).join('')}
|
|
160
|
+
</urlset>`;
|
|
161
|
+
|
|
162
|
+
return new Response(sitemap, {
|
|
163
|
+
headers: { 'Content-Type': 'application/xml' },
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Robots.txt
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// app/robots.ts
|
|
172
|
+
import type { MetadataRoute } from 'next';
|
|
173
|
+
|
|
174
|
+
export default function robots(): MetadataRoute.Robots {
|
|
175
|
+
return {
|
|
176
|
+
rules: [
|
|
177
|
+
{
|
|
178
|
+
userAgent: '*',
|
|
179
|
+
allow: '/',
|
|
180
|
+
disallow: ['/admin/', '/api/', '/private/'],
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
userAgent: 'Googlebot',
|
|
184
|
+
allow: '/',
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
sitemap: 'https://example.com/sitemap.xml',
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## JSON-LD Structured Data
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
// components/structured-data.tsx
|
|
196
|
+
export function ArticleJsonLd({
|
|
197
|
+
title,
|
|
198
|
+
description,
|
|
199
|
+
publishedTime,
|
|
200
|
+
author,
|
|
201
|
+
image,
|
|
202
|
+
url,
|
|
203
|
+
}: ArticleJsonLdProps) {
|
|
204
|
+
const jsonLd = {
|
|
205
|
+
'@context': 'https://schema.org',
|
|
206
|
+
'@type': 'Article',
|
|
207
|
+
headline: title,
|
|
208
|
+
description,
|
|
209
|
+
image,
|
|
210
|
+
datePublished: publishedTime,
|
|
211
|
+
author: {
|
|
212
|
+
'@type': 'Person',
|
|
213
|
+
name: author,
|
|
214
|
+
},
|
|
215
|
+
publisher: {
|
|
216
|
+
'@type': 'Organization',
|
|
217
|
+
name: 'My App',
|
|
218
|
+
logo: {
|
|
219
|
+
'@type': 'ImageObject',
|
|
220
|
+
url: 'https://example.com/logo.png',
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
mainEntityOfPage: url,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<script
|
|
228
|
+
type="application/ld+json"
|
|
229
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
|
230
|
+
/>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Usage in page
|
|
235
|
+
export default function BlogPost({ post }) {
|
|
236
|
+
return (
|
|
237
|
+
<>
|
|
238
|
+
<ArticleJsonLd
|
|
239
|
+
title={post.title}
|
|
240
|
+
description={post.excerpt}
|
|
241
|
+
publishedTime={post.publishedAt}
|
|
242
|
+
author={post.author.name}
|
|
243
|
+
image={post.featuredImage}
|
|
244
|
+
url={`https://example.com/blog/${post.slug}`}
|
|
245
|
+
/>
|
|
246
|
+
<article>...</article>
|
|
247
|
+
</>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Organization Schema
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
export function OrganizationJsonLd() {
|
|
256
|
+
const jsonLd = {
|
|
257
|
+
'@context': 'https://schema.org',
|
|
258
|
+
'@type': 'Organization',
|
|
259
|
+
name: 'My Company',
|
|
260
|
+
url: 'https://example.com',
|
|
261
|
+
logo: 'https://example.com/logo.png',
|
|
262
|
+
sameAs: [
|
|
263
|
+
'https://twitter.com/mycompany',
|
|
264
|
+
'https://linkedin.com/company/mycompany',
|
|
265
|
+
],
|
|
266
|
+
contactPoint: {
|
|
267
|
+
'@type': 'ContactPoint',
|
|
268
|
+
telephone: '+1-800-555-1234',
|
|
269
|
+
contactType: 'customer service',
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<script
|
|
275
|
+
type="application/ld+json"
|
|
276
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
|
277
|
+
/>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Canonical URLs
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
export const metadata: Metadata = {
|
|
286
|
+
alternates: {
|
|
287
|
+
canonical: 'https://example.com/page',
|
|
288
|
+
languages: {
|
|
289
|
+
'en-US': 'https://example.com/en-US/page',
|
|
290
|
+
'fr-FR': 'https://example.com/fr-FR/page',
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Performance (Core Web Vitals)
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
// next.config.js
|
|
300
|
+
module.exports = {
|
|
301
|
+
images: {
|
|
302
|
+
formats: ['image/avif', 'image/webp'],
|
|
303
|
+
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
|
304
|
+
},
|
|
305
|
+
experimental: {
|
|
306
|
+
optimizeCss: true,
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// Image component
|
|
311
|
+
import Image from 'next/image';
|
|
312
|
+
|
|
313
|
+
<Image
|
|
314
|
+
src="/hero.jpg"
|
|
315
|
+
alt="Hero image"
|
|
316
|
+
width={1200}
|
|
317
|
+
height={630}
|
|
318
|
+
priority // For LCP images
|
|
319
|
+
placeholder="blur"
|
|
320
|
+
blurDataURL="data:image/jpeg;base64,..."
|
|
321
|
+
/>
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Anti-patterns
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
// BAD: Missing metadata
|
|
328
|
+
export default function Page() {
|
|
329
|
+
return <div>Content</div>;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// GOOD: Always include metadata
|
|
333
|
+
export const metadata: Metadata = {
|
|
334
|
+
title: 'Page Title',
|
|
335
|
+
description: 'Page description',
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// BAD: Duplicate content without canonical
|
|
339
|
+
// page-1 and page-2 have same content
|
|
340
|
+
|
|
341
|
+
// GOOD: Set canonical
|
|
342
|
+
export const metadata: Metadata = {
|
|
343
|
+
alternates: { canonical: '/page-1' },
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// BAD: Blocking indexing of important pages
|
|
347
|
+
export const metadata: Metadata = {
|
|
348
|
+
robots: { index: false }, // Accidentally blocking!
|
|
349
|
+
};
|
|
350
|
+
```
|