元数据与 OG 图片

Metadata API 帮你为每个页面定义 <title><meta><link>,进而优化 SEO 和社交分享。你有三条路径:

  1. 静态 metadata 对象
  2. 动态 generateMetadata 函数
  3. 基于文件的约定favicon.icoopengraph-image.tsxrobots.txtsitemap.xml 等)

metadatagenerateMetadata 只在 服务端组件 中生效。

默认标签

即便你不写任何元数据,Next.js 也会自动注入:

<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

静态元数据

layout.tsxpage.tsx 里导出一个 Metadata 对象:

import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: '我的博客',
  description: '关于前端与产品的思考',
};

export default function Layout() {
  /* … */
}

动态元数据

根据 params 拉取数据再生成标题描述:

import type { Metadata } from 'next';

type Props = {
  params: Promise<{ slug: string }>;
};

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const post = await fetch(`https://api.vercel.app/blog/${slug}`).then(r => r.json());

  return {
    title: post.title,
    description: post.description,
  };
}

export default function Page() {
  /* … */
}

避免重复请求

页面本身和 generateMetadata 常常要同一份数据。用 React 的 cache 包一层,保证只请求一次:

import { cache } from 'react';
import { db } from '@/app/lib/db';

export const getPost = cache(async (slug: string) => {
  return db.query.posts.findFirst({ where: eq(posts.slug, slug) });
});

流式元数据

对动态渲染的页面,Next.js 会把元数据 单独流式 注入 HTML,不阻塞视觉内容。Twitterbot、Slackbot 这类必须在 <head> 里拿到 meta 的爬虫会自动降级为「等全部生成完再返回」。

文件约定

这些特殊文件可以静态放置,也可以用代码动态生成:

  • favicon.icoicon.jpgapple-icon.jpg
  • opengraph-image.jpgtwitter-image.jpg
  • robots.txtsitemap.xml

静态 favicon / OG

favicon.ico 放在 app/ 根部,把 opengraph-image.jpg 放在需要定制的路由里。Next.js 会自动注入对应 <meta> 标签,且就近路由的 OG 优先级更高。

动态 OG 图片

ImageResponse 按 JSX + CSS 生成 1200×630 的 PNG,非常适合博客每篇文章一张独有封面:

// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og';
import { getPost } from '@/app/lib/data';

export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';

export default async function Image({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 128,
          background: 'white',
          width: '100%',
          height: '100%',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        {post.title}
      </div>
    )
  );
}

ImageResponse 底层使用 satori + resvg,只支持 flexbox 等子集,grid 不可用。把它当做「可程序化的截图器」而非完整浏览器。

元資料與 OG 圖片

Metadata API 協助你為每個頁面定義 <title><meta><link>,從而優化 SEO 與社交分享。有三條路徑:

  1. 靜態 metadata 物件
  2. 動態 generateMetadata 函式
  3. 檔案慣例favicon.icoopengraph-image.tsxrobots.txtsitemap.xml 等)

metadatagenerateMetadata 僅於 伺服器元件 中生效。

預設標籤

即便不寫任何元資料,Next.js 仍會自動注入:

<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

靜態元資料

layout.tsxpage.tsx 匯出 Metadata 物件:

import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: '我的網誌',
  description: '關於前端與產品的思考',
};

export default function Layout() {
  /* … */
}

動態元資料

依據 params 拉取資料再生成標題與描述:

import type { Metadata } from 'next';

type Props = {
  params: Promise<{ slug: string }>;
};

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const post = await fetch(`https://api.vercel.app/blog/${slug}`).then(r => r.json());

  return {
    title: post.title,
    description: post.description,
  };
}

export default function Page() {
  /* … */
}

避免重複請求

頁面與 generateMetadata 常需要相同資料。用 React 的 cache 包裹一層,確保只請求一次:

import { cache } from 'react';
import { db } from '@/app/lib/db';

export const getPost = cache(async (slug: string) => {
  return db.query.posts.findFirst({ where: eq(posts.slug, slug) });
});

串流元資料

對動態渲染的頁面,Next.js 會把元資料 單獨串流 注入 HTML,不阻塞視覺內容。Twitterbot、Slackbot 這類必須於 <head> 內取得 meta 的爬蟲會自動降級為「等全部生成完再返回」。

檔案慣例

這些特殊檔案可靜態放置,亦可以程式動態生成:

  • favicon.icoicon.jpgapple-icon.jpg
  • opengraph-image.jpgtwitter-image.jpg
  • robots.txtsitemap.xml

靜態 favicon / OG

favicon.ico 放在 app/ 根部,將 opengraph-image.jpg 放在需要客製的路由。Next.js 會自動注入對應 <meta>,且就近路由的 OG 優先級較高。

動態 OG 圖片

ImageResponse 按 JSX + CSS 生成 1200×630 的 PNG,非常適合網誌每篇文章獨有封面:

// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og';
import { getPost } from '@/app/lib/data';

export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';

export default async function Image({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 128,
          background: 'white',
          width: '100%',
          height: '100%',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        {post.title}
      </div>
    )
  );
}

ImageResponse 底層使用 satori + resvg,僅支援 flexbox 等子集,grid 無法使用。請將其視為「可程序化的截圖器」,而非完整瀏覽器。

Metadata and OG Images

The Metadata API lets you define <title>, <meta>, and <link> tags per page for SEO and social sharing. Three paths:

  1. A static metadata object
  2. A dynamic generateMetadata function
  3. File conventions (favicon.ico, opengraph-image.tsx, robots.txt, sitemap.xml, …)

metadata and generateMetadata are supported only in Server Components.

Default tags

Even without custom metadata, Next.js injects the essentials:

<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

Static metadata

Export a Metadata object from layout.tsx or page.tsx:

import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'My Blog',
  description: 'Notes on front-end and product.',
};

export default function Layout() {
  /* … */
}

Dynamic metadata

Fetch data based on params and generate title/description:

import type { Metadata } from 'next';

type Props = {
  params: Promise<{ slug: string }>;
};

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const post = await fetch(`https://api.vercel.app/blog/${slug}`).then(r => r.json());

  return {
    title: post.title,
    description: post.description,
  };
}

export default function Page() {
  /* … */
}

Avoiding duplicate requests

The page itself and generateMetadata often need the same data. Wrap the fetcher with React's cache so it runs once:

import { cache } from 'react';
import { db } from '@/app/lib/db';

export const getPost = cache(async (slug: string) => {
  return db.query.posts.findFirst({ where: eq(posts.slug, slug) });
});

Streaming metadata

For dynamically rendered pages Next.js injects metadata as a separate stream, so the UI isn't blocked. Crawlers like Twitterbot or Slackbot that need metadata in the initial <head> are detected and served a blocking response instead.

File conventions

These special files can be static assets or generated in code:

  • favicon.ico, icon.jpg, apple-icon.jpg
  • opengraph-image.jpg, twitter-image.jpg
  • robots.txt, sitemap.xml

Static favicon / OG

Place favicon.ico at the root of app/, and opengraph-image.jpg wherever you want a route-specific image. The closer-to-the-leaf image wins.

Dynamic OG images

Use ImageResponse to generate 1200×630 PNGs with JSX + CSS — perfect for per-post covers:

// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og';
import { getPost } from '@/app/lib/data';

export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';

export default async function Image({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 128,
          background: 'white',
          width: '100%',
          height: '100%',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        {post.title}
      </div>
    )
  );
}

Under the hood ImageResponse uses satori + resvg. Only flexbox and a subset of CSS are supported (no grid). Think of it as a programmable screenshot tool, not a full browser.