项目文件夹

文件
wehub-resource-sync e011bb26cd
Lint skills / Validate catalog structure (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:36:55 +08:00

361 行
8.8 KiB
Markdown

# Next.js Patterns
Stack-specific patterns and anti-patterns for Next.js projects. Covers App Router and Pages Router. Stack-agnostic principles live in [SKILL.md](../SKILL.md).
---
## App Router specific
### Server vs client components
**Default to server components.** Reach for `"use client"` only when interactivity, browser APIs, or React state hooks are required.
```typescript
// Server component (default) - runs on server, no hydration cost
// app/page.tsx
export default async function Page() {
const data = await fetchData();
return <Article data={data} />;
}
// Client component - hydrates in browser
// app/components/InteractiveButton.tsx
"use client";
export default function InteractiveButton() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
```
**Anti-pattern:** Marking parent components as `"use client"` when only a leaf component needs it. This pulls everything below into the client bundle.
### ISR and revalidation
Pick the right rendering strategy per route:
```typescript
// Static (default) - rendered at build, served from CDN
export default async function Page() { ... }
// ISR (revalidate periodically) - rendered at build, regenerated on schedule
export const revalidate = 3600; // 1 hour
// Dynamic (rendered on every request) - never cached
export const dynamic = "force-dynamic";
// Common combination: dynamic with cache hint for downstream
export const dynamic = "force-dynamic";
export const revalidate = 3600;
```
### Build timeout failures
Common error:
```
Failed to build /route after 3 attempts because it took more than 60 seconds
```
**Cause:** A static route that queries a database or external API at build time, and the call exceeds the build timeout.
**Fix:** Add `dynamic = "force-dynamic"` to defer rendering to request time:
```typescript
export const dynamic = "force-dynamic";
export const revalidate = 3600;
export async function GET() {
try {
const data = await dbQuery();
return generateResponse(data);
} catch (e) {
console.error("Route error:", e);
return new Response("", { status: 200 }); // empty but valid
}
}
```
### TypeScript errors in scripts directory
If you have a `scripts/` folder with one-off scripts, exclude it from the build:
```json
// tsconfig.json
{
"exclude": ["node_modules", "scripts"]
}
```
### Canonical URL configuration
The canonical domain must come from a server-only environment variable. Never expose with `NEXT_PUBLIC_` prefix.
```typescript
// next.config.ts
const config = {
env: {
SITE_URL: process.env.SITE_URL ?? "https://yourdomain.com",
},
};
export default config;
```
```typescript
// Where the canonical is set
const url = process.env.SITE_URL; // server-only, never exposed to browser
```
**Anti-pattern:** Using `NEXT_PUBLIC_SITE_URL`. Vercel preview deploys will pick up the preview URL and pollute production canonicals.
---
## Pages Router specific
### getStaticProps timeouts
Same fundamental issue as App Router build timeouts. Solution is `getServerSideProps` for routes that need fresh data:
```typescript
// pages/dynamic-page.tsx
export async function getServerSideProps(context) {
const data = await dbQuery();
return { props: { data } };
}
```
Or ISR:
```typescript
export async function getStaticProps() {
const data = await dbQuery();
return {
props: { data },
revalidate: 3600,
};
}
```
---
## Common patterns across both routers
### API routes / route handlers
**Authentication on mutations:**
```typescript
// app/api/protected/route.ts (App Router)
export async function POST(req: Request) {
const secret = req.headers.get("authorization");
if (secret !== `Bearer ${process.env.API_SECRET}`) {
return new Response("Unauthorized", { status: 401 });
}
// ... handle the request
}
```
**Revalidation endpoint protection:**
```typescript
// app/api/revalidate/route.ts
import { revalidatePath } from "next/cache";
export async function POST(req: Request) {
const url = new URL(req.url);
const secret = url.searchParams.get("secret");
if (secret !== process.env.REVALIDATE_SECRET) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const path = url.searchParams.get("path") ?? "/";
revalidatePath(path);
return Response.json({ revalidated: true, path });
}
```
### Image optimization
Use the framework's built-in image component for proper optimization:
```typescript
import Image from "next/image";
<Image
src="/photo.jpg"
alt="Descriptive alt text"
width={800}
height={600}
loading="lazy" // explicit even though lazy is default for below-fold
/>
```
For external image domains, configure `next.config.ts`:
```typescript
const config = {
images: {
remotePatterns: [
{ protocol: "https", hostname: "cms.yourdomain.com" },
],
},
};
```
### Environment variables
Three categories. Be explicit about which is which.
```
Server-only secrets (never prefix with NEXT_PUBLIC_):
DATABASE_URL
API_SECRET
STRIPE_SECRET_KEY
REVALIDATE_SECRET
SITE_URL (use server-only even for canonical, despite being a "public" value)
Client-safe variables (NEXT_PUBLIC_ prefix exposes to browser):
NEXT_PUBLIC_GA_ID
NEXT_PUBLIC_DEFAULT_LANGUAGE
NEXT_PUBLIC_FEATURE_FLAGS
Build-time only:
ANALYZE_BUNDLE
NODE_ENV (set automatically)
```
**Red flags during review:**
- Any secret with `NEXT_PUBLIC_` prefix
- Canonical or site URL set to a preview/staging URL in production
- Service role keys exposed to the browser
---
## Common bugs and fixes
### Page renders partially after deploy
**Symptom:** Hero loads but content below is missing or stale.
**Cause:** Stale ISR cache from before the deploy.
**Fix:** Force revalidation:
```bash
curl -X POST "https://yourdomain.com/api/revalidate?path=/&secret=$REVALIDATE_SECRET"
```
If the issue persists, check server logs for rendering errors during regeneration.
### CMS API redirects to main domain after DNS cutover
**Symptom:** API calls to the CMS return 302 redirects or 403 errors.
**Cause:** API URL points at the main domain, which now serves the Next.js app instead of the CMS.
**Fix:** Use a dedicated CMS subdomain:
```
WP_API_URL=https://cms.yourdomain.com/wp-json # correct
WP_API_URL=https://yourdomain.com/wp-json # loops back to Next.js
```
### Images return 502 after domain change
**Cause:** Image optimizer fetching from a domain that no longer hosts the source images.
**Fix:** Update `images.remotePatterns` in `next.config.ts` to match the new image origin. Rewrite media URLs in data fetches to point at the correct origin.
### Client component re-renders excessively
**Cause:** Inline object or function as prop. New reference on every render triggers re-render in the child.
**Fix:** Memoize:
```typescript
// Before (creates new object each render)
<Child config={{ foo: "bar" }} />
// After
const config = useMemo(() => ({ foo: "bar" }), []);
<Child config={config} />
```
---
## Performance patterns
### Avoid request waterfalls
```typescript
// Bad: sequential fetches
const user = await getUser();
const posts = await getPostsByUser(user.id);
const comments = await getCommentsForPosts(posts.map(p => p.id));
// Good: parallel where possible
const user = await getUser();
const [posts, settings] = await Promise.all([
getPostsByUser(user.id),
getSettingsForUser(user.id),
]);
```
### Database query patterns
```typescript
// Bad: N+1
for (const post of posts) {
const author = await db.user.findUnique({ where: { id: post.authorId } });
}
// Good: batch fetch with relation
const posts = await db.post.findMany({
include: { author: true },
});
// Always limit large scans
const data = await db.post.findMany({
where: { ... },
take: 100,
});
```
### Streaming with Suspense
For App Router, stream slow content to improve perceived performance:
```typescript
// app/page.tsx
import { Suspense } from "react";
import FastContent from "./FastContent";
import SlowContent from "./SlowContent";
export default function Page() {
return (
<>
<FastContent />
<Suspense fallback={<Loading />}>
<SlowContent />
</Suspense>
</>
);
}
```
---
## Code review checklist for Next.js
- [ ] Server components default; `"use client"` only where needed
- [ ] ISR routes have appropriate `revalidate` value
- [ ] Database-querying routes have `force-dynamic` if they can timeout at build
- [ ] Canonical/site URL uses server-only env var
- [ ] No secrets in `NEXT_PUBLIC_` variables
- [ ] API routes have auth checks on mutations
- [ ] Revalidation endpoint protected with secret
- [ ] Images use the `Image` component, with explicit width/height
- [ ] External image domains in `images.remotePatterns`
- [ ] Scripts directory excluded from tsconfig if causing type errors
- [ ] Suspense boundaries for slow content where streaming helps