metadata 基础

每个 page / layout 可以 export metadata

import type { Metadata } from 'next';

export const metadata: Metadata = {
    title: 'About · WadeLy',
    description: 'About WadeLy, a developer who codes for fun.',
};

Next.js 自动生成:

<title>About · WadeLy</title>
<meta name="description" content="About WadeLy...">

根 layout 设全局默认

// src/app/layout.tsx
export const metadata: Metadata = {
    title: {
        default: 'WadeLy',
        template: '%s · WadeLy',       // 子页用 %s 占位
    },
    description: 'WadeLy 的个人主页和笔记。',
    metadataBase: new URL('https://51testgame.com'),
    icons: {
        icon: '/favicon.ico',
        apple: '/apple-icon.png',
    },
};

之后每个页面只写自己的 title,会自动套 template:

// /about/page.tsx
export const metadata = { title: 'About' };
// 渲染成 <title>About · WadeLy</title>

OpenGraph + Twitter Card(社交分享)

export const metadata: Metadata = {
    title: 'My Awesome Post',
    description: 'A post about React.',

    openGraph: {
        title: 'My Awesome Post',
        description: 'A post about React.',
        url: 'https://51testgame.com/blog/post',
        siteName: 'WadeLy',
        images: [{ url: '/og-image.png', width: 1200, height: 630 }],
        locale: 'zh_CN',
        type: 'article',
    },

    twitter: {
        card: 'summary_large_image',
        title: 'My Awesome Post',
        description: 'A post about React.',
        images: ['/og-image.png'],
    },
};

发到微信 / Twitter / 即刻自动显示卡片预览。

动态 metadata(基于 params)

import type { Metadata } from 'next';

// Next.js 15+:params 是 Promise
export async function generateMetadata({
    params,
}: {
    params: Promise<{ slug: string }>;
}): Promise<Metadata> {
    const { slug } = await params;
    const post = await getPost(slug);
    return {
        title: post.title,
        description: post.excerpt,
        openGraph: {
            title: post.title,
            description: post.excerpt,
            images: [post.coverImage],
            type: 'article',
            publishedTime: post.date,
            authors: [post.author],
        },
    };
}

export default async function BlogPost({ params }) { ... }

自动生成 OG 图片(Next.js 内置)

src/app/blog/[slug]/opengraph-image.tsx:

import { ImageResponse } from 'next/og';

export const size = { width: 1200, height: 630 };

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

每篇文章自动有定制 OG 图——分享时显示标题。

sitemap.xml

// src/app/sitemap.ts
import type { MetadataRoute } from 'next';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
    const posts = await getAllPosts();

    return [
        { url: 'https://51testgame.com', lastModified: new Date(), priority: 1 },
        { url: 'https://51testgame.com/about', lastModified: new Date() },
        ...posts.map(p => ({
            url: `https://51testgame.com/blog/${p.slug}`,
            lastModified: p.updatedAt,
            priority: 0.7,
        })),
    ];
}

Next.js 自动生成 /sitemap.xml

robots.txt

// src/app/robots.ts
import type { MetadataRoute } from 'next';

export default function robots(): MetadataRoute.Robots {
    return {
        rules: {
            userAgent: '*',
            allow: '/',
            disallow: '/admin/',
        },
        sitemap: 'https://51testgame.com/sitemap.xml',
    };
}

自动生成 /robots.txt

RSS feed

Next.js 没内置——用 Server Component + Response:

// src/app/rss.xml/route.ts
export async function GET() {
    const posts = await getAllPosts();
    const xml = `<?xml version="1.0"?>
<rss version="2.0">
<channel>
    <title>WadeLy</title>
    <link>https://51testgame.com</link>
    ${posts.map(p => `
        <item>
            <title>${p.title}</title>
            <link>https://51testgame.com/blog/${p.slug}</link>
            <pubDate>${new Date(p.date).toUTCString()}</pubDate>
            <description>${p.excerpt}</description>
        </item>
    `).join('')}
</channel>
</rss>`;

    return new Response(xml, {
        headers: { 'Content-Type': 'application/xml' },
    });
}

访问 /rss.xml 拿到 feed。

JSON-LD 结构化数据

export default function BlogPost({ post }) {
    const jsonLd = {
        '@context': 'https://schema.org',
        '@type': 'BlogPosting',
        headline: post.title,
        author: { '@type': 'Person', name: 'WadeLy' },
        datePublished: post.date,
        image: post.coverImage,
    };

    return (
        <>
            <script
                type="application/ld+json"
                dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
            />
            <article>...</article>
        </>
    );
}

帮助 Google 理解结构 → 在搜索结果里显示更丰富的卡片。

验证

部署后到这些工具检查:

  • metadataBase 必须设——OG 图相对 URL 没基址会变 localhost
  • 每页独立 metadata(不要全站一样的 title)—— SEO 损失大
  • 中文站 locale: 'zh_CN',不是 'zh-CN'(OpenGraph 标准)
  • 改了 metadata 要刷新 deploy / 重新生成 SSG 才生效

下一篇:部署到 Vercel。