使用 Notion + Next.js 搭建博客

分类
教程
标签
经验分享工具推荐

最近发现 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