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