最近发现 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-x
3. 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 文档