如何在NextJS中获取并渲染Contentful富文本内容?
调整GraphQL Query获取富文本内容
首先修改你的GraphQL查询,明确获取富文本字段content的完整结构,包含节点数据和关联的资源信息:
const query = ` { blogCollection { items{ title slug cover{ url title } content { json links { assets { block { sys { id } url title description } } entries { block { sys { id } # 若嵌入条目有特定字段,可在此补充,比如title、slug等 } } } } } } } `
其中content.json是富文本的结构化核心数据,links部分用来获取关联的资产(如图片)和嵌入条目,确保渲染时能拿到对应资源的完整信息。
在Next.js中渲染富文本内容
Contentful提供官方工具包处理富文本渲染,步骤如下:
1. 安装依赖
在项目中安装富文本渲染工具:
npm install @contentful/rich-text-react-renderer @contentful/rich-text-types # 或 yarn add @contentful/rich-text-react-renderer @contentful/rich-text-types
2. 创建富文本渲染组件
自定义渲染逻辑,处理不同类型的富文本节点(标题、列表、图片等):
import { documentToReactComponents } from '@contentful/rich-text-react-renderer'; import { BLOCKS, MARKS, INLINES } from '@contentful/rich-text-types'; export default function BlogContent({ content }) { const { json, links } = content; // 构建资产映射,快速查找关联图片 const assetMap = new Map(); links?.assets?.block?.forEach(asset => { assetMap.set(asset.sys.id, asset); }); // 自定义渲染配置 const renderOptions = { // 处理文本标记(加粗、斜体等) renderMark: { [MARKS.BOLD]: text => <strong>{text}</strong>, [MARKS.ITALIC]: text => <em>{text}</em>, [MARKS.UNDERLINE]: text => <u>{text}</u>, [MARKS.CODE]: text => <code className="bg-gray-100 px-1 py-0.5 rounded">{text}</code>, }, // 处理块级节点 renderNode: { // 标题系列 [BLOCKS.HEADING_1]: (node, children) => <h1 className="text-4xl font-bold mb-4">{children}</h1>, [BLOCKS.HEADING_2]: (node, children) => <h2 className="text-3xl font-bold mb-3">{children}</h2>, [BLOCKS.HEADING_3]: (node, children) => <h3 className="text-2xl font-bold mb-2">{children}</h3>, [BLOCKS.HEADING_4]: (node, children) => <h4 className="text-xl font-bold mb-2">{children}</h4>, [BLOCKS.HEADING_5]: (node, children) => <h5 className="text-lg font-bold mb-2">{children}</h5>, [BLOCKS.HEADING_6]: (node, children) => <h6 className="text-base font-bold mb-2">{children}</h6>, // 段落 [BLOCKS.PARAGRAPH]: (node, children) => <p className="mb-4">{children}</p>, // 列表 [BLOCKS.UNORDERED_LIST]: (node, children) => <ul className="list-disc pl-6 mb-4">{children}</ul>, [BLOCKS.ORDERED_LIST]: (node, children) => <ol className="list-decimal pl-6 mb-4">{children}</ol>, [BLOCKS.LIST_ITEM]: (node, children) => <li className="mb-2">{children}</li>, // 引用 [BLOCKS.BLOCKQUOTE]: (node, children) => <blockquote className="border-l-4 border-gray-300 pl-4 italic mb-4">{children}</blockquote>, // 水平线 [BLOCKS.HR]: () => <hr className="border-gray-200 my-6" />, // 代码块 [BLOCKS.CODE]: (node, children) => <pre className="bg-gray-100 p-4 rounded mb-4 overflow-x-auto"><code>{children}</code></pre>, // 嵌入图片资产 [BLOCKS.EMBEDDED_ASSET]: (node) => { const asset = assetMap.get(node.data.target.sys.id); if (!asset) return null; return ( <figure className="mb-6"> <img src={asset.url} alt={asset.title || asset.description || '博客图片'} className="w-full h-auto rounded" /> {asset.title && <figcaption className="text-sm text-gray-600 mt-2 text-center">{asset.title}</figcaption>} </figure> ); }, // 外部链接 [INLINES.HYPERLINK]: (node, children) => { return <a href={node.data.uri} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">{children}</a>; }, // 资产链接 [INLINES.ASSET_HYPERLINK]: (node, children) => { const asset = assetMap.get(node.data.target.sys.id); if (!asset) return null; return <a href={asset.url} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">{children}</a>; }, // 若有嵌入条目,可在此添加处理逻辑 // [BLOCKS.EMBEDDED_ENTRY]: (node) => { // const entry = entryMap.get(node.data.target.sys.id); // return <div className="embedded-entry">{/* 渲染条目内容 */}</div>; // } }, }; return documentToReactComponents(json, renderOptions); }
3. 在博客页面中使用组件
在博客详情页引入渲染组件,传入获取到的富文本数据:
// 博客详情页示例 import BlogContent from '../components/BlogContent'; export default function BlogPost({ blog }) { return ( <div className="max-w-3xl mx-auto p-6"> <h1 className="text-4xl font-bold mb-6">{blog.title}</h1> {blog.cover && ( <img src={blog.cover.url} alt={blog.cover.title} className="w-full h-auto rounded mb-6" /> )} <BlogContent content={blog.content} /> </div> ); } // 通过getStaticProps获取博客数据 export async function getStaticProps({ params }) { const response = await fetch('https://graphql.contentful.com/content/v1/spaces/你的空间ID', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer 你的访问令牌`, }, body: JSON.stringify({ query: ` { blogCollection(where: {slug: "${params.slug}"}, limit: 1) { items { title slug cover { url title } content { json links { assets { block { sys { id } url title description } } } } } } } `, }), }); const data = await response.json(); const blog = data.data.blogCollection.items[0]; return { props: { blog }, revalidate: 60, // 增量静态再生 }; } // 生成所有博客路径 export async function getStaticPaths() { const response = await fetch('https://graphql.contentful.com/content/v1/spaces/你的空间ID', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer 你的访问令牌`, }, body: JSON.stringify({ query: ` { blogCollection { items { slug } } } `, }), }); const data = await response.json(); const paths = data.data.blogCollection.items.map(item => ({ params: { slug: item.slug }, })); return { paths, fallback: 'blocking' }; }
注意替换代码中的你的空间ID和你的访问令牌为Contentful后台的对应值。
内容的提问来源于stack exchange,提问作者Ahmed Zrouqui
相关产品推荐
相关产品推荐

