autoworkflow 3.1.4 → 3.5.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/.claude/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +174 -11
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/pre-edit.sh +129 -0
- package/.claude/hooks/session-check.sh +79 -0
- package/.claude/settings.json +40 -6
- package/.claude/settings.local.json +3 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +135 -52
- package/package.json +1 -1
- package/system/triggers.md +152 -11
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# Next.js App Router Skill
|
|
2
|
+
|
|
3
|
+
## File Conventions
|
|
4
|
+
\`\`\`
|
|
5
|
+
app/
|
|
6
|
+
├── layout.tsx # Root layout (required)
|
|
7
|
+
├── page.tsx # Home page (/)
|
|
8
|
+
├── loading.tsx # Loading UI (Suspense boundary)
|
|
9
|
+
├── error.tsx # Error boundary
|
|
10
|
+
├── not-found.tsx # 404 page
|
|
11
|
+
├── global-error.tsx # Root error boundary
|
|
12
|
+
├── (auth)/ # Route group (doesn't affect URL)
|
|
13
|
+
│ ├── login/page.tsx # /login
|
|
14
|
+
│ └── register/page.tsx # /register
|
|
15
|
+
├── dashboard/
|
|
16
|
+
│ ├── layout.tsx # Dashboard layout
|
|
17
|
+
│ ├── @modal/(.)edit/page.tsx # Parallel route + intercepting
|
|
18
|
+
│ └── [id]/page.tsx # /dashboard/123
|
|
19
|
+
└── api/
|
|
20
|
+
└── users/route.ts # API route handler
|
|
21
|
+
\`\`\`
|
|
22
|
+
|
|
23
|
+
## Server vs Client Components
|
|
24
|
+
\`\`\`tsx
|
|
25
|
+
// ✅ SERVER COMPONENT (default)
|
|
26
|
+
// Can: fetch data, access backend, use async/await
|
|
27
|
+
// Cannot: useState, useEffect, onClick
|
|
28
|
+
export default async function ProductPage({ params }) {
|
|
29
|
+
const product = await db.product.findUnique({ where: { id: params.id } });
|
|
30
|
+
return <ProductDetails product={product} />;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ✅ CLIENT COMPONENT - add 'use client'
|
|
34
|
+
'use client';
|
|
35
|
+
export function AddToCartButton({ productId }) {
|
|
36
|
+
const [loading, setLoading] = useState(false);
|
|
37
|
+
return <button onClick={() => addToCart(productId)}>Add</button>;
|
|
38
|
+
}
|
|
39
|
+
\`\`\`
|
|
40
|
+
|
|
41
|
+
## Metadata & SEO
|
|
42
|
+
\`\`\`tsx
|
|
43
|
+
// Static metadata
|
|
44
|
+
export const metadata: Metadata = {
|
|
45
|
+
title: 'My App',
|
|
46
|
+
description: 'App description',
|
|
47
|
+
openGraph: {
|
|
48
|
+
title: 'My App',
|
|
49
|
+
description: 'App description',
|
|
50
|
+
images: ['/og-image.png'],
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Dynamic metadata
|
|
55
|
+
export async function generateMetadata({ params }): Promise<Metadata> {
|
|
56
|
+
const product = await getProduct(params.id);
|
|
57
|
+
return {
|
|
58
|
+
title: product.name,
|
|
59
|
+
description: product.description,
|
|
60
|
+
openGraph: {
|
|
61
|
+
images: [product.image],
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// JSON-LD structured data
|
|
67
|
+
export default function ProductPage({ product }) {
|
|
68
|
+
const jsonLd = {
|
|
69
|
+
'@context': 'https://schema.org',
|
|
70
|
+
'@type': 'Product',
|
|
71
|
+
name: product.name,
|
|
72
|
+
description: product.description,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<>
|
|
77
|
+
<script
|
|
78
|
+
type="application/ld+json"
|
|
79
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
|
80
|
+
/>
|
|
81
|
+
<ProductDetails product={product} />
|
|
82
|
+
</>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
\`\`\`
|
|
86
|
+
|
|
87
|
+
## Middleware
|
|
88
|
+
\`\`\`typescript
|
|
89
|
+
// middleware.ts (root of project)
|
|
90
|
+
import { NextResponse } from 'next/server';
|
|
91
|
+
import type { NextRequest } from 'next/server';
|
|
92
|
+
|
|
93
|
+
export function middleware(request: NextRequest) {
|
|
94
|
+
// Auth check
|
|
95
|
+
const token = request.cookies.get('token');
|
|
96
|
+
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
|
|
97
|
+
return NextResponse.redirect(new URL('/login', request.url));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Add headers
|
|
101
|
+
const response = NextResponse.next();
|
|
102
|
+
response.headers.set('x-request-id', crypto.randomUUID());
|
|
103
|
+
|
|
104
|
+
return response;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const config = {
|
|
108
|
+
matcher: ['/dashboard/:path*', '/api/:path*'],
|
|
109
|
+
};
|
|
110
|
+
\`\`\`
|
|
111
|
+
|
|
112
|
+
## Caching & Revalidation
|
|
113
|
+
\`\`\`tsx
|
|
114
|
+
// Time-based revalidation
|
|
115
|
+
async function getProducts() {
|
|
116
|
+
const res = await fetch('https://api.example.com/products', {
|
|
117
|
+
next: { revalidate: 3600 } // Revalidate every hour
|
|
118
|
+
});
|
|
119
|
+
return res.json();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// On-demand revalidation
|
|
123
|
+
import { revalidatePath, revalidateTag } from 'next/cache';
|
|
124
|
+
|
|
125
|
+
// In Server Action
|
|
126
|
+
'use server';
|
|
127
|
+
export async function updateProduct(id: string, data: FormData) {
|
|
128
|
+
await db.product.update({ where: { id }, data });
|
|
129
|
+
revalidatePath('/products'); // Revalidate specific path
|
|
130
|
+
revalidateTag('products'); // Revalidate by tag
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Tag your fetches
|
|
134
|
+
fetch('https://api.example.com/products', {
|
|
135
|
+
next: { tags: ['products'] }
|
|
136
|
+
});
|
|
137
|
+
\`\`\`
|
|
138
|
+
|
|
139
|
+
## Streaming with Suspense
|
|
140
|
+
\`\`\`tsx
|
|
141
|
+
import { Suspense } from 'react';
|
|
142
|
+
|
|
143
|
+
export default function Dashboard() {
|
|
144
|
+
return (
|
|
145
|
+
<div>
|
|
146
|
+
<h1>Dashboard</h1>
|
|
147
|
+
|
|
148
|
+
{/* Show skeleton while loading */}
|
|
149
|
+
<Suspense fallback={<StatsSkeleton />}>
|
|
150
|
+
<Stats />
|
|
151
|
+
</Suspense>
|
|
152
|
+
|
|
153
|
+
<Suspense fallback={<ChartSkeleton />}>
|
|
154
|
+
<RevenueChart />
|
|
155
|
+
</Suspense>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Each component can be async and stream independently
|
|
161
|
+
async function Stats() {
|
|
162
|
+
const stats = await getStats(); // Slow query
|
|
163
|
+
return <StatsDisplay stats={stats} />;
|
|
164
|
+
}
|
|
165
|
+
\`\`\`
|
|
166
|
+
|
|
167
|
+
## Error Handling
|
|
168
|
+
\`\`\`tsx
|
|
169
|
+
// error.tsx - catches errors in this segment
|
|
170
|
+
'use client';
|
|
171
|
+
|
|
172
|
+
export default function Error({
|
|
173
|
+
error,
|
|
174
|
+
reset,
|
|
175
|
+
}: {
|
|
176
|
+
error: Error & { digest?: string };
|
|
177
|
+
reset: () => void;
|
|
178
|
+
}) {
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
logError(error); // Send to error tracking
|
|
181
|
+
}, [error]);
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<div>
|
|
185
|
+
<h2>Something went wrong!</h2>
|
|
186
|
+
<button onClick={() => reset()}>Try again</button>
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// not-found.tsx - custom 404
|
|
192
|
+
export default function NotFound() {
|
|
193
|
+
return (
|
|
194
|
+
<div>
|
|
195
|
+
<h2>Page Not Found</h2>
|
|
196
|
+
<Link href="/">Go home</Link>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Trigger not-found programmatically
|
|
202
|
+
import { notFound } from 'next/navigation';
|
|
203
|
+
|
|
204
|
+
async function getUser(id: string) {
|
|
205
|
+
const user = await db.user.findUnique({ where: { id } });
|
|
206
|
+
if (!user) notFound();
|
|
207
|
+
return user;
|
|
208
|
+
}
|
|
209
|
+
\`\`\`
|
|
210
|
+
|
|
211
|
+
## Server Actions
|
|
212
|
+
\`\`\`tsx
|
|
213
|
+
'use server';
|
|
214
|
+
|
|
215
|
+
import { revalidatePath } from 'next/cache';
|
|
216
|
+
import { redirect } from 'next/navigation';
|
|
217
|
+
|
|
218
|
+
export async function createUser(formData: FormData) {
|
|
219
|
+
const data = {
|
|
220
|
+
name: formData.get('name') as string,
|
|
221
|
+
email: formData.get('email') as string,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// Validate
|
|
225
|
+
const result = userSchema.safeParse(data);
|
|
226
|
+
if (!result.success) {
|
|
227
|
+
return { error: result.error.flatten() };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Create
|
|
231
|
+
await prisma.user.create({ data: result.data });
|
|
232
|
+
|
|
233
|
+
// Revalidate and redirect
|
|
234
|
+
revalidatePath('/users');
|
|
235
|
+
redirect('/users');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Usage in form
|
|
239
|
+
<form action={createUser}>
|
|
240
|
+
<input name="name" required />
|
|
241
|
+
<input name="email" type="email" required />
|
|
242
|
+
<button type="submit">Create</button>
|
|
243
|
+
</form>
|
|
244
|
+
\`\`\`
|
|
245
|
+
|
|
246
|
+
## ❌ DON'T
|
|
247
|
+
- Use 'use client' unless you need interactivity
|
|
248
|
+
- Use getServerSideProps (that's Pages Router)
|
|
249
|
+
- Put sensitive logic in client components
|
|
250
|
+
- Forget to handle loading/error states
|
|
251
|
+
- Over-fetch in Server Components (no deduplication)
|
|
252
|
+
|
|
253
|
+
## ✅ DO
|
|
254
|
+
- Default to Server Components
|
|
255
|
+
- Keep client components small and at leaf nodes
|
|
256
|
+
- Use Server Actions for form submissions
|
|
257
|
+
- Add metadata for SEO
|
|
258
|
+
- Use Suspense for streaming
|
|
259
|
+
- Implement proper error boundaries
|
|
260
|
+
- Use revalidation strategies appropriately
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Next.js Pages Router Skill
|
|
2
|
+
|
|
3
|
+
## Data Fetching
|
|
4
|
+
\`\`\`tsx
|
|
5
|
+
// getStaticProps - Build time (ISR with revalidate)
|
|
6
|
+
export async function getStaticProps() {
|
|
7
|
+
const posts = await getPosts();
|
|
8
|
+
return {
|
|
9
|
+
props: { posts },
|
|
10
|
+
revalidate: 60, // Regenerate every 60 seconds
|
|
11
|
+
notFound: false, // Return 404 if true
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// getServerSideProps - Request time (SSR)
|
|
16
|
+
export async function getServerSideProps(context) {
|
|
17
|
+
const { params, req, res, query } = context;
|
|
18
|
+
|
|
19
|
+
// Access cookies/headers
|
|
20
|
+
const token = req.cookies.token;
|
|
21
|
+
if (!token) {
|
|
22
|
+
return { redirect: { destination: '/login', permanent: false } };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const user = await getUser(params.id);
|
|
26
|
+
if (!user) {
|
|
27
|
+
return { notFound: true };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { props: { user } };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// getStaticPaths - Dynamic static pages
|
|
34
|
+
export async function getStaticPaths() {
|
|
35
|
+
const posts = await getAllPosts();
|
|
36
|
+
return {
|
|
37
|
+
paths: posts.map(p => ({ params: { id: p.id } })),
|
|
38
|
+
fallback: 'blocking' // 'false' | 'blocking' | true
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
\`\`\`
|
|
42
|
+
|
|
43
|
+
## Custom _app.tsx
|
|
44
|
+
\`\`\`tsx
|
|
45
|
+
// pages/_app.tsx
|
|
46
|
+
import type { AppProps } from 'next/app';
|
|
47
|
+
import { SessionProvider } from 'next-auth/react';
|
|
48
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
49
|
+
|
|
50
|
+
const queryClient = new QueryClient();
|
|
51
|
+
|
|
52
|
+
export default function App({ Component, pageProps }: AppProps) {
|
|
53
|
+
// Use per-page layouts
|
|
54
|
+
const getLayout = Component.getLayout ?? ((page) => page);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<SessionProvider session={pageProps.session}>
|
|
58
|
+
<QueryClientProvider client={queryClient}>
|
|
59
|
+
{getLayout(<Component {...pageProps} />)}
|
|
60
|
+
</QueryClientProvider>
|
|
61
|
+
</SessionProvider>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
\`\`\`
|
|
65
|
+
|
|
66
|
+
## Custom _document.tsx
|
|
67
|
+
\`\`\`tsx
|
|
68
|
+
// pages/_document.tsx
|
|
69
|
+
import { Html, Head, Main, NextScript } from 'next/document';
|
|
70
|
+
|
|
71
|
+
export default function Document() {
|
|
72
|
+
return (
|
|
73
|
+
<Html lang="en">
|
|
74
|
+
<Head>
|
|
75
|
+
<link rel="icon" href="/favicon.ico" />
|
|
76
|
+
<meta name="theme-color" content="#000000" />
|
|
77
|
+
</Head>
|
|
78
|
+
<body>
|
|
79
|
+
<Main />
|
|
80
|
+
<NextScript />
|
|
81
|
+
</body>
|
|
82
|
+
</Html>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
\`\`\`
|
|
86
|
+
|
|
87
|
+
## API Routes with Middleware
|
|
88
|
+
\`\`\`typescript
|
|
89
|
+
// pages/api/users.ts
|
|
90
|
+
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
91
|
+
|
|
92
|
+
// Middleware wrapper
|
|
93
|
+
function withAuth(handler: Function) {
|
|
94
|
+
return async (req: NextApiRequest, res: NextApiResponse) => {
|
|
95
|
+
const token = req.headers.authorization?.split(' ')[1];
|
|
96
|
+
if (!token) {
|
|
97
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
req.user = verifyToken(token);
|
|
102
|
+
return handler(req, res);
|
|
103
|
+
} catch {
|
|
104
|
+
return res.status(401).json({ error: 'Invalid token' });
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
110
|
+
switch (req.method) {
|
|
111
|
+
case 'GET':
|
|
112
|
+
const users = await prisma.user.findMany();
|
|
113
|
+
return res.status(200).json(users);
|
|
114
|
+
case 'POST':
|
|
115
|
+
const user = await prisma.user.create({ data: req.body });
|
|
116
|
+
return res.status(201).json(user);
|
|
117
|
+
default:
|
|
118
|
+
res.setHeader('Allow', ['GET', 'POST']);
|
|
119
|
+
return res.status(405).end();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export default withAuth(handler);
|
|
124
|
+
\`\`\`
|
|
125
|
+
|
|
126
|
+
## Middleware (Edge)
|
|
127
|
+
\`\`\`typescript
|
|
128
|
+
// middleware.ts (root level)
|
|
129
|
+
import { NextResponse } from 'next/server';
|
|
130
|
+
import type { NextRequest } from 'next/server';
|
|
131
|
+
|
|
132
|
+
export function middleware(request: NextRequest) {
|
|
133
|
+
const token = request.cookies.get('token');
|
|
134
|
+
|
|
135
|
+
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
|
|
136
|
+
return NextResponse.redirect(new URL('/login', request.url));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return NextResponse.next();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const config = {
|
|
143
|
+
matcher: ['/dashboard/:path*', '/api/protected/:path*'],
|
|
144
|
+
};
|
|
145
|
+
\`\`\`
|
|
146
|
+
|
|
147
|
+
## Per-Page Layouts
|
|
148
|
+
\`\`\`tsx
|
|
149
|
+
// pages/dashboard/index.tsx
|
|
150
|
+
import DashboardLayout from '@/layouts/DashboardLayout';
|
|
151
|
+
|
|
152
|
+
export default function DashboardPage() {
|
|
153
|
+
return <div>Dashboard Content</div>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
DashboardPage.getLayout = function getLayout(page: ReactElement) {
|
|
157
|
+
return <DashboardLayout>{page}</DashboardLayout>;
|
|
158
|
+
};
|
|
159
|
+
\`\`\`
|
|
160
|
+
|
|
161
|
+
## ❌ DON'T
|
|
162
|
+
- Use getInitialProps (legacy, blocks optimization)
|
|
163
|
+
- Fetch data in useEffect when SSR/SSG works
|
|
164
|
+
- Put secrets in client-side code
|
|
165
|
+
- Forget fallback handling in getStaticPaths
|
|
166
|
+
|
|
167
|
+
## ✅ DO
|
|
168
|
+
- Use getStaticProps with revalidate for cacheable data
|
|
169
|
+
- Use getServerSideProps only when needed (auth, real-time)
|
|
170
|
+
- Handle loading/error states for fallback pages
|
|
171
|
+
- Use middleware for auth/redirects
|
|
172
|
+
- Implement per-page layouts for complex UIs
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Nuxt 3 Skill
|
|
2
|
+
|
|
3
|
+
## Directory Structure
|
|
4
|
+
\`\`\`
|
|
5
|
+
app.vue # Root component
|
|
6
|
+
nuxt.config.ts # Configuration
|
|
7
|
+
pages/ # File-based routing
|
|
8
|
+
index.vue # /
|
|
9
|
+
users/
|
|
10
|
+
index.vue # /users
|
|
11
|
+
[id].vue # /users/:id
|
|
12
|
+
components/ # Auto-imported
|
|
13
|
+
composables/ # Auto-imported (useX)
|
|
14
|
+
server/
|
|
15
|
+
api/ # API routes
|
|
16
|
+
middleware/ # Server middleware
|
|
17
|
+
middleware/ # Route middleware
|
|
18
|
+
plugins/ # App plugins
|
|
19
|
+
layouts/ # Layouts
|
|
20
|
+
\`\`\`
|
|
21
|
+
|
|
22
|
+
## Data Fetching
|
|
23
|
+
\`\`\`vue
|
|
24
|
+
<script setup>
|
|
25
|
+
// useFetch - SSR + client hydration, auto-deduplication
|
|
26
|
+
const { data: users, pending, error, refresh } = await useFetch('/api/users');
|
|
27
|
+
|
|
28
|
+
// useAsyncData - for custom async functions
|
|
29
|
+
const { data: user } = await useAsyncData('user', () => {
|
|
30
|
+
return $fetch(\`/api/users/\${route.params.id}\`);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// useLazyFetch - doesn't block navigation
|
|
34
|
+
const { data: stats, pending } = useLazyFetch('/api/stats');
|
|
35
|
+
|
|
36
|
+
// With options
|
|
37
|
+
const { data } = await useFetch('/api/users', {
|
|
38
|
+
query: { page: 1, limit: 10 },
|
|
39
|
+
headers: { Authorization: \`Bearer \${token}\` },
|
|
40
|
+
transform: (data) => data.users, // Transform response
|
|
41
|
+
pick: ['id', 'name'], // Only pick these fields
|
|
42
|
+
watch: [page], // Refetch when page changes
|
|
43
|
+
});
|
|
44
|
+
\`\`\`
|
|
45
|
+
|
|
46
|
+
## Server API Routes
|
|
47
|
+
\`\`\`typescript
|
|
48
|
+
// server/api/users.get.ts
|
|
49
|
+
export default defineEventHandler(async (event) => {
|
|
50
|
+
return await prisma.user.findMany();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// server/api/users.post.ts
|
|
54
|
+
export default defineEventHandler(async (event) => {
|
|
55
|
+
const body = await readBody(event);
|
|
56
|
+
|
|
57
|
+
// Validation
|
|
58
|
+
if (!body.email) {
|
|
59
|
+
throw createError({ statusCode: 400, message: 'Email required' });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return await prisma.user.create({ data: body });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// server/api/users/[id].get.ts
|
|
66
|
+
export default defineEventHandler(async (event) => {
|
|
67
|
+
const id = getRouterParam(event, 'id');
|
|
68
|
+
const user = await prisma.user.findUnique({ where: { id } });
|
|
69
|
+
|
|
70
|
+
if (!user) {
|
|
71
|
+
throw createError({ statusCode: 404, message: 'User not found' });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return user;
|
|
75
|
+
});
|
|
76
|
+
\`\`\`
|
|
77
|
+
|
|
78
|
+
## Middleware
|
|
79
|
+
\`\`\`typescript
|
|
80
|
+
// middleware/auth.ts
|
|
81
|
+
export default defineNuxtRouteMiddleware((to, from) => {
|
|
82
|
+
const { user } = useAuth();
|
|
83
|
+
|
|
84
|
+
if (!user.value && to.path !== '/login') {
|
|
85
|
+
return navigateTo('/login');
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Apply to page
|
|
90
|
+
<script setup>
|
|
91
|
+
definePageMeta({
|
|
92
|
+
middleware: 'auth',
|
|
93
|
+
// Or inline:
|
|
94
|
+
// middleware: [(to, from) => { ... }]
|
|
95
|
+
});
|
|
96
|
+
</script>
|
|
97
|
+
|
|
98
|
+
// Global middleware
|
|
99
|
+
// middleware/auth.global.ts
|
|
100
|
+
export default defineNuxtRouteMiddleware((to) => {
|
|
101
|
+
// Runs on every navigation
|
|
102
|
+
});
|
|
103
|
+
\`\`\`
|
|
104
|
+
|
|
105
|
+
## Plugins
|
|
106
|
+
\`\`\`typescript
|
|
107
|
+
// plugins/api.ts
|
|
108
|
+
export default defineNuxtPlugin((nuxtApp) => {
|
|
109
|
+
const api = $fetch.create({
|
|
110
|
+
baseURL: '/api',
|
|
111
|
+
onRequest({ options }) {
|
|
112
|
+
const token = useCookie('token');
|
|
113
|
+
if (token.value) {
|
|
114
|
+
options.headers = { Authorization: \`Bearer \${token.value}\` };
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
provide: { api }
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Usage in components
|
|
125
|
+
const { $api } = useNuxtApp();
|
|
126
|
+
const users = await $api('/users');
|
|
127
|
+
\`\`\`
|
|
128
|
+
|
|
129
|
+
## State Management
|
|
130
|
+
\`\`\`typescript
|
|
131
|
+
// composables/useAuth.ts
|
|
132
|
+
export const useAuth = () => {
|
|
133
|
+
const user = useState<User | null>('user', () => null);
|
|
134
|
+
|
|
135
|
+
const login = async (email: string, password: string) => {
|
|
136
|
+
user.value = await $fetch('/api/auth/login', {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
body: { email, password }
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const logout = () => {
|
|
143
|
+
user.value = null;
|
|
144
|
+
navigateTo('/login');
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return { user, login, logout };
|
|
148
|
+
};
|
|
149
|
+
\`\`\`
|
|
150
|
+
|
|
151
|
+
## Runtime Config
|
|
152
|
+
\`\`\`typescript
|
|
153
|
+
// nuxt.config.ts
|
|
154
|
+
export default defineNuxtConfig({
|
|
155
|
+
runtimeConfig: {
|
|
156
|
+
// Server-only (not exposed to client)
|
|
157
|
+
apiSecret: process.env.API_SECRET,
|
|
158
|
+
|
|
159
|
+
// Public (exposed to client)
|
|
160
|
+
public: {
|
|
161
|
+
apiBase: process.env.API_BASE || '/api'
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Usage
|
|
167
|
+
const config = useRuntimeConfig();
|
|
168
|
+
// Server: config.apiSecret
|
|
169
|
+
// Client: config.public.apiBase
|
|
170
|
+
\`\`\`
|
|
171
|
+
|
|
172
|
+
## Error Handling
|
|
173
|
+
\`\`\`vue
|
|
174
|
+
<!-- error.vue (root level) -->
|
|
175
|
+
<script setup>
|
|
176
|
+
const props = defineProps<{ error: { statusCode: number; message: string } }>();
|
|
177
|
+
|
|
178
|
+
const handleError = () => clearError({ redirect: '/' });
|
|
179
|
+
</script>
|
|
180
|
+
|
|
181
|
+
<template>
|
|
182
|
+
<div>
|
|
183
|
+
<h1>{{ error.statusCode }}</h1>
|
|
184
|
+
<p>{{ error.message }}</p>
|
|
185
|
+
<button @click="handleError">Go Home</button>
|
|
186
|
+
</div>
|
|
187
|
+
</template>
|
|
188
|
+
\`\`\`
|
|
189
|
+
|
|
190
|
+
## ❌ DON'T
|
|
191
|
+
- Use useFetch in non-setup contexts
|
|
192
|
+
- Forget to handle pending/error states
|
|
193
|
+
- Put secrets in runtimeConfig.public
|
|
194
|
+
- Use $fetch without useFetch for SSR data
|
|
195
|
+
|
|
196
|
+
## ✅ DO
|
|
197
|
+
- Use useFetch for SSR data fetching
|
|
198
|
+
- Use useState for shared state
|
|
199
|
+
- Use auto-imports (no manual imports needed)
|
|
200
|
+
- Use middleware for auth/guards
|
|
201
|
+
- Use runtimeConfig for environment variables
|
|
202
|
+
- Handle errors with createError
|