最近发现 Notion 作为内容管理工具非常好用,于是我决定把博客搬到 Notion 上,并通过 Next.js + react-notion-x 来实现页面渲染。本文会分享完整流程,包括数据库配置、代码实现以及内容渲染。
1. 在 Notion 中创建数据库
首先需要在 Notion 中建立一个数据库来存放博客文章。
我为数据库添加了以下属性:
- Title:文章标题
- Status:文章状态(如:构思中、已发布)
- Categories:分类(单选)
- Tags:标签(多选)
- CreatedAt:创建时间
- URLName:文章的唯一标识
- Digest:文章摘要
配置好数据库后,就可以直接在 Notion 中编写文章了。
2. 安装依赖
在 Next.js 项目中安装 Notion 相关依赖:
1pnpm add @notionhq/client notion-client react-notion-x3. Notion API 封装
在 
lib/notion.ts 中封装操作 Notion 的 API,包括分页查询、获取所有文章、解析 Markdown 和 RecordMap。1/* eslint-disable @typescript-eslint/no-explicit-any */
2import { Client } from '@notionhq/client';
3import { NotionAPI } from 'notion-client';
4
5// 初始化客户端
6export const notion = new Client({ auth: process.env.NOTION_TOKEN });
7export const NOTION_DATABASE_ID = process.env.NOTION_DATABASE_ID;
8const notionApi = new NotionAPI({
9  activeUser: process.env.NOTION_ACTIVE_USER,
10  authToken: process.env.NOTION_TOKEN_V2,
11});
12
13// ... 这里省略缓存与 transform 函数
14
15// 获取分页博客
16export async function getPaginatedBlogs(pageSize: number = 10, startCursor?: string) {
17  const response = await notion.databases.query({
18    database_id: NOTION_DATABASE_ID,
19    filter: { property: 'Status', status: { equals: '已发布' } },
20    sorts: [{ property: 'CreatedAt', direction: 'descending' }],
21    page_size: pageSize,
22    start_cursor: startCursor,
23  });
24  return response.results.map(transformNotionPageToBlog);
25}
26
27// 获取所有博客(用于计算总数和静态生成)
28export async function getAllBlogs(): Promise<NotionBlog[]> {
29  const allBlogs: NotionBlog[] = [];
30  let hasMore = true;
31  let startCursor: string | undefined;
32
33  while (hasMore) {
34    const result = await getPaginatedBlogs(100, startCursor);
35    allBlogs.push(...result.data);
36    hasMore = result.hasMore;
37    startCursor = result.nextCursor || undefined;
38  }
39
40  return allBlogs;
41}
42
43// 使用 notion-client 获取完整的 recordMap,用于 react-notion-x 渲染
44export async function getPageRecordMap(pageId: string) {
45  try {
46    const cleanId = pageId.replace(/-/g, '');
47
48    // 添加超时控制,最多等待8秒
49    const timeoutPromise = new Promise((_, reject) => {
50      setTimeout(() => reject(new Error('RecordMap获取超时')), 8000);
51    });
52
53    const recordMapPromise = notionApi.getPage(cleanId);
54    
55    const recordMap = await Promise.race([recordMapPromise, timeoutPromise]);
56
57    return recordMap;
58  } catch (error) {
59    console.error(`[NotionRecordMap] RecordMap 获取失败`, {
60      error: error instanceof Error ? error.message : String(error),
61      pageId,
62      cleanId: pageId.replace(/-/g, ''),
63    });
64    
65    // 如果是超时错误,返回一个空的recordMap而不是抛出错误
66    if (error instanceof Error && error.message.includes('超时')) {
67      return {
68        block: {},
69        collection: {},
70        collection_view: {},
71        notion_user: {},
72        signed_urls: {},
73        preview_images: {},
74      };
75    }
76    
77    throw error;
78  }
79}4. 内容渲染器
使用 
react-notion-x 来渲染文章内容,并在页面中自定义博客标题、摘要、分类和标签。1'use client';
2
3import { NotionRenderer } from 'react-notion-x';
4import { useTheme } from 'next-themes';
5
6const NotionContent = ({ recordMap, blog }) => {
7  const { resolvedTheme } = useTheme();
8  const isDark = resolvedTheme === 'dark';
9
10  return (
11    <div className="notion container mx-auto px-4 py-12 max-w-5xl">
12      <NotionRendererpageTitle={<h1 className="text-4xl font-bold">{blog.title}</h1>}
13        disableHeader={true}
14        recordMap={recordMap}
15        darkMode={isDark}
16        fullPage={true}
17        forceCustomImages
18      />
19    </div>
20  );
21};
22
23export default NotionContent;这样,每一篇 Notion 文章都能无缝渲染到博客中。
5. 环境变量配置
在 
.env.local 中添加必要的配置:1NOTION_TOKEN=你的NotionAPIKey
2NOTION_DATABASE_ID=你的数据库ID
3NOTION_ACTIVE_USER=可选
4NOTION_TOKEN_V2=可选- NOTION_TOKEN:从 Notion API 获取
- NOTION_DATABASE_ID:数据库页面 URL 中的 ID
- 其它参数可参考 react-notion-x 文档