260322:1648 Correct Coresspondence / Doing RFA / Correct CI
This commit is contained in:
@@ -11,6 +11,7 @@ Apply these rules when writing or reviewing Next.js code.
|
||||
## File Conventions
|
||||
|
||||
See [file-conventions.md](./file-conventions.md) for:
|
||||
|
||||
- Project structure and special files
|
||||
- Route segments (dynamic, catch-all, groups)
|
||||
- Parallel and intercepting routes
|
||||
@@ -21,6 +22,7 @@ See [file-conventions.md](./file-conventions.md) for:
|
||||
Detect invalid React Server Component patterns.
|
||||
|
||||
See [rsc-boundaries.md](./rsc-boundaries.md) for:
|
||||
|
||||
- Async client component detection (invalid)
|
||||
- Non-serializable props detection
|
||||
- Server Action exceptions
|
||||
@@ -30,6 +32,7 @@ See [rsc-boundaries.md](./rsc-boundaries.md) for:
|
||||
Next.js 15+ async API changes.
|
||||
|
||||
See [async-patterns.md](./async-patterns.md) for:
|
||||
|
||||
- Async `params` and `searchParams`
|
||||
- Async `cookies()` and `headers()`
|
||||
- Migration codemod
|
||||
@@ -37,18 +40,21 @@ See [async-patterns.md](./async-patterns.md) for:
|
||||
## Runtime Selection
|
||||
|
||||
See [runtime-selection.md](./runtime-selection.md) for:
|
||||
|
||||
- Default to Node.js runtime
|
||||
- When Edge runtime is appropriate
|
||||
|
||||
## Directives
|
||||
|
||||
See [directives.md](./directives.md) for:
|
||||
|
||||
- `'use client'`, `'use server'` (React)
|
||||
- `'use cache'` (Next.js)
|
||||
|
||||
## Functions
|
||||
|
||||
See [functions.md](./functions.md) for:
|
||||
|
||||
- Navigation hooks: `useRouter`, `usePathname`, `useSearchParams`, `useParams`
|
||||
- Server functions: `cookies`, `headers`, `draftMode`, `after`
|
||||
- Generate functions: `generateStaticParams`, `generateMetadata`
|
||||
@@ -56,6 +62,7 @@ See [functions.md](./functions.md) for:
|
||||
## Error Handling
|
||||
|
||||
See [error-handling.md](./error-handling.md) for:
|
||||
|
||||
- `error.tsx`, `global-error.tsx`, `not-found.tsx`
|
||||
- `redirect`, `permanentRedirect`, `notFound`
|
||||
- `forbidden`, `unauthorized` (auth errors)
|
||||
@@ -64,6 +71,7 @@ See [error-handling.md](./error-handling.md) for:
|
||||
## Data Patterns
|
||||
|
||||
See [data-patterns.md](./data-patterns.md) for:
|
||||
|
||||
- Server Components vs Server Actions vs Route Handlers
|
||||
- Avoiding data waterfalls (`Promise.all`, Suspense, preload)
|
||||
- Client component data fetching
|
||||
@@ -71,6 +79,7 @@ See [data-patterns.md](./data-patterns.md) for:
|
||||
## Route Handlers
|
||||
|
||||
See [route-handlers.md](./route-handlers.md) for:
|
||||
|
||||
- `route.ts` basics
|
||||
- GET handler conflicts with `page.tsx`
|
||||
- Environment behavior (no React DOM)
|
||||
@@ -79,6 +88,7 @@ See [route-handlers.md](./route-handlers.md) for:
|
||||
## Metadata & OG Images
|
||||
|
||||
See [metadata.md](./metadata.md) for:
|
||||
|
||||
- Static and dynamic metadata
|
||||
- `generateMetadata` function
|
||||
- OG image generation with `next/og`
|
||||
@@ -87,6 +97,7 @@ See [metadata.md](./metadata.md) for:
|
||||
## Image Optimization
|
||||
|
||||
See [image.md](./image.md) for:
|
||||
|
||||
- Always use `next/image` over `<img>`
|
||||
- Remote images configuration
|
||||
- Responsive `sizes` attribute
|
||||
@@ -96,6 +107,7 @@ See [image.md](./image.md) for:
|
||||
## Font Optimization
|
||||
|
||||
See [font.md](./font.md) for:
|
||||
|
||||
- `next/font` setup
|
||||
- Google Fonts, local fonts
|
||||
- Tailwind CSS integration
|
||||
@@ -104,6 +116,7 @@ See [font.md](./font.md) for:
|
||||
## Bundling
|
||||
|
||||
See [bundling.md](./bundling.md) for:
|
||||
|
||||
- Server-incompatible packages
|
||||
- CSS imports (not link tags)
|
||||
- Polyfills (already included)
|
||||
@@ -113,6 +126,7 @@ See [bundling.md](./bundling.md) for:
|
||||
## Scripts
|
||||
|
||||
See [scripts.md](./scripts.md) for:
|
||||
|
||||
- `next/script` vs native script tags
|
||||
- Inline scripts need `id`
|
||||
- Loading strategies
|
||||
@@ -121,6 +135,7 @@ See [scripts.md](./scripts.md) for:
|
||||
## Hydration Errors
|
||||
|
||||
See [hydration-error.md](./hydration-error.md) for:
|
||||
|
||||
- Common causes (browser APIs, dates, invalid HTML)
|
||||
- Debugging with error overlay
|
||||
- Fixes for each cause
|
||||
@@ -128,12 +143,14 @@ See [hydration-error.md](./hydration-error.md) for:
|
||||
## Suspense Boundaries
|
||||
|
||||
See [suspense-boundaries.md](./suspense-boundaries.md) for:
|
||||
|
||||
- CSR bailout with `useSearchParams` and `usePathname`
|
||||
- Which hooks require Suspense boundaries
|
||||
|
||||
## Parallel & Intercepting Routes
|
||||
|
||||
See [parallel-routes.md](./parallel-routes.md) for:
|
||||
|
||||
- Modal patterns with `@slot` and `(.)` interceptors
|
||||
- `default.tsx` for fallbacks
|
||||
- Closing modals correctly with `router.back()`
|
||||
@@ -141,6 +158,7 @@ See [parallel-routes.md](./parallel-routes.md) for:
|
||||
## Self-Hosting
|
||||
|
||||
See [self-hosting.md](./self-hosting.md) for:
|
||||
|
||||
- `output: 'standalone'` for Docker
|
||||
- Cache handlers for multi-instance ISR
|
||||
- What works vs needs extra setup
|
||||
@@ -148,6 +166,6 @@ See [self-hosting.md](./self-hosting.md) for:
|
||||
## Debug Tricks
|
||||
|
||||
See [debug-tricks.md](./debug-tricks.md) for:
|
||||
|
||||
- MCP endpoint for AI-assisted debugging
|
||||
- Rebuild specific routes with `--debug-build-paths`
|
||||
|
||||
|
||||
@@ -9,21 +9,18 @@ Always type them as `Promise<...>` and await them.
|
||||
### Pages and Layouts
|
||||
|
||||
```tsx
|
||||
type Props = { params: Promise<{ slug: string }> }
|
||||
type Props = { params: Promise<{ slug: string }> };
|
||||
|
||||
export default async function Page({ params }: Props) {
|
||||
const { slug } = await params
|
||||
const { slug } = await params;
|
||||
}
|
||||
```
|
||||
|
||||
### Route Handlers
|
||||
|
||||
```tsx
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await params
|
||||
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -31,13 +28,13 @@ export async function GET(
|
||||
|
||||
```tsx
|
||||
type Props = {
|
||||
params: Promise<{ slug: string }>
|
||||
searchParams: Promise<{ query?: string }>
|
||||
}
|
||||
params: Promise<{ slug: string }>;
|
||||
searchParams: Promise<{ query?: string }>;
|
||||
};
|
||||
|
||||
export default async function Page({ params, searchParams }: Props) {
|
||||
const { slug } = await params
|
||||
const { query } = await searchParams
|
||||
const { slug } = await params;
|
||||
const { query } = await searchParams;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -46,37 +43,37 @@ export default async function Page({ params, searchParams }: Props) {
|
||||
Use `React.use()` for non-async components:
|
||||
|
||||
```tsx
|
||||
import { use } from 'react'
|
||||
import { use } from 'react';
|
||||
|
||||
type Props = { params: Promise<{ slug: string }> }
|
||||
type Props = { params: Promise<{ slug: string }> };
|
||||
|
||||
export default function Page({ params }: Props) {
|
||||
const { slug } = use(params)
|
||||
const { slug } = use(params);
|
||||
}
|
||||
```
|
||||
|
||||
### generateMetadata
|
||||
|
||||
```tsx
|
||||
type Props = { params: Promise<{ slug: string }> }
|
||||
type Props = { params: Promise<{ slug: string }> };
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { slug } = await params
|
||||
return { title: slug }
|
||||
const { slug } = await params;
|
||||
return { title: slug };
|
||||
}
|
||||
```
|
||||
|
||||
## Async Cookies and Headers
|
||||
|
||||
```tsx
|
||||
import { cookies, headers } from 'next/headers'
|
||||
import { cookies, headers } from 'next/headers';
|
||||
|
||||
export default async function Page() {
|
||||
const cookieStore = await cookies()
|
||||
const headersList = await headers()
|
||||
const cookieStore = await cookies();
|
||||
const headersList = await headers();
|
||||
|
||||
const theme = cookieStore.get('theme')
|
||||
const userAgent = headersList.get('user-agent')
|
||||
const theme = cookieStore.get('theme');
|
||||
const userAgent = headersList.get('user-agent');
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -21,21 +21,21 @@ If the package is only needed on client:
|
||||
|
||||
```tsx
|
||||
// Bad: Fails - package uses window
|
||||
import SomeChart from 'some-chart-library'
|
||||
import SomeChart from 'some-chart-library';
|
||||
|
||||
export default function Page() {
|
||||
return <SomeChart />
|
||||
return <SomeChart />;
|
||||
}
|
||||
|
||||
// Good: Use dynamic import with ssr: false
|
||||
import dynamic from 'next/dynamic'
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const SomeChart = dynamic(() => import('some-chart-library'), {
|
||||
ssr: false,
|
||||
})
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return <SomeChart />
|
||||
return <SomeChart />;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -47,10 +47,11 @@ For packages that should run on server but have bundling issues:
|
||||
// next.config.js
|
||||
module.exports = {
|
||||
serverExternalPackages: ['problematic-package'],
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Use this for:
|
||||
|
||||
- Packages with native bindings (sharp, bcrypt)
|
||||
- Packages that don't bundle well (some ORMs)
|
||||
- Packages with circular dependencies
|
||||
@@ -61,19 +62,19 @@ Wrap the entire usage in a client component:
|
||||
|
||||
```tsx
|
||||
// components/ChartWrapper.tsx
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import { Chart } from 'chart-library'
|
||||
import { Chart } from 'chart-library';
|
||||
|
||||
export function ChartWrapper(props) {
|
||||
return <Chart {...props} />
|
||||
return <Chart {...props} />;
|
||||
}
|
||||
|
||||
// app/page.tsx (server component)
|
||||
import { ChartWrapper } from '@/components/ChartWrapper'
|
||||
import { ChartWrapper } from '@/components/ChartWrapper';
|
||||
|
||||
export default function Page() {
|
||||
return <ChartWrapper data={data} />
|
||||
return <ChartWrapper data={data} />;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -83,13 +84,13 @@ Import CSS files instead of using `<link>` tags. Next.js handles bundling and op
|
||||
|
||||
```tsx
|
||||
// Bad: Manual link tag
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
<link rel="stylesheet" href="/styles.css" />;
|
||||
|
||||
// Good: Import CSS
|
||||
import './styles.css'
|
||||
import './styles.css';
|
||||
|
||||
// Good: CSS Modules
|
||||
import styles from './Button.module.css'
|
||||
import styles from './Button.module.css';
|
||||
```
|
||||
|
||||
## Polyfills
|
||||
@@ -121,21 +122,21 @@ Module not found: ESM packages need to be imported
|
||||
// next.config.js
|
||||
module.exports = {
|
||||
transpilePackages: ['some-esm-package', 'another-package'],
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Common Problematic Packages
|
||||
|
||||
| Package | Issue | Solution |
|
||||
|---------|-------|----------|
|
||||
| `sharp` | Native bindings | `serverExternalPackages: ['sharp']` |
|
||||
| `bcrypt` | Native bindings | `serverExternalPackages: ['bcrypt']` or use `bcryptjs` |
|
||||
| `canvas` | Native bindings | `serverExternalPackages: ['canvas']` |
|
||||
| `recharts` | Uses window | `dynamic(() => import('recharts'), { ssr: false })` |
|
||||
| `react-quill` | Uses document | `dynamic(() => import('react-quill'), { ssr: false })` |
|
||||
| `mapbox-gl` | Uses window | `dynamic(() => import('mapbox-gl'), { ssr: false })` |
|
||||
| `monaco-editor` | Uses window | `dynamic(() => import('@monaco-editor/react'), { ssr: false })` |
|
||||
| `lottie-web` | Uses document | `dynamic(() => import('lottie-react'), { ssr: false })` |
|
||||
| Package | Issue | Solution |
|
||||
| --------------- | --------------- | --------------------------------------------------------------- |
|
||||
| `sharp` | Native bindings | `serverExternalPackages: ['sharp']` |
|
||||
| `bcrypt` | Native bindings | `serverExternalPackages: ['bcrypt']` or use `bcryptjs` |
|
||||
| `canvas` | Native bindings | `serverExternalPackages: ['canvas']` |
|
||||
| `recharts` | Uses window | `dynamic(() => import('recharts'), { ssr: false })` |
|
||||
| `react-quill` | Uses document | `dynamic(() => import('react-quill'), { ssr: false })` |
|
||||
| `mapbox-gl` | Uses window | `dynamic(() => import('mapbox-gl'), { ssr: false })` |
|
||||
| `monaco-editor` | Uses window | `dynamic(() => import('@monaco-editor/react'), { ssr: false })` |
|
||||
| `lottie-web` | Uses document | `dynamic(() => import('lottie-react'), { ssr: false })` |
|
||||
|
||||
## Bundle Analysis
|
||||
|
||||
@@ -146,6 +147,7 @@ next experimental-analyze
|
||||
```
|
||||
|
||||
This opens an interactive UI to:
|
||||
|
||||
- Filter by route, environment (client/server), and type
|
||||
- Inspect module sizes and import chains
|
||||
- View treemap visualization
|
||||
@@ -174,7 +176,7 @@ module.exports = {
|
||||
webpack: (config) => {
|
||||
// custom webpack config
|
||||
},
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Reference: https://nextjs.org/docs/app/building-your-application/upgrading/from-webpack-to-turbopack
|
||||
|
||||
@@ -33,17 +33,20 @@ async function UsersPage() {
|
||||
const users = await db.user.findMany();
|
||||
|
||||
// Or fetch from external API
|
||||
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
|
||||
const posts = await fetch('https://api.example.com/posts').then((r) => r.json());
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{users.map(user => <li key={user.id}>{user.name}</li>)}
|
||||
{users.map((user) => (
|
||||
<li key={user.id}>{user.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
|
||||
- No API to maintain
|
||||
- No client-server waterfall
|
||||
- Secrets stay on server
|
||||
@@ -89,12 +92,14 @@ export default function NewPost() {
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
|
||||
- End-to-end type safety
|
||||
- Progressive enhancement (works without JS)
|
||||
- Automatic request handling
|
||||
- Integrated with React transitions
|
||||
|
||||
**Constraints**:
|
||||
|
||||
- POST only (no GET caching semantics)
|
||||
- Internal use only (no external access)
|
||||
- Cannot return non-serializable data
|
||||
@@ -122,12 +127,14 @@ export async function POST(request: NextRequest) {
|
||||
```
|
||||
|
||||
**When to use**:
|
||||
|
||||
- External API access (mobile apps, third parties)
|
||||
- Webhooks from external services
|
||||
- GET endpoints that need HTTP caching
|
||||
- OpenAPI/Swagger documentation needed
|
||||
|
||||
**When NOT to use**:
|
||||
|
||||
- Internal data fetching (use Server Components)
|
||||
- Mutations from your UI (use Server Actions)
|
||||
|
||||
@@ -138,8 +145,8 @@ export async function POST(request: NextRequest) {
|
||||
```tsx
|
||||
// Bad: Sequential waterfalls
|
||||
async function Dashboard() {
|
||||
const user = await getUser(); // Wait...
|
||||
const posts = await getPosts(); // Then wait...
|
||||
const user = await getUser(); // Wait...
|
||||
const posts = await getPosts(); // Then wait...
|
||||
const comments = await getComments(); // Then wait...
|
||||
|
||||
return <div>...</div>;
|
||||
@@ -151,11 +158,7 @@ async function Dashboard() {
|
||||
```tsx
|
||||
// Good: Parallel fetching
|
||||
async function Dashboard() {
|
||||
const [user, posts, comments] = await Promise.all([
|
||||
getUser(),
|
||||
getPosts(),
|
||||
getComments(),
|
||||
]);
|
||||
const [user, posts, comments] = await Promise.all([getUser(), getPosts(), getComments()]);
|
||||
|
||||
return <div>...</div>;
|
||||
}
|
||||
@@ -238,7 +241,7 @@ async function Page() {
|
||||
}
|
||||
|
||||
// Client Component
|
||||
'use client';
|
||||
('use client');
|
||||
function ClientComponent({ initialData }) {
|
||||
const [data, setData] = useState(initialData);
|
||||
// ...
|
||||
@@ -256,7 +259,7 @@ function ClientComponent() {
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/data')
|
||||
.then(r => r.json())
|
||||
.then((r) => r.json())
|
||||
.then(setData);
|
||||
}, []);
|
||||
|
||||
@@ -289,9 +292,9 @@ function ClientComponent() {
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Use Case | HTTP Method | Caching |
|
||||
|---------|----------|-------------|---------|
|
||||
| Server Component fetch | Internal reads | Any | Full Next.js caching |
|
||||
| Server Action | Mutations, form submissions | POST only | No |
|
||||
| Route Handler | External APIs, webhooks | Any | GET can be cached |
|
||||
| Client fetch to API | Client-side reads | Any | HTTP cache headers |
|
||||
| Pattern | Use Case | HTTP Method | Caching |
|
||||
| ---------------------- | --------------------------- | ----------- | -------------------- |
|
||||
| Server Component fetch | Internal reads | Any | Full Next.js caching |
|
||||
| Server Action | Mutations, form submissions | POST only | No |
|
||||
| Route Handler | External APIs, webhooks | Any | GET can be cached |
|
||||
| Client fetch to API | Client-side reads | Any | HTTP cache headers |
|
||||
|
||||
@@ -35,42 +35,58 @@ curl -X POST http://localhost:<port>/_next/mcp \
|
||||
### Available Tools
|
||||
|
||||
#### `get_errors`
|
||||
|
||||
Get current errors from dev server (build errors, runtime errors with source-mapped stacks):
|
||||
|
||||
```json
|
||||
{ "name": "get_errors", "arguments": {} }
|
||||
```
|
||||
|
||||
#### `get_routes`
|
||||
|
||||
Discover all routes by scanning filesystem:
|
||||
|
||||
```json
|
||||
{ "name": "get_routes", "arguments": {} }
|
||||
// Optional: { "name": "get_routes", "arguments": { "routerType": "app" } }
|
||||
```
|
||||
|
||||
Returns: `{ "appRouter": ["/", "/api/users/[id]", ...], "pagesRouter": [...] }`
|
||||
|
||||
#### `get_project_metadata`
|
||||
|
||||
Get project path and dev server URL:
|
||||
|
||||
```json
|
||||
{ "name": "get_project_metadata", "arguments": {} }
|
||||
```
|
||||
|
||||
Returns: `{ "projectPath": "/path/to/project", "devServerUrl": "http://localhost:3000" }`
|
||||
|
||||
#### `get_page_metadata`
|
||||
|
||||
Get runtime metadata about current page render (requires active browser session):
|
||||
|
||||
```json
|
||||
{ "name": "get_page_metadata", "arguments": {} }
|
||||
```
|
||||
|
||||
Returns segment trie data showing layouts, boundaries, and page components.
|
||||
|
||||
#### `get_logs`
|
||||
|
||||
Get path to Next.js development log file:
|
||||
|
||||
```json
|
||||
{ "name": "get_logs", "arguments": {} }
|
||||
```
|
||||
|
||||
Returns path to `<distDir>/logs/next-development.log`
|
||||
|
||||
#### `get_server_action_by_id`
|
||||
|
||||
Locate a Server Action by ID:
|
||||
|
||||
```json
|
||||
{ "name": "get_server_action_by_id", "arguments": { "actionId": "<action-id>" } }
|
||||
```
|
||||
@@ -100,6 +116,7 @@ next build --debug-build-paths "/blog/[slug]"
|
||||
```
|
||||
|
||||
Use this to:
|
||||
|
||||
- Quickly verify a build fix without full rebuild
|
||||
- Debug static generation issues for specific pages
|
||||
- Iterate faster on build errors
|
||||
|
||||
@@ -7,18 +7,19 @@ These are React directives, not Next.js specific.
|
||||
### `'use client'`
|
||||
|
||||
Marks a component as a Client Component. Required for:
|
||||
|
||||
- React hooks (`useState`, `useEffect`, etc.)
|
||||
- Event handlers (`onClick`, `onChange`)
|
||||
- Browser APIs (`window`, `localStorage`)
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useState } from 'react';
|
||||
|
||||
export function Counter() {
|
||||
const [count, setCount] = useState(0)
|
||||
return <button onClick={() => setCount(count + 1)}>{count}</button>
|
||||
const [count, setCount] = useState(0);
|
||||
return <button onClick={() => setCount(count + 1)}>{count}</button>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -29,7 +30,7 @@ Reference: https://react.dev/reference/rsc/use-client
|
||||
Marks a function as a Server Action. Can be passed to Client Components.
|
||||
|
||||
```tsx
|
||||
'use server'
|
||||
'use server';
|
||||
|
||||
export async function submitForm(formData: FormData) {
|
||||
// Runs on server
|
||||
@@ -41,10 +42,10 @@ Or inline within a Server Component:
|
||||
```tsx
|
||||
export default function Page() {
|
||||
async function submit() {
|
||||
'use server'
|
||||
'use server';
|
||||
// Runs on server
|
||||
}
|
||||
return <form action={submit}>...</form>
|
||||
return <form action={submit}>...</form>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -59,10 +60,10 @@ Reference: https://react.dev/reference/rsc/use-server
|
||||
Marks a function or component for caching. Part of Next.js Cache Components.
|
||||
|
||||
```tsx
|
||||
'use cache'
|
||||
'use cache';
|
||||
|
||||
export async function getCachedData() {
|
||||
return await fetchData()
|
||||
return await fetchData();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -11,21 +11,15 @@ Reference: https://nextjs.org/docs/app/getting-started/error-handling
|
||||
Catches errors in a route segment and its children:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string }
|
||||
reset: () => void
|
||||
}) {
|
||||
export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
|
||||
return (
|
||||
<div>
|
||||
<h2>Something went wrong!</h2>
|
||||
<button onClick={() => reset()}>Try again</button>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -36,15 +30,9 @@ export default function Error({
|
||||
Catches errors in root layout:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
export default function GlobalError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string }
|
||||
reset: () => void
|
||||
}) {
|
||||
export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
@@ -52,7 +40,7 @@ export default function GlobalError({
|
||||
<button onClick={() => reset()}>Try again</button>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -107,6 +95,7 @@ async function createPost(formData: FormData) {
|
||||
```
|
||||
|
||||
Same applies to:
|
||||
|
||||
- `redirect()` - 307 temporary redirect
|
||||
- `permanentRedirect()` - 308 permanent redirect
|
||||
- `notFound()` - 404 not found
|
||||
@@ -116,15 +105,15 @@ Same applies to:
|
||||
Use `unstable_rethrow()` to re-throw these errors in catch blocks:
|
||||
|
||||
```tsx
|
||||
import { unstable_rethrow } from 'next/navigation'
|
||||
import { unstable_rethrow } from 'next/navigation';
|
||||
|
||||
async function action() {
|
||||
try {
|
||||
// ...
|
||||
redirect('/success')
|
||||
redirect('/success');
|
||||
} catch (error) {
|
||||
unstable_rethrow(error) // Re-throws Next.js internal errors
|
||||
return { error: 'Something went wrong' }
|
||||
unstable_rethrow(error); // Re-throws Next.js internal errors
|
||||
return { error: 'Something went wrong' };
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -132,13 +121,13 @@ async function action() {
|
||||
## Redirects
|
||||
|
||||
```tsx
|
||||
import { redirect, permanentRedirect } from 'next/navigation'
|
||||
import { redirect, permanentRedirect } from 'next/navigation';
|
||||
|
||||
// 307 Temporary - use for most cases
|
||||
redirect('/new-path')
|
||||
redirect('/new-path');
|
||||
|
||||
// 308 Permanent - use for URL migrations (cached by browsers)
|
||||
permanentRedirect('/new-url')
|
||||
permanentRedirect('/new-url');
|
||||
```
|
||||
|
||||
## Auth Errors
|
||||
@@ -146,20 +135,20 @@ permanentRedirect('/new-url')
|
||||
Trigger auth-related error pages:
|
||||
|
||||
```tsx
|
||||
import { forbidden, unauthorized } from 'next/navigation'
|
||||
import { forbidden, unauthorized } from 'next/navigation';
|
||||
|
||||
async function Page() {
|
||||
const session = await getSession()
|
||||
const session = await getSession();
|
||||
|
||||
if (!session) {
|
||||
unauthorized() // Renders unauthorized.tsx (401)
|
||||
unauthorized(); // Renders unauthorized.tsx (401)
|
||||
}
|
||||
|
||||
if (!session.hasAccess) {
|
||||
forbidden() // Renders forbidden.tsx (403)
|
||||
forbidden(); // Renders forbidden.tsx (403)
|
||||
}
|
||||
|
||||
return <Dashboard />
|
||||
return <Dashboard />;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -168,12 +157,12 @@ Create corresponding error pages:
|
||||
```tsx
|
||||
// app/forbidden.tsx
|
||||
export default function Forbidden() {
|
||||
return <div>You don't have access to this resource</div>
|
||||
return <div>You don't have access to this resource</div>;
|
||||
}
|
||||
|
||||
// app/unauthorized.tsx
|
||||
export default function Unauthorized() {
|
||||
return <div>Please log in to continue</div>
|
||||
return <div>Please log in to continue</div>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -190,24 +179,24 @@ export default function NotFound() {
|
||||
<h2>Not Found</h2>
|
||||
<p>Could not find the requested resource</p>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Triggering Not Found
|
||||
|
||||
```tsx
|
||||
import { notFound } from 'next/navigation'
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
const post = await getPost(id)
|
||||
const { id } = await params;
|
||||
const post = await getPost(id);
|
||||
|
||||
if (!post) {
|
||||
notFound() // Renders closest not-found.tsx
|
||||
notFound(); // Renders closest not-found.tsx
|
||||
}
|
||||
|
||||
return <div>{post.title}</div>
|
||||
return <div>{post.title}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -27,16 +27,16 @@ app/
|
||||
|
||||
## Special Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `page.tsx` | UI for a route segment |
|
||||
| `layout.tsx` | Shared UI for segment and children |
|
||||
| `loading.tsx` | Loading UI (Suspense boundary) |
|
||||
| `error.tsx` | Error UI (Error boundary) |
|
||||
| `not-found.tsx` | 404 UI |
|
||||
| `route.ts` | API endpoint |
|
||||
| `template.tsx` | Like layout but re-renders on navigation |
|
||||
| `default.tsx` | Fallback for parallel routes |
|
||||
| File | Purpose |
|
||||
| --------------- | ---------------------------------------- |
|
||||
| `page.tsx` | UI for a route segment |
|
||||
| `layout.tsx` | Shared UI for segment and children |
|
||||
| `loading.tsx` | Loading UI (Suspense boundary) |
|
||||
| `error.tsx` | Error UI (Error boundary) |
|
||||
| `not-found.tsx` | 404 UI |
|
||||
| `route.ts` | API endpoint |
|
||||
| `template.tsx` | Like layout but re-renders on navigation |
|
||||
| `default.tsx` | Fallback for parallel routes |
|
||||
|
||||
## Route Segments
|
||||
|
||||
@@ -74,6 +74,7 @@ app/
|
||||
```
|
||||
|
||||
Conventions:
|
||||
|
||||
- `(.)` - same level
|
||||
- `(..)` - one level up
|
||||
- `(..)(..)` - two levels up
|
||||
@@ -128,10 +129,10 @@ export const proxyConfig = {
|
||||
};
|
||||
```
|
||||
|
||||
| Version | File | Export | Config |
|
||||
|---------|------|--------|--------|
|
||||
| v14-15 | `middleware.ts` | `middleware()` | `config` |
|
||||
| v16+ | `proxy.ts` | `proxy()` | `proxyConfig` |
|
||||
| Version | File | Export | Config |
|
||||
| ------- | --------------- | -------------- | ------------- |
|
||||
| v14-15 | `middleware.ts` | `middleware()` | `config` |
|
||||
| v16+ | `proxy.ts` | `proxy()` | `proxyConfig` |
|
||||
|
||||
**Migration**: Run `npx @next/codemod@latest upgrade` to auto-rename.
|
||||
|
||||
|
||||
@@ -6,44 +6,45 @@ Use `next/font` for automatic font optimization with zero layout shift.
|
||||
|
||||
```tsx
|
||||
// app/layout.tsx
|
||||
import { Inter } from 'next/font/google'
|
||||
import { Inter } from 'next/font/google';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" className={inter.className}>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Multiple Fonts
|
||||
|
||||
```tsx
|
||||
import { Inter, Roboto_Mono } from 'next/font/google'
|
||||
import { Inter, Roboto_Mono } from 'next/font/google';
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-inter',
|
||||
})
|
||||
});
|
||||
|
||||
const robotoMono = Roboto_Mono({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-roboto-mono',
|
||||
})
|
||||
});
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Use in CSS:
|
||||
|
||||
```css
|
||||
body {
|
||||
font-family: var(--font-inter);
|
||||
@@ -61,35 +62,35 @@ code {
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
weight: '400',
|
||||
})
|
||||
});
|
||||
|
||||
// Multiple weights
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
weight: ['400', '500', '700'],
|
||||
})
|
||||
});
|
||||
|
||||
// Variable font (recommended) - includes all weights
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
// No weight needed - variable fonts support all weights
|
||||
})
|
||||
});
|
||||
|
||||
// With italic
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
style: ['normal', 'italic'],
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Local Fonts
|
||||
|
||||
```tsx
|
||||
import localFont from 'next/font/local'
|
||||
import localFont from 'next/font/local';
|
||||
|
||||
const myFont = localFont({
|
||||
src: './fonts/MyFont.woff2',
|
||||
})
|
||||
});
|
||||
|
||||
// Multiple files for different weights
|
||||
const myFont = localFont({
|
||||
@@ -105,32 +106,32 @@ const myFont = localFont({
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
// Variable font
|
||||
const myFont = localFont({
|
||||
src: './fonts/MyFont-Variable.woff2',
|
||||
variable: '--font-my-font',
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Tailwind CSS Integration
|
||||
|
||||
```tsx
|
||||
// app/layout.tsx
|
||||
import { Inter } from 'next/font/google'
|
||||
import { Inter } from 'next/font/google';
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-inter',
|
||||
})
|
||||
});
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="en" className={inter.variable}>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -144,7 +145,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Preloading Subsets
|
||||
@@ -153,10 +154,10 @@ Only load needed character subsets:
|
||||
|
||||
```tsx
|
||||
// Latin only (most common)
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
// Multiple subsets
|
||||
const inter = Inter({ subsets: ['latin', 'latin-ext', 'cyrillic'] })
|
||||
const inter = Inter({ subsets: ['latin', 'latin-ext', 'cyrillic'] });
|
||||
```
|
||||
|
||||
## Display Strategy
|
||||
@@ -167,7 +168,7 @@ Control font loading behavior:
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
display: 'swap', // Default - shows fallback, swaps when loaded
|
||||
})
|
||||
});
|
||||
|
||||
// Options:
|
||||
// 'auto' - browser decides
|
||||
@@ -231,15 +232,15 @@ const inter = Inter({ subsets: ['latin'] })
|
||||
```tsx
|
||||
// For component-specific fonts, export from a shared file
|
||||
// lib/fonts.ts
|
||||
import { Inter, Playfair_Display } from 'next/font/google'
|
||||
import { Inter, Playfair_Display } from 'next/font/google';
|
||||
|
||||
export const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
|
||||
export const playfair = Playfair_Display({ subsets: ['latin'], variable: '--font-playfair' })
|
||||
export const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });
|
||||
export const playfair = Playfair_Display({ subsets: ['latin'], variable: '--font-playfair' });
|
||||
|
||||
// components/Heading.tsx
|
||||
import { playfair } from '@/lib/fonts'
|
||||
import { playfair } from '@/lib/fonts';
|
||||
|
||||
export function Heading({ children }) {
|
||||
return <h1 className={playfair.className}>{children}</h1>
|
||||
return <h1 className={playfair.className}>{children}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -6,45 +6,45 @@ Reference: https://nextjs.org/docs/app/api-reference/functions
|
||||
|
||||
## Navigation Hooks (Client)
|
||||
|
||||
| Hook | Purpose | Reference |
|
||||
|------|---------|-----------|
|
||||
| `useRouter` | Programmatic navigation (`push`, `replace`, `back`, `refresh`) | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-router) |
|
||||
| `usePathname` | Get current pathname | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-pathname) |
|
||||
| `useSearchParams` | Read URL search parameters | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-search-params) |
|
||||
| `useParams` | Access dynamic route parameters | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-params) |
|
||||
| `useSelectedLayoutSegment` | Active child segment (one level) | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segment) |
|
||||
| `useSelectedLayoutSegments` | All active segments below layout | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segments) |
|
||||
| `useLinkStatus` | Check link prefetch status | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-link-status) |
|
||||
| `useReportWebVitals` | Report Core Web Vitals metrics | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-report-web-vitals) |
|
||||
| Hook | Purpose | Reference |
|
||||
| --------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| `useRouter` | Programmatic navigation (`push`, `replace`, `back`, `refresh`) | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-router) |
|
||||
| `usePathname` | Get current pathname | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-pathname) |
|
||||
| `useSearchParams` | Read URL search parameters | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-search-params) |
|
||||
| `useParams` | Access dynamic route parameters | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-params) |
|
||||
| `useSelectedLayoutSegment` | Active child segment (one level) | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segment) |
|
||||
| `useSelectedLayoutSegments` | All active segments below layout | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segments) |
|
||||
| `useLinkStatus` | Check link prefetch status | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-link-status) |
|
||||
| `useReportWebVitals` | Report Core Web Vitals metrics | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-report-web-vitals) |
|
||||
|
||||
## Server Functions
|
||||
|
||||
| Function | Purpose | Reference |
|
||||
|----------|---------|-----------|
|
||||
| `cookies` | Read/write cookies | [Docs](https://nextjs.org/docs/app/api-reference/functions/cookies) |
|
||||
| `headers` | Read request headers | [Docs](https://nextjs.org/docs/app/api-reference/functions/headers) |
|
||||
| `draftMode` | Enable preview of unpublished CMS content | [Docs](https://nextjs.org/docs/app/api-reference/functions/draft-mode) |
|
||||
| `after` | Run code after response finishes streaming | [Docs](https://nextjs.org/docs/app/api-reference/functions/after) |
|
||||
| Function | Purpose | Reference |
|
||||
| ------------ | -------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| `cookies` | Read/write cookies | [Docs](https://nextjs.org/docs/app/api-reference/functions/cookies) |
|
||||
| `headers` | Read request headers | [Docs](https://nextjs.org/docs/app/api-reference/functions/headers) |
|
||||
| `draftMode` | Enable preview of unpublished CMS content | [Docs](https://nextjs.org/docs/app/api-reference/functions/draft-mode) |
|
||||
| `after` | Run code after response finishes streaming | [Docs](https://nextjs.org/docs/app/api-reference/functions/after) |
|
||||
| `connection` | Wait for connection before dynamic rendering | [Docs](https://nextjs.org/docs/app/api-reference/functions/connection) |
|
||||
| `userAgent` | Parse User-Agent header | [Docs](https://nextjs.org/docs/app/api-reference/functions/userAgent) |
|
||||
| `userAgent` | Parse User-Agent header | [Docs](https://nextjs.org/docs/app/api-reference/functions/userAgent) |
|
||||
|
||||
## Generate Functions
|
||||
|
||||
| Function | Purpose | Reference |
|
||||
|----------|---------|-----------|
|
||||
| `generateStaticParams` | Pre-render dynamic routes at build time | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-static-params) |
|
||||
| `generateMetadata` | Dynamic metadata | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) |
|
||||
| `generateViewport` | Dynamic viewport config | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-viewport) |
|
||||
| `generateSitemaps` | Multiple sitemaps for large sites | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-sitemaps) |
|
||||
| `generateImageMetadata` | Multiple OG images per route | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-image-metadata) |
|
||||
| Function | Purpose | Reference |
|
||||
| ----------------------- | --------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `generateStaticParams` | Pre-render dynamic routes at build time | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-static-params) |
|
||||
| `generateMetadata` | Dynamic metadata | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) |
|
||||
| `generateViewport` | Dynamic viewport config | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-viewport) |
|
||||
| `generateSitemaps` | Multiple sitemaps for large sites | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-sitemaps) |
|
||||
| `generateImageMetadata` | Multiple OG images per route | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-image-metadata) |
|
||||
|
||||
## Request/Response
|
||||
|
||||
| Function | Purpose | Reference |
|
||||
|----------|---------|-----------|
|
||||
| `NextRequest` | Extended Request with helpers | [Docs](https://nextjs.org/docs/app/api-reference/functions/next-request) |
|
||||
| `NextResponse` | Extended Response with helpers | [Docs](https://nextjs.org/docs/app/api-reference/functions/next-response) |
|
||||
| `ImageResponse` | Generate OG images | [Docs](https://nextjs.org/docs/app/api-reference/functions/image-response) |
|
||||
| Function | Purpose | Reference |
|
||||
| --------------- | ------------------------------ | -------------------------------------------------------------------------- |
|
||||
| `NextRequest` | Extended Request with helpers | [Docs](https://nextjs.org/docs/app/api-reference/functions/next-request) |
|
||||
| `NextResponse` | Extended Response with helpers | [Docs](https://nextjs.org/docs/app/api-reference/functions/next-response) |
|
||||
| `ImageResponse` | Generate OG images | [Docs](https://nextjs.org/docs/app/api-reference/functions/image-response) |
|
||||
|
||||
## Common Examples
|
||||
|
||||
@@ -54,30 +54,30 @@ Use `next/link` for internal navigation instead of `<a>` tags.
|
||||
|
||||
```tsx
|
||||
// Bad: Plain anchor tag
|
||||
<a href="/about">About</a>
|
||||
<a href="/about">About</a>;
|
||||
|
||||
// Good: Next.js Link
|
||||
import Link from 'next/link'
|
||||
import Link from 'next/link';
|
||||
|
||||
<Link href="/about">About</Link>
|
||||
<Link href="/about">About</Link>;
|
||||
```
|
||||
|
||||
Active link styling:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
export function NavLink({ href, children }) {
|
||||
const pathname = usePathname()
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<Link href={href} className={pathname === href ? 'active' : ''}>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -86,23 +86,23 @@ export function NavLink({ href, children }) {
|
||||
```tsx
|
||||
// app/blog/[slug]/page.tsx
|
||||
export async function generateStaticParams() {
|
||||
const posts = await getPosts()
|
||||
return posts.map((post) => ({ slug: post.slug }))
|
||||
const posts = await getPosts();
|
||||
return posts.map((post) => ({ slug: post.slug }));
|
||||
}
|
||||
```
|
||||
|
||||
### After Response
|
||||
|
||||
```tsx
|
||||
import { after } from 'next/server'
|
||||
import { after } from 'next/server';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const data = await processRequest(request)
|
||||
const data = await processRequest(request);
|
||||
|
||||
after(async () => {
|
||||
await logAnalytics(data)
|
||||
})
|
||||
await logAnalytics(data);
|
||||
});
|
||||
|
||||
return Response.json({ success: true })
|
||||
return Response.json({ success: true });
|
||||
}
|
||||
```
|
||||
|
||||
@@ -17,16 +17,16 @@ In development, click the hydration error to see the server/client diff.
|
||||
|
||||
```tsx
|
||||
// Bad: Causes mismatch - window doesn't exist on server
|
||||
<div>{window.innerWidth}</div>
|
||||
<div>{window.innerWidth}</div>;
|
||||
|
||||
// Good: Use client component with mounted check
|
||||
'use client'
|
||||
import { useState, useEffect } from 'react'
|
||||
('use client');
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export function ClientOnly({ children }: { children: React.ReactNode }) {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
useEffect(() => setMounted(true), [])
|
||||
return mounted ? children : null
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => setMounted(true), []);
|
||||
return mounted ? children : null;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -36,12 +36,12 @@ Server and client may be in different timezones:
|
||||
|
||||
```tsx
|
||||
// Bad: Causes mismatch
|
||||
<span>{new Date().toLocaleString()}</span>
|
||||
<span>{new Date().toLocaleString()}</span>;
|
||||
|
||||
// Good: Render on client only
|
||||
'use client'
|
||||
const [time, setTime] = useState<string>()
|
||||
useEffect(() => setTime(new Date().toLocaleString()), [])
|
||||
('use client');
|
||||
const [time, setTime] = useState<string>();
|
||||
useEffect(() => setTime(new Date().toLocaleString()), []);
|
||||
```
|
||||
|
||||
### Random Values or IDs
|
||||
@@ -78,14 +78,9 @@ Scripts that modify DOM during hydration.
|
||||
|
||||
```tsx
|
||||
// Good: Use next/script with afterInteractive
|
||||
import Script from 'next/script'
|
||||
import Script from 'next/script';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Script
|
||||
src="https://example.com/script.js"
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
)
|
||||
return <Script src="https://example.com/script.js" strategy="afterInteractive" />;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -6,11 +6,11 @@ Use `next/image` for automatic image optimization.
|
||||
|
||||
```tsx
|
||||
// Bad: Avoid native img
|
||||
<img src="/hero.png" alt="Hero" />
|
||||
<img src="/hero.png" alt="Hero" />;
|
||||
|
||||
// Good: Use next/image
|
||||
import Image from 'next/image'
|
||||
<Image src="/hero.png" alt="Hero" width={800} height={400} />
|
||||
import Image from 'next/image';
|
||||
<Image src="/hero.png" alt="Hero" width={800} height={400} />;
|
||||
```
|
||||
|
||||
## Required Props
|
||||
@@ -51,7 +51,7 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Responsive Images
|
||||
@@ -155,19 +155,19 @@ When using `output: 'export'`, use `unoptimized` or custom loader:
|
||||
|
||||
```tsx
|
||||
// Option 1: Disable optimization
|
||||
<Image src="/hero.png" alt="Hero" width={800} height={400} unoptimized />
|
||||
<Image src="/hero.png" alt="Hero" width={800} height={400} unoptimized />;
|
||||
|
||||
// Option 2: Global config
|
||||
// next.config.js
|
||||
module.exports = {
|
||||
output: 'export',
|
||||
images: { unoptimized: true },
|
||||
}
|
||||
};
|
||||
|
||||
// Option 3: Custom loader (Cloudinary, Imgix, etc.)
|
||||
const cloudinaryLoader = ({ src, width, quality }) => {
|
||||
return `https://res.cloudinary.com/demo/image/upload/w_${width},q_${quality || 75}/${src}`
|
||||
}
|
||||
return `https://res.cloudinary.com/demo/image/upload/w_${width},q_${quality || 75}/${src}`;
|
||||
};
|
||||
|
||||
<Image loader={cloudinaryLoader} src="sample.jpg" alt="Sample" width={800} height={400} />
|
||||
<Image loader={cloudinaryLoader} src="sample.jpg" alt="Sample" width={800} height={400} />;
|
||||
```
|
||||
|
||||
@@ -7,6 +7,7 @@ Add SEO metadata to Next.js pages using the Metadata API.
|
||||
The `metadata` object and `generateMetadata` function are **only supported in Server Components**. They cannot be used in Client Components.
|
||||
|
||||
If the target page has `'use client'`:
|
||||
|
||||
1. Remove `'use client'` if possible, move client logic to child components
|
||||
2. Or extract metadata to a parent Server Component layout
|
||||
3. Or split the file: Server Component with metadata imports Client Components
|
||||
@@ -14,25 +15,25 @@ If the target page has `'use client'`:
|
||||
## Static Metadata
|
||||
|
||||
```tsx
|
||||
import type { Metadata } from 'next'
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Page Title',
|
||||
description: 'Page description for search engines',
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Dynamic Metadata
|
||||
|
||||
```tsx
|
||||
import type { Metadata } from 'next'
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
type Props = { params: Promise<{ slug: string }> }
|
||||
type Props = { params: Promise<{ slug: string }> };
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { slug } = await params
|
||||
const post = await getPost(slug)
|
||||
return { title: post.title, description: post.description }
|
||||
const { slug } = await params;
|
||||
const post = await getPost(slug);
|
||||
return { title: post.title, description: post.description };
|
||||
}
|
||||
```
|
||||
|
||||
@@ -41,11 +42,11 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
Use React `cache()` when the same data is needed for both metadata and page:
|
||||
|
||||
```tsx
|
||||
import { cache } from 'react'
|
||||
import { cache } from 'react';
|
||||
|
||||
export const getPost = cache(async (slug: string) => {
|
||||
return await db.posts.findFirst({ where: { slug } })
|
||||
})
|
||||
return await db.posts.findFirst({ where: { slug } });
|
||||
});
|
||||
```
|
||||
|
||||
## Viewport
|
||||
@@ -53,17 +54,17 @@ export const getPost = cache(async (slug: string) => {
|
||||
Separate from metadata for streaming support:
|
||||
|
||||
```tsx
|
||||
import type { Viewport } from 'next'
|
||||
import type { Viewport } from 'next';
|
||||
|
||||
export const viewport: Viewport = {
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
themeColor: '#000000',
|
||||
}
|
||||
};
|
||||
|
||||
// Or dynamic
|
||||
export function generateViewport({ params }): Viewport {
|
||||
return { themeColor: getThemeColor(params) }
|
||||
return { themeColor: getThemeColor(params) };
|
||||
}
|
||||
```
|
||||
|
||||
@@ -74,7 +75,7 @@ In root layout for consistent naming:
|
||||
```tsx
|
||||
export const metadata: Metadata = {
|
||||
title: { default: 'Site Name', template: '%s | Site Name' },
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Metadata File Conventions
|
||||
@@ -83,16 +84,16 @@ Reference: https://nextjs.org/docs/app/getting-started/project-structure#metadat
|
||||
|
||||
Place these files in `app/` directory (or route segments):
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `favicon.ico` | Favicon |
|
||||
| `icon.png` / `icon.svg` | App icon |
|
||||
| `apple-icon.png` | Apple app icon |
|
||||
| `opengraph-image.png` | OG image |
|
||||
| `twitter-image.png` | Twitter card image |
|
||||
| `sitemap.ts` / `sitemap.xml` | Sitemap (use `generateSitemaps` for multiple) |
|
||||
| `robots.ts` / `robots.txt` | Robots directives |
|
||||
| `manifest.ts` / `manifest.json` | Web app manifest |
|
||||
| File | Purpose |
|
||||
| ------------------------------- | --------------------------------------------- |
|
||||
| `favicon.ico` | Favicon |
|
||||
| `icon.png` / `icon.svg` | App icon |
|
||||
| `apple-icon.png` | Apple app icon |
|
||||
| `opengraph-image.png` | OG image |
|
||||
| `twitter-image.png` | Twitter card image |
|
||||
| `sitemap.ts` / `sitemap.xml` | Sitemap (use `generateSitemaps` for multiple) |
|
||||
| `robots.ts` / `robots.txt` | Robots directives |
|
||||
| `manifest.ts` / `manifest.json` | Web app manifest |
|
||||
|
||||
## SEO Best Practice: Static Files Are Often Enough
|
||||
|
||||
@@ -108,6 +109,7 @@ app/
|
||||
```
|
||||
|
||||
**Tips:**
|
||||
|
||||
- A single `opengraph-image.png` covers both Open Graph and Twitter (Twitter falls back to OG)
|
||||
- Static `title` and `description` in layout metadata is sufficient for most pages
|
||||
- Only use dynamic `generateMetadata` when content varies per page
|
||||
@@ -126,7 +128,7 @@ Generate dynamic Open Graph images using `next/og`.
|
||||
|
||||
```tsx
|
||||
// Good
|
||||
import { ImageResponse } from 'next/og'
|
||||
import { ImageResponse } from 'next/og';
|
||||
|
||||
// Bad
|
||||
// import { ImageResponse } from '@vercel/og'
|
||||
@@ -137,11 +139,11 @@ import { ImageResponse } from 'next/og'
|
||||
|
||||
```tsx
|
||||
// app/opengraph-image.tsx
|
||||
import { ImageResponse } from 'next/og'
|
||||
import { ImageResponse } from 'next/og';
|
||||
|
||||
export const alt = 'Site Name'
|
||||
export const size = { width: 1200, height: 630 }
|
||||
export const contentType = 'image/png'
|
||||
export const alt = 'Site Name';
|
||||
export const size = { width: 1200, height: 630 };
|
||||
export const contentType = 'image/png';
|
||||
|
||||
export default function Image() {
|
||||
return new ImageResponse(
|
||||
@@ -161,7 +163,7 @@ export default function Image() {
|
||||
</div>
|
||||
),
|
||||
{ ...size }
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -169,17 +171,17 @@ export default function Image() {
|
||||
|
||||
```tsx
|
||||
// app/blog/[slug]/opengraph-image.tsx
|
||||
import { ImageResponse } from 'next/og'
|
||||
import { ImageResponse } from 'next/og';
|
||||
|
||||
export const alt = 'Blog Post'
|
||||
export const size = { width: 1200, height: 630 }
|
||||
export const contentType = 'image/png'
|
||||
export const alt = 'Blog Post';
|
||||
export const size = { width: 1200, height: 630 };
|
||||
export const contentType = 'image/png';
|
||||
|
||||
type Props = { params: Promise<{ slug: string }> }
|
||||
type Props = { params: Promise<{ slug: string }> };
|
||||
|
||||
export default async function Image({ params }: Props) {
|
||||
const { slug } = await params
|
||||
const post = await getPost(slug)
|
||||
const { slug } = await params;
|
||||
const post = await getPost(slug);
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
@@ -202,33 +204,26 @@ export default async function Image({ params }: Props) {
|
||||
</div>
|
||||
),
|
||||
{ ...size }
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Fonts
|
||||
|
||||
```tsx
|
||||
import { ImageResponse } from 'next/og'
|
||||
import { join } from 'path'
|
||||
import { readFile } from 'fs/promises'
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { join } from 'path';
|
||||
import { readFile } from 'fs/promises';
|
||||
|
||||
export default async function Image() {
|
||||
const fontPath = join(process.cwd(), 'assets/fonts/Inter-Bold.ttf')
|
||||
const fontData = await readFile(fontPath)
|
||||
const fontPath = join(process.cwd(), 'assets/fonts/Inter-Bold.ttf');
|
||||
const fontData = await readFile(fontPath);
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div style={{ fontFamily: 'Inter', fontSize: 64 }}>
|
||||
Custom Font Text
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [{ name: 'Inter', data: fontData, style: 'normal' }],
|
||||
}
|
||||
)
|
||||
return new ImageResponse(<div style={{ fontFamily: 'Inter', fontSize: 64 }}>Custom Font Text</div>, {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [{ name: 'Inter', data: fontData, style: 'normal' }],
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
@@ -240,6 +235,7 @@ export default async function Image() {
|
||||
## Styling Notes
|
||||
|
||||
ImageResponse uses Flexbox layout:
|
||||
|
||||
- Use `display: 'flex'`
|
||||
- No CSS Grid support
|
||||
- Styles must be inline objects
|
||||
@@ -250,22 +246,22 @@ Use `generateImageMetadata` for multiple images per route:
|
||||
|
||||
```tsx
|
||||
// app/blog/[slug]/opengraph-image.tsx
|
||||
import { ImageResponse } from 'next/og'
|
||||
import { ImageResponse } from 'next/og';
|
||||
|
||||
export async function generateImageMetadata({ params }) {
|
||||
const images = await getPostImages(params.slug)
|
||||
const images = await getPostImages(params.slug);
|
||||
return images.map((img, idx) => ({
|
||||
id: idx,
|
||||
alt: img.alt,
|
||||
size: { width: 1200, height: 630 },
|
||||
contentType: 'image/png',
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
export default async function Image({ params, id }) {
|
||||
const images = await getPostImages(params.slug)
|
||||
const image = images[id]
|
||||
return new ImageResponse(/* ... */)
|
||||
const images = await getPostImages(params.slug);
|
||||
const image = images[id];
|
||||
return new ImageResponse(/* ... */);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -275,26 +271,22 @@ Use `generateSitemaps` for large sites:
|
||||
|
||||
```tsx
|
||||
// app/sitemap.ts
|
||||
import type { MetadataRoute } from 'next'
|
||||
import type { MetadataRoute } from 'next';
|
||||
|
||||
export async function generateSitemaps() {
|
||||
// Return array of sitemap IDs
|
||||
return [{ id: 0 }, { id: 1 }, { id: 2 }]
|
||||
return [{ id: 0 }, { id: 1 }, { id: 2 }];
|
||||
}
|
||||
|
||||
export default async function sitemap({
|
||||
id,
|
||||
}: {
|
||||
id: number
|
||||
}): Promise<MetadataRoute.Sitemap> {
|
||||
const start = id * 50000
|
||||
const end = start + 50000
|
||||
const products = await getProducts(start, end)
|
||||
export default async function sitemap({ id }: { id: number }): Promise<MetadataRoute.Sitemap> {
|
||||
const start = id * 50000;
|
||||
const end = start + 50000;
|
||||
const products = await getProducts(start, end);
|
||||
|
||||
return products.map((product) => ({
|
||||
url: `https://example.com/product/${product.id}`,
|
||||
lastModified: product.updatedAt,
|
||||
}))
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -24,13 +24,7 @@ app/
|
||||
|
||||
```tsx
|
||||
// app/layout.tsx
|
||||
export default function RootLayout({
|
||||
children,
|
||||
modal,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
modal: React.ReactNode;
|
||||
}) {
|
||||
export default function RootLayout({ children, modal }: { children: React.ReactNode; modal: React.ReactNode }) {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
@@ -63,11 +57,7 @@ The `(.)` prefix intercepts routes at the same level.
|
||||
// app/@modal/(.)photos/[id]/page.tsx
|
||||
import { Modal } from '@/components/modal';
|
||||
|
||||
export default async function PhotoModal({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
export default async function PhotoModal({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const photo = await getPhoto(id);
|
||||
|
||||
@@ -83,11 +73,7 @@ export default async function PhotoModal({
|
||||
|
||||
```tsx
|
||||
// app/photos/[id]/page.tsx
|
||||
export default async function PhotoPage({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
export default async function PhotoPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const photo = await getPhoto(id);
|
||||
|
||||
@@ -127,11 +113,14 @@ export function Modal({ children }: { children: React.ReactNode }) {
|
||||
}, [router]);
|
||||
|
||||
// Close on overlay click
|
||||
const handleOverlayClick = useCallback((e: React.MouseEvent) => {
|
||||
if (e.target === overlayRef.current) {
|
||||
router.back(); // Correct
|
||||
}
|
||||
}, [router]);
|
||||
const handleOverlayClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (e.target === overlayRef.current) {
|
||||
router.back(); // Correct
|
||||
}
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -156,11 +145,13 @@ export function Modal({ children }: { children: React.ReactNode }) {
|
||||
### Why NOT `router.push('/')` or `<Link href="/">`?
|
||||
|
||||
Using `push` or `Link` to "close" a modal:
|
||||
|
||||
1. Adds a new history entry (back button shows modal again)
|
||||
2. Doesn't properly clear the intercepted route
|
||||
3. Can cause the modal to flash or persist unexpectedly
|
||||
|
||||
`router.back()` correctly:
|
||||
|
||||
1. Removes the intercepted route from history
|
||||
2. Returns to the previous page
|
||||
3. Properly unmounts the modal
|
||||
@@ -169,18 +160,19 @@ Using `push` or `Link` to "close" a modal:
|
||||
|
||||
Matchers match **route segments**, not filesystem paths:
|
||||
|
||||
| Matcher | Matches | Example |
|
||||
|---------|---------|---------|
|
||||
| `(.)` | Same level | `@modal/(.)photos` intercepts `/photos` |
|
||||
| `(..)` | One level up | `@modal/(..)settings` from `/dashboard/@modal` intercepts `/settings` |
|
||||
| `(..)(..)` | Two levels up | Rarely used |
|
||||
| `(...)` | From root | `@modal/(...)photos` intercepts `/photos` from anywhere |
|
||||
| Matcher | Matches | Example |
|
||||
| ---------- | ------------- | --------------------------------------------------------------------- |
|
||||
| `(.)` | Same level | `@modal/(.)photos` intercepts `/photos` |
|
||||
| `(..)` | One level up | `@modal/(..)settings` from `/dashboard/@modal` intercepts `/settings` |
|
||||
| `(..)(..)` | Two levels up | Rarely used |
|
||||
| `(...)` | From root | `@modal/(...)photos` intercepts `/photos` from anywhere |
|
||||
|
||||
**Common mistake**: Thinking `(..)` means "parent folder" - it means "parent route segment".
|
||||
|
||||
## Handling Hard Navigation
|
||||
|
||||
When users directly visit `/photos/123` (bookmark, refresh, shared link):
|
||||
|
||||
- The intercepting route is bypassed
|
||||
- The full `photos/[id]/page.tsx` renders
|
||||
- Modal doesn't appear (expected behavior)
|
||||
@@ -230,6 +222,7 @@ app/
|
||||
### 4. Intercepted Route Shows Wrong Content
|
||||
|
||||
Check your matcher:
|
||||
|
||||
- `(.)photos` intercepts `/photos` from the same route level
|
||||
- If your `@modal` is in `app/dashboard/@modal`, use `(.)photos` to intercept `/dashboard/photos`, not `/photos`
|
||||
|
||||
@@ -272,7 +265,7 @@ export default async function Gallery() {
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{photos.map(photo => (
|
||||
{photos.map((photo) => (
|
||||
<Link key={photo.id} href={`/photos/${photo.id}`}>
|
||||
<img src={photo.thumbnail} alt={photo.title} />
|
||||
</Link>
|
||||
|
||||
@@ -7,14 +7,14 @@ Create API endpoints with `route.ts` files.
|
||||
```tsx
|
||||
// app/api/users/route.ts
|
||||
export async function GET() {
|
||||
const users = await getUsers()
|
||||
return Response.json(users)
|
||||
const users = await getUsers();
|
||||
return Response.json(users);
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json()
|
||||
const user = await createUser(body)
|
||||
return Response.json(user, { status: 201 })
|
||||
const body = await request.json();
|
||||
const user = await createUser(body);
|
||||
return Response.json(user, { status: 201 });
|
||||
}
|
||||
```
|
||||
|
||||
@@ -60,11 +60,11 @@ Route handlers run in a **Server Component-like environment**:
|
||||
|
||||
```tsx
|
||||
// Bad: This won't work - no React DOM in route handlers
|
||||
import { renderToString } from 'react-dom/server'
|
||||
import { renderToString } from 'react-dom/server';
|
||||
|
||||
export async function GET() {
|
||||
const html = renderToString(<Component />) // Error!
|
||||
return new Response(html)
|
||||
const html = renderToString(<Component />); // Error!
|
||||
return new Response(html);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -72,18 +72,15 @@ export async function GET() {
|
||||
|
||||
```tsx
|
||||
// app/api/users/[id]/route.ts
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await params
|
||||
const user = await getUser(id)
|
||||
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const user = await getUser(id);
|
||||
|
||||
if (!user) {
|
||||
return Response.json({ error: 'Not found' }, { status: 404 })
|
||||
return Response.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return Response.json(user)
|
||||
return Response.json(user);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -92,17 +89,17 @@ export async function GET(
|
||||
```tsx
|
||||
export async function GET(request: Request) {
|
||||
// URL and search params
|
||||
const { searchParams } = new URL(request.url)
|
||||
const query = searchParams.get('q')
|
||||
const { searchParams } = new URL(request.url);
|
||||
const query = searchParams.get('q');
|
||||
|
||||
// Headers
|
||||
const authHeader = request.headers.get('authorization')
|
||||
const authHeader = request.headers.get('authorization');
|
||||
|
||||
// Cookies (Next.js helper)
|
||||
const cookieStore = await cookies()
|
||||
const token = cookieStore.get('token')
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('token');
|
||||
|
||||
return Response.json({ query, token })
|
||||
return Response.json({ query, token });
|
||||
}
|
||||
```
|
||||
|
||||
@@ -110,37 +107,37 @@ export async function GET(request: Request) {
|
||||
|
||||
```tsx
|
||||
// JSON response
|
||||
return Response.json({ data })
|
||||
return Response.json({ data });
|
||||
|
||||
// With status
|
||||
return Response.json({ error: 'Not found' }, { status: 404 })
|
||||
return Response.json({ error: 'Not found' }, { status: 404 });
|
||||
|
||||
// With headers
|
||||
return Response.json(data, {
|
||||
headers: {
|
||||
'Cache-Control': 'max-age=3600',
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
// Redirect
|
||||
return Response.redirect(new URL('/login', request.url))
|
||||
return Response.redirect(new URL('/login', request.url));
|
||||
|
||||
// Stream
|
||||
return new Response(stream, {
|
||||
headers: { 'Content-Type': 'text/event-stream' },
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## When to Use Route Handlers vs Server Actions
|
||||
|
||||
| Use Case | Route Handlers | Server Actions |
|
||||
|----------|----------------|----------------|
|
||||
| Form submissions | No | Yes |
|
||||
| Data mutations from UI | No | Yes |
|
||||
| Third-party webhooks | Yes | No |
|
||||
| External API consumption | Yes | No |
|
||||
| Public REST API | Yes | No |
|
||||
| File uploads | Both work | Both work |
|
||||
| Use Case | Route Handlers | Server Actions |
|
||||
| ------------------------ | -------------- | -------------- |
|
||||
| Form submissions | No | Yes |
|
||||
| Data mutations from UI | No | Yes |
|
||||
| Third-party webhooks | Yes | No |
|
||||
| External API consumption | Yes | No |
|
||||
| Public REST API | Yes | No |
|
||||
| File uploads | Both work | Both work |
|
||||
|
||||
**Prefer Server Actions** for mutations triggered from your UI.
|
||||
**Use Route Handlers** for external integrations and public APIs.
|
||||
|
||||
@@ -12,33 +12,33 @@ Client components **cannot** be async functions. Only Server Components can be a
|
||||
|
||||
```tsx
|
||||
// Bad: async client component
|
||||
'use client'
|
||||
'use client';
|
||||
export default async function UserProfile() {
|
||||
const user = await getUser() // Cannot await in client component
|
||||
return <div>{user.name}</div>
|
||||
const user = await getUser(); // Cannot await in client component
|
||||
return <div>{user.name}</div>;
|
||||
}
|
||||
|
||||
// Good: Remove async, fetch data in parent server component
|
||||
// page.tsx (server component - no 'use client')
|
||||
export default async function Page() {
|
||||
const user = await getUser()
|
||||
return <UserProfile user={user} />
|
||||
const user = await getUser();
|
||||
return <UserProfile user={user} />;
|
||||
}
|
||||
|
||||
// UserProfile.tsx (client component)
|
||||
'use client'
|
||||
('use client');
|
||||
export function UserProfile({ user }: { user: User }) {
|
||||
return <div>{user.name}</div>
|
||||
return <div>{user.name}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Bad: async arrow function client component
|
||||
'use client'
|
||||
'use client';
|
||||
const Dashboard = async () => {
|
||||
const data = await fetchDashboard()
|
||||
return <div>{data}</div>
|
||||
}
|
||||
const data = await fetchDashboard();
|
||||
return <div>{data}</div>;
|
||||
};
|
||||
|
||||
// Good: Fetch in server component, pass data down
|
||||
```
|
||||
@@ -48,6 +48,7 @@ const Dashboard = async () => {
|
||||
Props passed from Server → Client must be JSON-serializable.
|
||||
|
||||
**Detect:** Server component passes these to a client component:
|
||||
|
||||
- Functions (except Server Actions with `'use server'`)
|
||||
- `Date` objects
|
||||
- `Map`, `Set`, `WeakMap`, `WeakSet`
|
||||
@@ -59,16 +60,16 @@ Props passed from Server → Client must be JSON-serializable.
|
||||
// Bad: Function prop
|
||||
// page.tsx (server)
|
||||
export default function Page() {
|
||||
const handleClick = () => console.log('clicked')
|
||||
return <ClientButton onClick={handleClick} />
|
||||
const handleClick = () => console.log('clicked');
|
||||
return <ClientButton onClick={handleClick} />;
|
||||
}
|
||||
|
||||
// Good: Define function inside client component
|
||||
// ClientButton.tsx
|
||||
'use client'
|
||||
('use client');
|
||||
export function ClientButton() {
|
||||
const handleClick = () => console.log('clicked')
|
||||
return <button onClick={handleClick}>Click</button>
|
||||
const handleClick = () => console.log('clicked');
|
||||
return <button onClick={handleClick}>Click</button>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -76,28 +77,28 @@ export function ClientButton() {
|
||||
// Bad: Date object (silently becomes string, then crashes)
|
||||
// page.tsx (server)
|
||||
export default async function Page() {
|
||||
const post = await getPost()
|
||||
return <PostCard createdAt={post.createdAt} /> // Date object
|
||||
const post = await getPost();
|
||||
return <PostCard createdAt={post.createdAt} />; // Date object
|
||||
}
|
||||
|
||||
// PostCard.tsx (client) - will crash on .getFullYear()
|
||||
'use client'
|
||||
('use client');
|
||||
export function PostCard({ createdAt }: { createdAt: Date }) {
|
||||
return <span>{createdAt.getFullYear()}</span> // Runtime error!
|
||||
return <span>{createdAt.getFullYear()}</span>; // Runtime error!
|
||||
}
|
||||
|
||||
// Good: Serialize to string on server
|
||||
// page.tsx (server)
|
||||
export default async function Page() {
|
||||
const post = await getPost()
|
||||
return <PostCard createdAt={post.createdAt.toISOString()} />
|
||||
const post = await getPost();
|
||||
return <PostCard createdAt={post.createdAt.toISOString()} />;
|
||||
}
|
||||
|
||||
// PostCard.tsx (client)
|
||||
'use client'
|
||||
('use client');
|
||||
export function PostCard({ createdAt }: { createdAt: string }) {
|
||||
const date = new Date(createdAt)
|
||||
return <span>{date.getFullYear()}</span>
|
||||
const date = new Date(createdAt);
|
||||
return <span>{date.getFullYear()}</span>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -127,33 +128,33 @@ Functions marked with `'use server'` CAN be passed to client components.
|
||||
```tsx
|
||||
// Valid: Server Action can be passed
|
||||
// actions.ts
|
||||
'use server'
|
||||
'use server';
|
||||
export async function submitForm(formData: FormData) {
|
||||
// server-side logic
|
||||
}
|
||||
|
||||
// page.tsx (server)
|
||||
import { submitForm } from './actions'
|
||||
import { submitForm } from './actions';
|
||||
export default function Page() {
|
||||
return <ClientForm onSubmit={submitForm} /> // OK!
|
||||
return <ClientForm onSubmit={submitForm} />; // OK!
|
||||
}
|
||||
|
||||
// ClientForm.tsx (client)
|
||||
'use client'
|
||||
('use client');
|
||||
export function ClientForm({ onSubmit }: { onSubmit: (data: FormData) => Promise<void> }) {
|
||||
return <form action={onSubmit}>...</form>
|
||||
return <form action={onSubmit}>...</form>;
|
||||
}
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Valid? | Fix |
|
||||
|---------|--------|-----|
|
||||
| `'use client'` + `async function` | No | Fetch in server parent, pass data |
|
||||
| Pass `() => {}` to client | No | Define in client or use server action |
|
||||
| Pass `new Date()` to client | No | Use `.toISOString()` |
|
||||
| Pass `new Map()` to client | No | Convert to object/array |
|
||||
| Pass class instance to client | No | Pass plain object |
|
||||
| Pass server action to client | Yes | - |
|
||||
| Pass `string/number/boolean` | Yes | - |
|
||||
| Pass plain object/array | Yes | - |
|
||||
| Pattern | Valid? | Fix |
|
||||
| --------------------------------- | ------ | ------------------------------------- |
|
||||
| `'use client'` + `async function` | No | Fetch in server parent, pass data |
|
||||
| Pass `() => {}` to client | No | Define in client or use server action |
|
||||
| Pass `new Date()` to client | No | Use `.toISOString()` |
|
||||
| Pass `new Map()` to client | No | Convert to object/array |
|
||||
| Pass class instance to client | No | Pass plain object |
|
||||
| Pass server action to client | Yes | - |
|
||||
| Pass `string/number/boolean` | Yes | - |
|
||||
| Pass plain object/array | Yes | - |
|
||||
|
||||
@@ -32,6 +32,7 @@ export const runtime = 'edge'
|
||||
## Detection
|
||||
|
||||
**Before adding `runtime = 'edge'`**, check:
|
||||
|
||||
1. Does the project already use Edge runtime?
|
||||
2. Is there a specific latency requirement?
|
||||
3. Are all dependencies Edge-compatible?
|
||||
|
||||
@@ -8,12 +8,12 @@ Always use `next/script` instead of native `<script>` tags for better performanc
|
||||
|
||||
```tsx
|
||||
// Bad: Native script tag
|
||||
<script src="https://example.com/script.js"></script>
|
||||
<script src="https://example.com/script.js"></script>;
|
||||
|
||||
// Good: Next.js Script component
|
||||
import Script from 'next/script'
|
||||
import Script from 'next/script';
|
||||
|
||||
<Script src="https://example.com/script.js" />
|
||||
<Script src="https://example.com/script.js" />;
|
||||
```
|
||||
|
||||
## Inline Scripts Need ID
|
||||
@@ -100,7 +100,7 @@ export default function Layout({ children }) {
|
||||
## Google Tag Manager
|
||||
|
||||
```tsx
|
||||
import { GoogleTagManager } from '@next/third-parties/google'
|
||||
import { GoogleTagManager } from '@next/third-parties/google';
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return (
|
||||
@@ -108,7 +108,7 @@ export default function Layout({ children }) {
|
||||
<GoogleTagManager gtmId="GTM-XXXXX" />
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -116,26 +116,22 @@ export default function Layout({ children }) {
|
||||
|
||||
```tsx
|
||||
// YouTube embed
|
||||
import { YouTubeEmbed } from '@next/third-parties/google'
|
||||
import { YouTubeEmbed } from '@next/third-parties/google';
|
||||
|
||||
<YouTubeEmbed videoid="dQw4w9WgXcQ" />
|
||||
<YouTubeEmbed videoid="dQw4w9WgXcQ" />;
|
||||
|
||||
// Google Maps
|
||||
import { GoogleMapsEmbed } from '@next/third-parties/google'
|
||||
import { GoogleMapsEmbed } from '@next/third-parties/google';
|
||||
|
||||
<GoogleMapsEmbed
|
||||
apiKey="YOUR_API_KEY"
|
||||
mode="place"
|
||||
q="Brooklyn+Bridge,New+York,NY"
|
||||
/>
|
||||
<GoogleMapsEmbed apiKey="YOUR_API_KEY" mode="place" q="Brooklyn+Bridge,New+York,NY" />;
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Issue | Fix |
|
||||
|---------|-------|-----|
|
||||
| `<script src="...">` | No optimization | Use `next/script` |
|
||||
| `<Script>` without id | Can't track inline scripts | Add `id` attribute |
|
||||
| `<Script>` inside `<Head>` | Wrong placement | Move outside Head |
|
||||
| Inline GA/GTM scripts | No optimization | Use `@next/third-parties` |
|
||||
| `strategy="beforeInteractive"` outside layout | Won't work | Only use in root layout |
|
||||
| Pattern | Issue | Fix |
|
||||
| --------------------------------------------- | -------------------------- | ------------------------- |
|
||||
| `<script src="...">` | No optimization | Use `next/script` |
|
||||
| `<Script>` without id | Can't track inline scripts | Add `id` attribute |
|
||||
| `<Script>` inside `<Head>` | Wrong placement | Move outside Head |
|
||||
| Inline GA/GTM scripts | No optimization | Use `@next/third-parties` |
|
||||
| `strategy="beforeInteractive"` outside layout | Won't work | Only use in root layout |
|
||||
|
||||
@@ -77,12 +77,12 @@ services:
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- '3000:3000'
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/api/health"]
|
||||
test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3000/api/health']
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
@@ -95,16 +95,18 @@ For traditional server deployments:
|
||||
```js
|
||||
// ecosystem.config.js
|
||||
module.exports = {
|
||||
apps: [{
|
||||
name: 'nextjs',
|
||||
script: '.next/standalone/server.js',
|
||||
instances: 'max',
|
||||
exec_mode: 'cluster',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: 3000,
|
||||
apps: [
|
||||
{
|
||||
name: 'nextjs',
|
||||
script: '.next/standalone/server.js',
|
||||
instances: 'max',
|
||||
exec_mode: 'cluster',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: 3000,
|
||||
},
|
||||
},
|
||||
}],
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
@@ -168,11 +170,7 @@ module.exports = class CacheHandler {
|
||||
|
||||
// Set TTL based on revalidate option
|
||||
if (ctx?.revalidate) {
|
||||
await redis.setex(
|
||||
CACHE_PREFIX + key,
|
||||
ctx.revalidate,
|
||||
JSON.stringify(cacheData)
|
||||
);
|
||||
await redis.setex(CACHE_PREFIX + key, ctx.revalidate, JSON.stringify(cacheData));
|
||||
} else {
|
||||
await redis.set(CACHE_PREFIX + key, JSON.stringify(cacheData));
|
||||
}
|
||||
@@ -197,10 +195,12 @@ const BUCKET = process.env.CACHE_BUCKET;
|
||||
module.exports = class CacheHandler {
|
||||
async get(key) {
|
||||
try {
|
||||
const response = await s3.send(new GetObjectCommand({
|
||||
Bucket: BUCKET,
|
||||
Key: `cache/${key}`,
|
||||
}));
|
||||
const response = await s3.send(
|
||||
new GetObjectCommand({
|
||||
Bucket: BUCKET,
|
||||
Key: `cache/${key}`,
|
||||
})
|
||||
);
|
||||
const body = await response.Body.transformToString();
|
||||
return JSON.parse(body);
|
||||
} catch (err) {
|
||||
@@ -210,32 +210,34 @@ module.exports = class CacheHandler {
|
||||
}
|
||||
|
||||
async set(key, data, ctx) {
|
||||
await s3.send(new PutObjectCommand({
|
||||
Bucket: BUCKET,
|
||||
Key: `cache/${key}`,
|
||||
Body: JSON.stringify({
|
||||
value: data,
|
||||
lastModified: Date.now(),
|
||||
}),
|
||||
ContentType: 'application/json',
|
||||
}));
|
||||
await s3.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: BUCKET,
|
||||
Key: `cache/${key}`,
|
||||
Body: JSON.stringify({
|
||||
value: data,
|
||||
lastModified: Date.now(),
|
||||
}),
|
||||
ContentType: 'application/json',
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## What Works vs What Needs Setup
|
||||
|
||||
| Feature | Single Instance | Multi-Instance | Notes |
|
||||
|---------|----------------|----------------|-------|
|
||||
| SSR | Yes | Yes | No special setup |
|
||||
| SSG | Yes | Yes | Built at deploy time |
|
||||
| ISR | Yes | Needs cache handler | Filesystem cache breaks |
|
||||
| Image Optimization | Yes | Yes | CPU-intensive, consider CDN |
|
||||
| Middleware | Yes | Yes | Runs on Node.js |
|
||||
| Edge Runtime | Limited | Limited | Some features Node-only |
|
||||
| `revalidatePath/Tag` | Yes | Needs cache handler | Must share cache |
|
||||
| `next/font` | Yes | Yes | Fonts bundled at build |
|
||||
| Draft Mode | Yes | Yes | Cookie-based |
|
||||
| Feature | Single Instance | Multi-Instance | Notes |
|
||||
| -------------------- | --------------- | ------------------- | --------------------------- |
|
||||
| SSR | Yes | Yes | No special setup |
|
||||
| SSG | Yes | Yes | Built at deploy time |
|
||||
| ISR | Yes | Needs cache handler | Filesystem cache breaks |
|
||||
| Image Optimization | Yes | Yes | CPU-intensive, consider CDN |
|
||||
| Middleware | Yes | Yes | Runs on Node.js |
|
||||
| Edge Runtime | Limited | Limited | Some features Node-only |
|
||||
| `revalidatePath/Tag` | Yes | Needs cache handler | Must share cache |
|
||||
| `next/font` | Yes | Yes | Fonts bundled at build |
|
||||
| Draft Mode | Yes | Yes | Cookie-based |
|
||||
|
||||
## Image Optimization
|
||||
|
||||
@@ -244,6 +246,7 @@ Next.js Image Optimization works out of the box but is CPU-intensive.
|
||||
### Option 1: Built-in (Simple)
|
||||
|
||||
Works automatically, but consider:
|
||||
|
||||
- Set `deviceSizes` and `imageSizes` in config to limit variants
|
||||
- Use `minimumCacheTTL` to reduce regeneration
|
||||
|
||||
@@ -317,6 +320,7 @@ npx @opennextjs/aws build
|
||||
```
|
||||
|
||||
Supports:
|
||||
|
||||
- AWS Lambda + CloudFront
|
||||
- Cloudflare Workers
|
||||
- Netlify Functions
|
||||
|
||||
@@ -8,27 +8,27 @@ Always requires Suspense boundary in static routes. Without it, the entire page
|
||||
|
||||
```tsx
|
||||
// Bad: Entire page becomes CSR
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
export default function SearchBar() {
|
||||
const searchParams = useSearchParams()
|
||||
return <div>Query: {searchParams.get('q')}</div>
|
||||
const searchParams = useSearchParams();
|
||||
return <div>Query: {searchParams.get('q')}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Good: Wrap in Suspense
|
||||
import { Suspense } from 'react'
|
||||
import SearchBar from './search-bar'
|
||||
import { Suspense } from 'react';
|
||||
import SearchBar from './search-bar';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<SearchBar />
|
||||
</Suspense>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -39,12 +39,12 @@ Requires Suspense boundary when route has dynamic parameters.
|
||||
```tsx
|
||||
// In dynamic route [slug]
|
||||
// Bad: No Suspense
|
||||
'use client'
|
||||
import { usePathname } from 'next/navigation'
|
||||
'use client';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
export function Breadcrumb() {
|
||||
const pathname = usePathname()
|
||||
return <nav>{pathname}</nav>
|
||||
const pathname = usePathname();
|
||||
return <nav>{pathname}</nav>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -59,9 +59,9 @@ If you use `generateStaticParams`, Suspense is optional.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Hook | Suspense Required |
|
||||
|------|-------------------|
|
||||
| `useSearchParams()` | Yes |
|
||||
| `usePathname()` | Yes (dynamic routes) |
|
||||
| `useParams()` | No |
|
||||
| `useRouter()` | No |
|
||||
| Hook | Suspense Required |
|
||||
| ------------------- | -------------------- |
|
||||
| `useSearchParams()` | Yes |
|
||||
| `usePathname()` | Yes (dynamic routes) |
|
||||
| `useParams()` | No |
|
||||
| `useRouter()` | No |
|
||||
|
||||
Reference in New Issue
Block a user