前言
在当今的社交媒体时代,链接预览图片已成为提升内容分享体验和点击率的关键因素。一个精心设计的预览图不仅能够吸引用户注意,还能传递更多的内容信息。本文将详细介绍如何使用 Next.js 14 实现专业的社交媒体预览图片生成功能。
基础概念
OpenGraph 协议
OpenGraph 协议(简称 OGP)是由 Facebook 推出的一项开放标准。它的主要目的是让网页内容在社交媒体上以更丰富的形式展示。
想象一下,当你在微信或 Discord 中分享一个链接时,不再只是显示一个简单的 URL,而是能看到一个精美的预览卡片。这个卡片包含了标题、描述、图片等信息。这就是 OpenGraph 协议的作用。
工作原理: 通过在网页的 <head>
标签中添加特定的元数据标签,告诉社交平台如何展示这个页面。就像是给网页添加一个"社交名片"。
协议介绍: The Open Graph protocol
关键元数据:
og:title
: 对象标题。og:type
: 对象类型 (website
,article
,book
等)。og:image
: 对象图像 URL (影响视觉效果)。og:url
: 对象规范 URL (唯一 ID)。og:description
: 对象简述。og:locale
: 语言地区 (如zh_CN
)。og:site_name
: 网站名。
og:image
重要属性:
og:image:url
: 同og:image
。og:image:type
: MIME 类型 (如image/jpeg
)。og:image:width
: 宽度 (像素)。og:image:height
: 高度 (像素)。og:image:alt
: 图像描述 (无障碍)。
示例:
<meta property="og:title" content="Gemini 2.0 现已向所有人开放 | BestBlogs.dev" />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://www.bestblogs.dev/article/c377bf" />
<meta property="og:image" content="https://www.bestblogs.dev/article/c377bf/opengraph-image" />
<meta property="og:description" content="谷歌深思宣布 Gemini 2.0 Flash 正式发布,并同时推出实验版本的 Gemini 2.0 Pro 和 Flash-Lite,具备增强的性能、多模态能力和强大的安全措施。" />
<meta property="og:site_name" content="BestBlogs.dev" />
<meta property="og:locale" content="en_US" />
<meta property="og:image:url" content="https://www.bestblogs.dev/article/c377bf/opengraph-image" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="Gemini 2.0 现已向所有人开放 | BestBlogs.dev" />
X Cards (Twitter Cards)
X Cards (原 Twitter Cards) 是 X (Twitter) 平台的类似机制,用元标签控制推文中链接的预览效果。X Cards 将文本链接转为带图片、视频的预览,提升吸引力和点击率。
核心思想: 类似 OpenGraph,用元标签描述网页。
协议介绍: X Cards
关键元数据:
twitter:card
(必需): 卡片类型:summary
: 标题、描述、小图。summary_large_image
: 大图、标题、描述。app
: 应用下载。player
: 嵌入视频/音频。
twitter:site
: 网站 X 账号。twitter:creator
: 作者 X 账号。twitter:title
: 标题 (可回退到og:title
)。twitter:description
: 描述 (可回退到og:description
)。twitter:image
: 图片 URL (可回退到og:image
)。
示例:
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@hongming731" />
<meta name="twitter:title" content="Gemini 2.0 现已向所有人开放 | BestBlogs.dev" />
<meta name="twitter:description" content="谷歌深思宣布 Gemini 2.0 Flash 正式发布,并同时推出实验版本的 Gemini 2.0 Pro 和 Flash-Lite,具备增强的性能、多模态能力和强大的安全措施。" />
<meta name="twitter:image" content="https://www.bestblogs.dev/article/c377bf/twitter-image" />
<meta name="twitter:creator" content="@hongming731" />
<meta name="twitter:image:type" content="image/png" />
<meta name="twitter:image:width" content="1200" />
<meta name="twitter:image:height" content="675" />
X Cards 和 OpenGraph 兼容: X Cards 优先使用 twitter:
前缀属性,找不到则回退到 Open Graph 属性。详细介绍可参考:Cards Markup Tag Reference
技术方案
Next.js 14 为我们提供了三种实现社交预览图的方式。每种方式都有其适用场景,让我们一一了解:
- 静态元数据: 适用于网站固定页面,如首页、关于页等
- 动态元数据: 适用于博客文章、产品详情等动态内容
- 动态图片生成: 适用于需要完全自定义预览图的场景
让我们详细探讨每种方式的实现方法。
静态元数据
静态元数据可以在 layout.tsx
或 page.tsx
中通过导出 metadata
对象来定义,具体的定义规范可以参考官方的 文档,以下是一个完整的示例:
// app/[lang]/layout.tsx
export const metadata = {
metadataBase: new URL("https://www.bestblogs.dev"),
title: 'bestblogs.dev - 汇集顶级编程、人工智能、产品、科技文章,大语言模型摘要评分辅助阅读,探索编程和技术未来',
description: 'bestblogs.dev 为您提供独特的编程、人工智能、产品设计、商业科技和个人成长领域的价值导向内容,汇集自顶级技术公司和社区。我们利用先进语言模型,为您摘要、评分和翻译这些文章,节省您的阅读时间。我们了解数据筛选的痛点,致力于为您呈现精选内容。立即订阅,探索未来技术的无限可能!',
keywords: 'bestblogs, 编程, 软件, 开发, 技术, 阅读, 技术博客, 技术社区, 人工智能, 产品管理, 网页设计, 商业, 科技, 个人成长, 优质文章, 大语言模型, AI 阅读',
applicationName: "BestBlogs.dev",
openGraph: {
title: 'bestblogs.dev - 汇集顶级编程、人工智能、产品、科技文章,大语言模型摘要评分辅助阅读,探索编程和技术未来',
description: 'bestblogs.dev 为您提供独特的编程、人工智能、产品设计、商业科技和个人成长领域的价值导向内容,汇集自顶级技术公司和社区。我们利用先进语言模型,为您摘要、评分和翻译这些文章,节省您的阅读时间。我们了解数据筛选的痛点,致力于为您呈现精选内容。立即订阅,探索未来技术的无限可能!',
url: 'https://www.bestblogs.dev',
siteName: 'BestBlogs.dev',
images: [
{
url: 'https://www.bestblogs.dev/og.png',
width: 1200,
height: 630
}
],
locale: 'zh_CN',
type: 'website'
},
twitter: {
card: 'summary_large_image',
title: 'bestblogs.dev - 汇集顶级编程、人工智能、产品、科技文章,大语言模型摘要评分辅助阅读,探索编程和技术未来',
description: 'bestblogs.dev 为您提供独特的编程、人工智能、产品设计、商业科技和个人成长领域的价值导向内容,汇集自顶级技术公司和社区。我们利用先进语言模型,为您摘要、评分和翻译这些文章,节省您的阅读时间。我们了解数据筛选的痛点,致力于为您呈现精选内容。立即订阅,探索未来技术的无限可能!',
site: '@hongming731',
creator: '@hongming731',
images: [{
url: 'https://www.bestblogs.dev/og.png',
width: 1200,
height: 675,
alt: "BestBlogs.dev"
}]
},
// 其他元数据
}
需要注意的是:
- 元数据对象支持模板字符串,可以使用
${...}
语法动态生成值 - 如果在
layout.tsx
中定义,会作为默认值被所有子页面继承 - 子页面可以通过定义自己的
metadata
对象来覆盖父级配置
BestBlogs.dev 首页及文章列表等页面使用的是静态元数据,OpenGraph 和 Twitter Card 的图片使用的是 Open Graph Image Generator 生成的。这个网站提供了一个模板工具,输入网站信息和截图可以快速生成 OpenGraph 和 Twitter Card 的图片。
PS:如果你喜欢写代码来生成静态图片,建议使用 Vercel Open Graph Play Ground 这个在线工具,它还支持使用 Tailwind 来编写样式,非常方便。
动态元数据
动态元数据在 generateMetadata
函数 中生成,可以基于页面内容动态生成元数据,此时可以指定特定文章的 OpenGraph 或者 Twitter Card 的图片,之前的版本 BestBlogs.dev 使用的是文章的封面地址。以下是一个示例:
// app/[lang]/article/[id]/layout.tsx
export async function generateMetadata({ params }) {
const { id } = params;
const language = params.lang;
const resourceMeta = await getDirect(`/api/resource/meta`, { id, language }) as QualifiedResource;
if (!resourceMeta) {
return {};
}
const showTitle = `${resourceMeta.title} | BestBlogs`;
let url;
if (language !== 'zh') {
url = `https://www.bestblogs.dev/${language}/article/${id}`;
} else {
url = `https://www.bestblogs.dev/article/${id}`;
}
const image = resourceMeta.cover || 'https://www.bestblogs.dev/og.png';
const summary = resourceMeta.summary || "在 BestBlogs 上阅读最新的深度分析文章,提供独到见解和详尽信息。";
return {
title: showTitle,
description: summary,
image: image,
keywords: resourceMeta.tags ? resourceMeta.tags.join(', ') : '深度分析, 最新研究, 详尽信息',
openGraph: {
title: showTitle,
description: summary,
url: url,
siteName: 'BestBlogs.dev',
publishedTime: resourceMeta.publishDateTimeStr,
authors: resourceMeta.authors,
tags: resourceMeta.tags,
images: [
{
url: image,
width: 1200,
height: 630,
alt: showTitle
}
],
locale: language === 'zh' ? 'zh_CN' : 'en_US',
type: 'article'
},
twitter: {
card: 'summary_large_image',
title: showTitle,
description: summary,
site: '@hongming731',
images: [{
url: image,
width: 1200,
height: 675,
alt: showTitle
}]
},
alternates: {
canonical: url
}
}
}
动态图片生成
动态图片生成在 opengraph-image.tsx
和 twitter-image.tsx
中实现,可以基于页面内容动态生成图片,可参考 官方文档。以下是 BestBlogs.dev 网站文章详情页目前使用的 X Card 动态图片代码,包括了文章的标题、来源、作者、摘要、阅读时间、字数、分类、标签等一系列信息。
// app/[lang]/articles/[id]/twitter-image.tsx
/* eslint-disable @next/next/no-img-element */
import { ImageResponse } from 'next/og'
import { getDirect } from '@/lib/api'
import { QualifiedResource } from '@/types/QualifiedResource'
import { SHARED_STYLES, TWITTER_CONFIG } from '@/lib/social-image'
export const runtime = 'edge'
export const size = TWITTER_CONFIG.size
export const contentType = 'image/png'
export const revalidate = 3600
export default async function TwitterImage({
params,
}: {
params: { id: string; lang: string }
}) {
const resourceMeta: QualifiedResource | null = await getDirect(
'/api/resource/meta',
{ id: params.id, language: params.lang }
)
const { title, oneSentenceSummary, summary, sourceName, authors, tags, readTime, wordCount, mainDomainDesc } = resourceMeta
const displayTitle = title ?? 'BestBlogs.dev'
const displaySummary = oneSentenceSummary ?? summary?.substring(0, 80) ?? '阅读最新文章,获取深度分析与详尽信息。'
const displayTags = tags?.slice(0, 3) ?? ['技术博客']
const displayReadingTime = readTime ?? 12
const displayWordCount = wordCount?.toLocaleString('zh-CN') ?? '3,500'
const displayCategory = mainDomainDesc ?? '人工智能'
return new ImageResponse(
(
<div
style={{
display: 'flex',
width: '100%',
height: '100%',
background: `
linear-gradient(135deg, #020617 0%, #0F172A 100%),
radial-gradient(circle at 95% 5%, rgba(56, 189, 248, 0.2) 0%, transparent 50%),
radial-gradient(circle at 5% 95%, rgba(56, 189, 248, 0.15) 0%, transparent 50%),
linear-gradient(90deg, transparent 0%, rgba(56, 189, 248, 0.03) 50%, transparent 100%)
`,
position: 'relative',
overflow: 'hidden',
}}
>
{/* 右侧信息面板 */}
<div style={{
position: 'absolute',
top: '10%',
right: '4%',
width: '220px',
padding: '24px',
background: 'rgba(15, 23, 42, 0.85)',
borderRadius: '16px',
border: '1px solid rgba(56, 189, 248, 0.3)',
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(56, 189, 248, 0.1)',
backdropFilter: 'blur(16px)',
zIndex: 1,
display: 'flex',
flexDirection: 'column',
gap: '24px',
}}>
{/* 文章分类和标签 */}
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '12px',
}}>
<div style={{
fontSize: '13px',
color: '#38BDF8',
letterSpacing: '0.05em',
fontWeight: 600,
}}>文章分类</div>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
color: 'rgba(255, 255, 255, 0.9)',
fontSize: '14px',
marginBottom: '8px',
}}>
<div style={{
width: '4px',
height: '4px',
background: '#38BDF8',
borderRadius: '50%',
}} />
{displayCategory}
</div>
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '8px',
}}>
{displayTags.map((tag, index) => (
<div key={index} style={{
display: 'flex',
padding: '6px 12px',
background: 'rgba(56, 189, 248, 0.15)',
borderRadius: '8px',
fontSize: '13px',
color: '#7DD3FC',
border: '1px solid rgba(56, 189, 248, 0.2)',
}}>{tag}</div>
))}
</div>
</div>
{/* 阅读信息 */}
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '12px',
}}>
<div style={{
fontSize: '13px',
color: '#38BDF8',
letterSpacing: '0.05em',
fontWeight: 600,
}}>阅读信息</div>
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '8px',
}}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
color: 'rgba(255, 255, 255, 0.9)',
fontSize: '14px',
}}>
<div style={{
width: '4px',
height: '4px',
background: '#38BDF8',
borderRadius: '50%',
}} />
全文约 {displayWordCount} 字
</div>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
color: 'rgba(255, 255, 255, 0.9)',
fontSize: '14px',
}}>
<div style={{
width: '4px',
height: '4px',
background: '#38BDF8',
borderRadius: '50%',
}} />
预计阅读 {displayReadingTime} 分钟
</div>
</div>
</div>
</div>
{/* 几何装饰 */}
<svg
style={{
position: 'absolute',
top: '0',
right: '0',
width: '70%',
height: '100%',
opacity: 0.15,
transform: 'translateX(10%)',
}}
>
<path
d="M 50,0 L 100,0 L 100,100 L 45,100 Z"
fill="url(#tech-gradient)"
/>
<defs>
<linearGradient id="tech-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style={{ stopColor: '#38BDF8', stopOpacity: 0.9 }} />
<stop offset="100%" style={{ stopColor: '#0EA5E9', stopOpacity: 0.3 }} />
</linearGradient>
</defs>
</svg>
{/* 装饰性线条组 */}
<div style={{
position: 'absolute',
top: 0,
right: '45%',
width: '1px',
height: '100%',
background: 'linear-gradient(180deg, transparent, rgba(56, 189, 248, 0.3), transparent)',
}} />
<div style={{
position: 'absolute',
top: 0,
right: '46%',
width: '1px',
height: '100%',
background: 'linear-gradient(180deg, transparent, rgba(56, 189, 248, 0.15), transparent)',
}} />
{/* 光点效果组 */}
<div style={{
position: 'absolute',
top: '15%',
right: '25%',
width: '12px',
height: '12px',
background: '#38BDF8',
borderRadius: '50%',
boxShadow: '0 0 60px 20px rgba(56, 189, 248, 0.4)',
opacity: 0.8,
}} />
<div style={{
position: 'absolute',
top: '55%',
right: '48%',
width: '8px',
height: '8px',
background: '#7DD3FC',
borderRadius: '50%',
boxShadow: '0 0 50px 15px rgba(125, 211, 252, 0.3)',
opacity: 0.6,
}} />
<div style={{
position: 'absolute',
top: '75%',
right: '15%',
width: '6px',
height: '6px',
background: '#BAE6FD',
borderRadius: '50%',
boxShadow: '0 0 40px 12px rgba(186, 230, 253, 0.25)',
opacity: 0.5,
}} />
{/* 主要内容区域 */}
<div
style={{
display: 'flex',
flexDirection: 'column',
width: '100%',
height: '100%',
padding: '48px 56px 80px',
justifyContent: 'flex-start',
position: 'relative',
zIndex: 2,
}}
>
{/* 上半部分 */}
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '24px',
maxWidth: '72%',
marginBottom: 'auto',
}}>
{/* 来源信息 */}
<div style={{
display: 'flex',
gap: '16px',
alignItems: 'center',
marginBottom: '4px',
}}>
<div style={{
padding: '8px 16px',
background: 'rgba(56, 189, 248, 0.15)',
borderRadius: '10px',
fontSize: '15px',
color: '#7DD3FC',
letterSpacing: '0.02em',
border: '1px solid rgba(56, 189, 248, 0.3)',
backdropFilter: 'blur(8px)',
fontWeight: 500,
}}>
{sourceName}
</div>
{authors?.length ? (
<div style={{
fontSize: '15px',
color: 'rgba(255, 255, 255, 0.85)',
letterSpacing: '0.02em',
background: 'rgba(255, 255, 255, 0.08)',
padding: '8px 16px',
borderRadius: '10px',
backdropFilter: 'blur(8px)',
border: '1px solid rgba(255, 255, 255, 0.1)',
fontWeight: 500,
}}>
{authors.slice(0, 2).join(', ')}
</div>
) : null}
</div>
{/* 标题 */}
<h1
style={{
margin: 0,
fontSize: '60px',
fontWeight: 700,
lineHeight: 1.15,
letterSpacing: '-0.02em',
color: '#FFFFFF',
textShadow: '0 2px 10px rgba(0, 0, 0, 0.2)',
}}
>
{displayTitle}
</h1>
{/* 摘要 */}
<p
style={{
margin: 0,
fontSize: '22px',
lineHeight: 1.6,
color: 'rgba(255, 255, 255, 0.9)',
letterSpacing: '0.01em',
textShadow: '0 1px 2px rgba(0, 0, 0, 0.1)',
fontWeight: 400,
}}
>
{displaySummary}
</p>
</div>
{/* Logo 区域 */}
<div style={{
position: 'absolute',
bottom: '80px',
left: '56px',
right: '56px',
display: 'flex',
alignItems: 'center',
padding: '20px 24px',
background: 'rgba(255, 255, 255, 0.03)',
borderRadius: '16px',
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.2)',
backdropFilter: 'blur(16px)',
border: '1px solid rgba(56, 189, 248, 0.2)',
}}>
<img
src="https://bestblogs.dev/logo.png"
width={52}
height={52}
alt=""
style={{
borderRadius: '10px',
marginRight: '20px',
}}
/>
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '4px',
}}>
<div style={{
fontSize: '24px',
fontWeight: 600,
color: '#FFFFFF',
letterSpacing: '-0.01em',
}}>
BestBlogs.dev
</div>
<div style={{
fontSize: '15px',
color: 'rgba(255, 255, 255, 0.8)',
letterSpacing: '0.02em',
fontWeight: 400,
}}>
遇见更好的技术阅读
</div>
</div>
</div>
</div>
</div>
),
{
...size,
}
)
}
测试效果:
测试和调试
- 直接访问
输入文章的 URL 及后缀 /opengraph-image
或 /twitter-image
查看效果,例如:
http://localhost:3000/article/123/opengraph-image
http://localhost:3000/article/123/twitter-image
- OpenGraph 调试工具
使用 OpenGraph Dev 查看在各个社交平台的效果。
- Vercel Open Graph 预览
如果使用 Vercel 部署,可以通过 Open Graph 部署选项卡检查和验证 Open Graph 元数据。
- TDK 预览
使用 TDK 插件 预览 SEO 和社交媒体的展示信息。
next/og 介绍
在了解了基本的实现方案后,我们来深入探讨 Next.js 提供的动态图片生成工具 —— next/og
。这个工具基于 Vercel 的 @vercel/og
库实现,用于在 Vercel Functions 中计算和生成社交卡片图片。
核心优势
-
高性能
- 代码量小,函数启动几乎瞬时
- 生成过程快速,能被 Open Graph Debugger 等工具识别
- 支持 Edge Runtime,全球分发
-
易用性
- 使用熟悉的 HTML 和 CSS 语法定义图片
- 支持动态生成图片
-
成本效益
- 自动添加正确的缓存头
- 在边缘缓存计算结果
- 减少重复计算
使用说明
-
运行时支持
// Next.js App Router(推荐) export const runtime = 'edge' // 使用 Edge Runtime // 加载本地资源 import { readFile } from 'fs/promises' const imageData = await readFile('./public/logo.png') // 加载远程资源 const response = await fetch('https://example.com/image.jpg') const imageData = await response.arrayBuffer()
说明:支持 Node.js 运行时和 Edge 运行时。本地资源可以直接使用
fs.readFile
加载,远程资源可以使用fetch
加载。 -
字体处理
export default function OGImage() { return new ImageResponse( <div style={{ fontFamily: 'NotoSansSC' }}> 你好,世界! </div>, { width: 1200, height: 630, fonts: [ { name: 'NotoSansSC', data: await fetch( new URL('./NotoSansSC-Regular.otf', import.meta.url) ).then((res) => res.arrayBuffer()), weight: 400, style: 'normal', }, ], } ) }
说明:支持自定义字体,建议使用 ttf 或 otf 格式以获得最佳的解析速度。
-
性能优化
// 1. 缓存配置 export const revalidate = 3600 // 缓存一小时 // 2. 错误处理 try { return new ImageResponse(...) } catch (e) { console.error('OG Image Generation Error:', e) return new Response( await readFile('./public/default-og.png'), { headers: { 'content-type': 'image/png' } } ) }
技术限制
-
布局限制
- 仅支持 Flexbox 布局(
display: flex
)和基础 CSS 属性 - 支持绝对定位(
position: absolute
) - 不支持 Grid 布局和一些高级 CSS 特性
- 支持文本换行、居中和嵌套图片
- 具体支持的 CSS 属性请参考 Satori 文档
- 仅支持 Flexbox 布局(
-
资源限制
- bundle 大小限制为 500KB
- 包括 JSX、CSS、字体、图片等所有资源
- 超出限制时建议:
- 压缩资源
- 运行时加载资源
- 优化字体子集
-
字体支持
- 支持 ttf、otf 和 woff 格式
- 推荐使用 ttf 或 otf 格式以获得最佳性能
- 支持从 Google Fonts 下载字体子集
-
运行时要求
- 需要 Node.js 22 或更高版本
- Next.js 需要 v12.2.3 或更高版本
- App Router 项目已内置
@vercel/og
- 建议在
robots.txt
中允许爬虫访问 OG 图片生成路由
小结
本文深入探讨了使用 Next.js 14 实现社交媒体预览图片生成功能的完整方案。让我们回顾关键内容:
-
基础概念
- OpenGraph 协议的工作原理和关键元数据
- X Cards 的使用方法和与 OpenGraph 的兼容性
- 不同平台的图片规格和展示要求
-
实现方案
- 静态元数据:适用于固定内容页面
- 动态元数据:满足动态内容需求
- 动态图片生成:实现完全自定义的预览图
-
next/og 技术要点
- 高性能的 Edge Runtime 环境
- 灵活的字体和样式处理
- 完善的缓存和错误处理
- 布局和资源使用的限制
通过本文的学习,你应该已经掌握了:
- 如何选择合适的预览图生成方案
- 如何实现高性能的动态图片生成
- 如何处理常见的技术难点
- 如何优化生成图片的性能
希望这些内容能帮助你在项目中实现更好的社交媒体预览效果。如果你有任何问题或建议,欢迎评论交流。