You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 07:35:19