260322:1648 Correct Coresspondence / Doing RFA / Correct CI
This commit is contained in:
@@ -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,
|
||||
}))
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user