1. 项目概述当React遇见Notion一个强大的内容渲染引擎如果你和我一样既是开发者又是Notion的重度用户那你一定有过这样的想法我能不能把Notion里那些精心编排的页面直接搬到我的个人网站、博客或者产品文档里让Notion强大的编辑器成为我的内容创作后台而前端则用我熟悉的React技术栈来呈现。几年前这个想法实现起来颇为复杂需要自己解析Notion的私有API处理各种复杂的块类型和样式。但现在有了react-notion-x这个项目这一切变得前所未有的简单。react-notion-x是一个用于在React应用中渲染Notion页面的高质量、高性能React库。它的核心价值在于它不仅仅是一个简单的“页面查看器”而是一个功能完整、高度可定制、且与Notion官方体验高度一致的渲染引擎。你可以把它理解为一个“Notion渲染器SDK”。它负责将Notion页面数据通过官方API或第三方工具获取转换为一整套React组件包括页面布局、文本样式、列表、待办事项、代码块、数据库表格、看板、画廊等、嵌入内容如视频、地图、Figma等等。这意味着你可以用Notion来管理你的所有内容然后通过react-notion-x无缝地将这些内容集成到任何React应用中无论是Next.js、Gatsby、Vite还是Create React App项目。这个库特别适合几类人独立开发者或小团队希望快速搭建一个内容驱动的网站如博客、作品集、文档站而不想从头构建复杂的内容管理系统CMS产品经理或运营人员希望用更友好的方式Notion来维护产品更新日志、帮助文档并自动同步到官网以及任何希望将Notion的协作和编辑能力与自定义前端结合的开发者。我自己的技术博客和项目文档就完全基于此构建实测下来开发和内容更新效率提升了不止一个量级。2. 核心架构与设计哲学不只是渲染更是生态在深入代码之前理解react-notion-x的设计思路至关重要。它没有试图重新发明轮子去模拟一个Notion客户端而是聪明地扮演了“渲染层”的角色。其架构可以清晰地分为三层数据获取层、核心渲染层和扩展生态层。2.1 数据获取官方API与第三方方案的权衡react-notion-x本身不处理数据获取。它需要一个符合其预期的页面数据对象通常是RecordMap类型。这带来了极大的灵活性。你可以通过以下两种主流方式获取数据官方Notion API这是最“正统”的方式。你需要创建Notion集成获取API密钥然后调用notion.pages.retrieve或notion.blocks.children.list等接口。这种方式安全、稳定能获取到最新的数据结构和功能。但缺点是需要处理OAuth授权对于公开页面可简化并且API有速率限制。notion-client这是react-notion-x官方推荐的兄弟库。它提供了一个更友好的客户端封装了与Notion API的交互并内置了缓存等优化。更重要的是对于公开页面它可以使用一种更高效、无需认证的“民间”方式通过解析页面HTML来获取数据这非常适合构建静态网站。许多知名的Notion博客框架如Next.js Notion Starter Kit都基于此组合。注意使用非官方API方式获取公开页面数据虽然方便但存在被Notion更改页面结构而中断的风险。对于生产环境尤其是涉及私有内容时强烈建议使用官方API。2.2 核心渲染器组件化与样式隔离库的核心是NotionRenderer /组件。你只需要将获取到的recordMap数据和一个rootPageId传递给它它就能渲染出整个页面树。其内部实现非常精巧块类型映射Notion中有数十种块类型paragraph,heading_1,to_do,code,embed,collection_view等。react-notion-x为每一种块都预先定义了对应的React组件。这些组件负责将块的原始数据如文本内容、样式属性、子块列表转换为正确的HTML结构和CSS样式。样式系统它自带一套精心打磨的CSS样式力求与Notion官方的视觉效果保持一致包括字体通常使用Inter、颜色、间距、交互状态如悬停等。你可以完全覆盖这些样式也可以只做局部调整。样式通常通过独立的CSS文件引入确保了样式的模块化和可替换性。动态加载与性能对于大型页面库会智能地处理渲染。它不是一次性渲染所有内容而是遵循React的渲染流程。对于数据库视图如表格这类复杂组件渲染开销较大但库做了相应优化以保持流畅。2.3 可扩展性与自定义覆盖这是react-notion-x最强大的地方之一。它几乎允许你自定义每一个环节。组件覆盖你可以通过components属性传入一个自定义组件映射表。例如你可以用一个更强大的语法高亮组件如Prism替换默认的代码块组件或者为图片组件添加懒加载和灯箱效果。import { Code } from ./my-custom-code; import { Image } from ./my-lazy-image; const myComponents { code: Code, image: Image, }; NotionRenderer recordMap{recordMap} components{myComponents} /页面链接解析默认情况下页面内的链接指向其他Notion页面会使用next/link如果检测到或普通的a标签。你可以通过mapPageUrl属性完全控制页面URL的生成逻辑轻松将其适配到你的路由系统如React Router。属性覆盖几乎所有内部组件接受的Props如className,style都可以通过顶层配置传递下去让你能进行细粒度的样式和功能控制。3. 从零开始构建一个基于Notion的个人博客理论说了这么多我们来点实际的。我将带你一步步用react-notion-x和 Next.jsApp Router搭建一个最简单的个人博客。这个博客的内容完全由Notion中的一个页面数据库驱动。3.1 项目初始化与依赖安装首先创建一个新的Next.js项目并安装核心依赖。npx create-next-applatest notion-blog --typescript --tailwind --app cd notion-blog npm install react-notion-x notion-client这里我们选择了TypeScript、Tailwind CSS和最新的App Router。notion-client用于获取数据。3.2 获取Notion API权限并准备数据源创建集成访问 Notion Developers 创建一个新的“Internal Integration”。获取到NOTION_SECRET即API Key。分享数据库给集成在你的Notion工作区创建一个“Database”页面这就是你的博客文章库。添加一些属性如Title、Slug、Published复选框、Date日期。然后在这个数据库页面的右上角点击“Share”邀请你刚刚创建的集成输入集成的名称并赋予“Read”权限。复制这个数据库的IDURL中?v后面之前的那串长字符。环境变量在项目根目录创建.env.local文件NOTION_SECRETyour_secret_here NOTION_DATABASE_IDyour_database_id_here3.3 实现核心数据获取函数我们将创建一个服务层专门负责与Notion交互。新建lib/notion.tsimport { Client } from notionhq/client; import { NotionAPI } from notion-client; // 使用官方客户端查询数据库列表 const notionClient new Client({ auth: process.env.NOTION_SECRET, }); // 使用 notion-client 获取完整的页面渲染数据 const notionXClient new NotionAPI(); export async function getPublishedPosts() { const response await notionClient.databases.query({ database_id: process.env.NOTION_DATABASE_ID!, filter: { property: Published, checkbox: { equals: true, }, }, sorts: [ { property: Date, direction: descending, }, ], }); return response.results; } export async function getPageRecordMap(pageId: string) { // 使用 notion-client 获取渲染所需的 recordMap // 这里使用了更高效的第三方获取方式针对公开页面 // 如需使用官方API可配置 notionXClient 的 auth 参数 const recordMap await notionXClient.getPage(pageId); return recordMap; } export function getPageSlug(page: any): string { // 从页面属性中获取自定义的Slug如果没有则使用Notion生成的ID const slugProperty page.properties.Slug; if (slugProperty slugProperty.type rich_text slugProperty.rich_text[0]) { return slugProperty.rich_text[0].plain_text; } return page.id.replace(/-/g, ); }3.4 构建博客首页与文章详情页首页 (app/page.tsx)展示文章列表。import Link from next/link; import { getPublishedPosts, getPageSlug } from /lib/notion; export default async function Home() { const posts await getPublishedPosts(); return ( div classNamemax-w-4xl mx-auto p-8 h1 classNametext-4xl font-bold mb-10我的Notion博客/h1 div classNamespace-y-6 {posts.map((post) { const title post.properties.Title?.title[0]?.plain_text || Untitled; const slug getPageSlug(post); return ( article key{post.id} classNameborder-b pb-6 Link href{/blog/${slug}} h2 classNametext-2xl font-semibold hover:text-blue-600{title}/h2 /Link p classNametext-gray-500 mt-2 {new Date(post.last_edited_time).toLocaleDateString()} /p /article ); })} /div /div ); }文章详情页 (app/blog/[slug]/page.tsx)使用react-notion-x渲染完整页面。import { NotionRenderer } from react-notion-x; import { getPageRecordMap } from /lib/notion; import { getPublishedPosts, getPageSlug } from /lib/notion; import { notFound } from next/navigation; // 导入必要的样式和组件 import react-notion-x/src/styles.css; import { Code } from react-notion-x/build/third-party/code; import { Collection } from react-notion-x/build/third-party/collection; interface BlogPageProps { params: Promise{ slug: string }; } export default async function BlogPage({ params }: BlogPageProps) { const { slug } await params; const posts await getPublishedPosts(); const targetPost posts.find((post) getPageSlug(post) slug); if (!targetPost) { notFound(); } const recordMap await getPageRecordMap(targetPost.id); return ( div classNamemax-w-4xl mx-auto p-4 md:p-8 NotionRenderer recordMap{recordMap} fullPage{true} // 渲染为完整页面布局 darkMode{false} // 可根据主题切换 components{{ code: Code, // 使用增强的代码组件带语法高亮 collection: Collection, // 支持渲染数据库视图 }} mapPageUrl{(pageId) /blog/${pageId}} // 自定义内部页面链接映射 / /div ); } // 生成静态路径 export async function generateStaticParams() { const posts await getPublishedPosts(); return posts.map((post) ({ slug: getPageSlug(post), })); }3.5 样式优化与集成全局样式在app/globals.css中确保导入了Notion的样式并可以覆盖一些变量来自定义主题。import react-notion-x/src/styles.css; /* 自定义主题色 */ :root { --notion-max-width: 720px; } .notion { font-family: var(--font-sans), -apple-system, Inter, sans-serif; }元数据在app/layout.tsx中设置合适的SEO元数据。你也可以从Notion页面属性中动态获取标题和描述。至此一个最基本的、内容完全由Notion驱动的博客就搭建完成了。运行npm run dev你就可以看到效果。在Notion中编辑文章并勾选“Published”重新构建或刷新页面后变化就会立刻体现在你的网站上。4. 高级功能与深度定制实践基础功能跑通后我们会面临更多实际需求。react-notion-x提供了丰富的接口来满足这些需求。4.1 自定义渲染组件以代码块和图片为例默认的代码块支持语法高亮但你可能想换成你喜欢的主题或者增加“复制代码”按钮。图片组件可能缺少懒加载。自定义代码块组件 (components/MyCode.tsx):use client; import { Code as NotionCode } from react-notion-x/build/third-party/code; import { CopyToClipboard } from react-copy-to-clipboard; import { useState } from react; export function MyCode(props: any) { const [copied, setCopied] useState(false); const code props.content?.[0]?.[0] || ; const handleCopy () { setCopied(true); setTimeout(() setCopied(false), 2000); }; return ( div classNamerelative my-4 div classNameabsolute right-2 top-2 z-10 CopyToClipboard text{code} onCopy{handleCopy} button classNamepx-3 py-1 text-xs bg-gray-800 text-white rounded hover:bg-gray-700 {copied ? 已复制! : 复制} /button /CopyToClipboard /div {/* 使用原生的Code组件但可以传递自定义的语法高亮主题 */} NotionCode {...props} className!mt-0 / /div ); }自定义图片组件 (components/MyImage.tsx):use client; import Image from next/image; import { useState } from react; export function MyImage({ src, alt, ...props }: any) { const [isLoading, setIsLoading] useState(true); if (!src) return null; // 处理Notion图片URL可能需要代理或直接使用 const imageUrl src.startsWith(http) ? src : https://www.notion.so${src}; return ( div classNamemy-4 overflow-hidden rounded-lg Image src{imageUrl} alt{alt || Notion image} width{1200} height{630} className{ duration-300 ease-in-out ${isLoading ? scale-105 blur-lg : scale-100 blur-0} } onLoadingComplete{() setIsLoading(false)} {...props} / /div ); }然后在渲染器中替换它们NotionRenderer recordMap{recordMap} components{{ code: MyCode, image: MyImage, // ... 其他覆盖 }} /4.2 处理复杂块类型数据库与嵌入数据库视图Collection组件已经能很好地渲染表格、列表等视图。但你可能需要自定义每个数据库项的样式或者过滤、排序数据。这可以通过在Notion中配置不同的视图来实现react-notion-x会尊重这些视图设置。更复杂的交互如前端筛选则需要你自行获取collection_data并渲染。嵌入块Notion支持嵌入大量第三方内容YouTube, Twitter, Codepen, Figma等。react-notion-x默认会渲染一个iframe或链接。为了更好的性能和体验我建议使用专门的React库来渲染这些嵌入。例如对于YouTube你可以用react-lite-youtube-embed替换默认的embed组件它能提供更快的加载和预览图。4.3 性能优化与缓存策略对于内容站点性能至关重要。静态生成如上例所示在Next.js中使用generateStaticParams和getPageRecordMap在构建时获取所有页面数据并生成静态HTML。这是最快的方案。增量静态再生如果内容更新频繁可以使用Next.js的ISR。为详情页设置一个revalidate时间Next.js会在后台按需重新生成页面。// 在详情页组件中 export const revalidate 3600; // 每1小时重新验证一次客户端缓存notion-client本身有内存缓存。对于更持久的缓存可以考虑使用Redis或文件系统缓存层在数据获取函数中实现。图片优化使用Next.js的next/image组件如我们的MyImage示例自动处理图片的懒加载、尺寸优化和WebP格式转换。注意这需要配置next.config.js允许Notion的图片域名。5. 常见问题、排查技巧与避坑指南在实际使用中我踩过不少坑也总结了一些经验。5.1 数据获取失败与认证错误症状控制台报错403或Cannot read properties of undefined。排查检查环境变量确保NOTION_SECRET和NOTION_DATABASE_ID已正确设置在.env.local中并且已重启开发服务器。检查分享权限确认你已在Notion页面中分享了数据库或页面给你的集成Integration而不是个人账户。这是最常见的错误。检查API版本Notion API有时会更新。确保你安装的notionhq/client版本不是太旧。使用正确的页面ID确保你传递的是页面的UUID而不是URL。正确的ID是https://www.notion.so/username/Page-Title-a1b2c3d4e5f6g7h8i9j0中最后一部分。5.2 样式错乱或丢失症状页面布局混乱没有Notion的样式。排查确认导入了CSS确保在使用的组件或全局入口文件中导入了import react-notion-x/src/styles.css。检查CSS冲突如果你使用了Tailwind CSS等框架其预检样式可能会重置某些属性。在tailwind.config.js中设置corePlugins: { preflight: false }可以禁用预检但可能影响其他组件。更好的做法是提高Notion样式选择器的优先级或者将Notion渲染部分包裹在一个容器内使用CSS模块或scoped样式。检查fullPage属性fullPage{true}会应用完整的页面布局样式如最大宽度、背景色。如果你只想要内容区的样式可以设为false然后自己用容器控制布局。5.3 数据库视图不显示或显示不全症状页面中的表格、看板视图显示为空白或加载失败。排查引入Collection组件确保你已将Collection组件传入components属性。检查数据权限数据库视图的数据获取可能需要额外的权限。确保你的集成对该数据库有“Read”权限并且数据库本身及其父页面都已分享给集成。使用notion-client某些复杂的数据库视图数据使用官方的notionhq/client可能获取不全。notion-client库在获取recordMap时通常更可靠。5.4 构建时内存溢出或超时症状在Vercel或Netlify上构建时失败报内存错误或函数执行超时。排查页面数量过多如果你有上百篇文章在构建时一次性获取所有页面的recordMap可能会消耗大量内存和API调用。考虑分批次生成或只对热门文章预渲染其余使用客户端动态渲染CSR。优化数据获取在getPageRecordMap中可以尝试使用官方API配置auth并设置更低的超时和重试或者实现一个更健壮的缓存层避免重复获取未变更的页面。增加构建资源在部署平台上可以尝试增加构建机器的内存配置。5.5 内容更新延迟症状在Notion中更新了内容但网站上看不到变化。解决方案静态站点如果你使用静态生成需要重新触发构建如Vercel的Git Hook或手动部署。ISR确保你正确配置了revalidate。注意ISR的重新验证是在请求到来时触发的。第一个访问过期页面的用户会得到旧页面同时触发后台更新。客户端缓存检查浏览器缓存。notion-client的缓存可能导致客户端在短时间内看不到更新。在开发中可以暂时禁用缓存。我个人最实用的一个技巧创建一个“开发专用”的Notion页面里面包含所有你想测试的块类型标题、列表、待办、代码、数据库、嵌入等。在开发时先用这个固定的页面ID进行渲染调试可以快速隔离问题是出在数据获取还是组件渲染上避免被具体文章内容的不确定性干扰。