1. 项目概述一个基于Notion的现代化网站生成器如果你正在寻找一个能让你用Notion作为内容管理系统CMS快速搭建起一个兼具美观与性能的个人博客、作品集或文档站点的方案那么nextjs-notion-starter-kit这个开源项目绝对值得你花时间深入研究。它不是一个玩具而是一个经过实战检验、架构清晰的生产力工具。简单来说它打通了Notion这个我们无比熟悉的笔记/文档工具与Next.js这个前沿的React框架之间的桥梁让你能像管理笔记一样轻松更新网站内容同时享受Next.js带来的服务端渲染SSR、静态站点生成SSG等现代化Web开发特性所带来的极致性能与SEO优势。我第一次接触这个项目是因为厌倦了传统CMS的笨重和静态站点生成器如Jekyll、Hugo需要本地编译、提交代码的繁琐流程。我希望我的内容创作能回归纯粹——在一个地方书写然后自动、实时地同步到我的网站上。Notion恰好满足了我对“自由编辑”和“结构化数据”的所有幻想而nextjs-notion-starter-kit则完美地将这个幻想变成了现实。它不仅仅是一个“模板”更是一套完整的工程化解决方案涵盖了从数据获取、页面渲染、样式定制到部署上线的全链路。接下来我将带你深入拆解这个项目从设计思想到每一行关键代码分享我从中获得的经验与踩过的坑。2. 核心架构与设计哲学解析2.1 为什么是Notion Next.js这个组合的巧妙之处在于它精准地捕捉了当代个人开发者和小型团队的核心痛点内容生产与发布流程的脱节。传统的流程可能需要你在Markdown编辑器、Git、CI/CD工具和服务器之间反复横跳。而Notion作为内容源带来了革命性的改变。Notion的核心优势极致的编辑体验富文本、拖拽、数据库、看板视图这些功能让内容创作和管理变得直观而高效。你可以用数据库来管理博客文章包含标题、标签、日期、状态等属性用看板来规划内容排期这远比维护一堆Markdown文件要直观得多。强大的APINotion官方提供了完善的API可以以编程方式读取数据库Database和页面Page的内容包括块级结构Block、属性Properties等。这为外部应用消费内容提供了可能。实时协作与版本历史对于团队内容创作这是无可替代的优势。任何修改都有历史记录且可以多人同时编辑。Next.js的核心优势混合渲染模式它同时支持SSG构建时生成静态HTML和SSR请求时生成HTML。对于博客这类内容更新有一定频率但不需要实时性的站点SSG是绝佳选择它能生成最快的页面。而对于需要个性化或实时数据的页面SSR可以胜任。基于文件系统的路由pages或app目录下的文件结构自动映射为路由简化了开发。出色的开发者体验DX热重载、TypeScript开箱即用、丰富的插件生态。优异的性能自动代码分割、图片优化、字体优化等特性让网站性能指标如LCP、FID非常出色。nextjs-notion-starter-kit所做的就是通过Notion API将Notion中的数据“拉取”过来然后利用Next.js的SSG能力在构建时将这些数据渲染成静态页面。当你在Notion中更新内容后触发一个重新构建的钩子例如通过Vercel的Deploy Hooks网站就自动更新了。这实现了“内容在Notion发布全自动”的梦想工作流。2.2 项目整体架构拆解这个项目的代码结构清晰遵循了Next.js的最佳实践并抽象出了几个关键层nextjs-notion-starter-kit/ ├── lib/ # 核心逻辑层 │ ├── notion.ts # Notion API客户端封装、数据获取逻辑 │ └── utils.ts # 通用工具函数日期格式化、文本处理等 ├── components/ # React组件层 │ ├── NotionPage.tsx # 核心将Notion Block渲染为React组件的渲染器 │ ├── NotionText.tsx # 处理Notion中的富文本样式 │ └── ... (其他UI组件) ├── pages/ # 页面层 (或 app/ 目录取决于Next.js版本) │ ├── index.tsx # 首页通常展示文章列表 │ ├── [slug].tsx # 动态路由用于渲染具体的文章页面 │ └── api/ # API路由用于触发增量更新等 ├── public/ # 静态资源 ├── styles/ # 样式文件 (Tailwind CSS) ├── types/ # TypeScript类型定义 └── notion.config.ts # 项目配置Notion数据库ID、站点信息等数据流的核心配置在notion.config.ts中你需要填入你的Notion集成Integration的密钥NOTION_TOKEN和作为内容源的数据库IDNOTION_DATABASE_ID。获取在lib/notion.ts中项目封装了函数如getDatabasegetPagegetBlocks来调用Notion API。这些函数会处理认证、分页、错误重试等细节。转换获取到的原始Notion数据尤其是Block数组需要被转换为一棵适合React渲染的树形结构。NotionPage组件是这个过程的枢纽它递归地遍历Block根据Block的类型type调用对应的渲染组件如HeadingBlockParagraphBlockImageBlock。渲染在页面文件如pages/[slug].tsx中使用getStaticProps和getStaticPaths这两个Next.js的数据获取函数。getStaticPaths获取所有文章的slug通常从数据库的属性中提取生成所有可能的静态页面路径。getStaticProps则根据当前slug获取对应的页面数据和块数据并传递给页面组件进行SSG渲染。样式项目通常集成Tailwind CSS样式通过组合实用类Utility Classes的方式应用到各个渲染组件上保持了高度的可定制性。注意Notion API有速率限制。在getStaticProps中大量获取页面和块数据时如果文章很多可能会触发限制。项目通常通过缓存策略如使用lru-cache或增量构建来缓解此问题。3. 关键配置与核心代码深度剖析3.1 Notion集成配置与安全实践第一步也是最关键的一步是正确配置Notion集成。这不仅仅是拿到Token和ID那么简单其中有很多安全性和功能性的细节。创建Notion集成访问https://www.notion.so/my-integrations。点击 “New integration” 填写名称选择关联的工作区。关键权限设置对于只读的博客通常只需要勾选 “Read content” 和 “Read user information” 即可。切勿授予“Update content”或“Insert content”权限除非你的网站需要向Notion回写数据这能最大程度保证内容安全。创建后复制 “Internal Integration Token” 这就是你的NOTION_TOKEN。获取数据库ID并分享给集成在你的Notion工作区创建一个数据库Database这将是你的“文章库”。设计好属性如Title标题、Slug唯一标识、Published复选框用于控制是否发布、Date日期、Tags多选等。打开这个数据库页面点击右上角的 “...” - “Add connections” 搜索并添加你刚刚创建的集成。数据库的ID可以从其URL中提取。例如URL为https://www.notion.so/your-workspace/a1b2c3d4e5f6... 那么a1b2c3d4e5f6就是数据库ID。这就是你的NOTION_DATABASE_ID。环境变量管理 绝对不要将NOTION_TOKEN和NOTION_DATABASE_ID硬编码在代码中或提交到Git仓库。必须使用环境变量。# .env.local 文件 (本地开发) NOTION_TOKENsecret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx NOTION_DATABASE_IDxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx在部署平台如Vercel、Netlify上也需要在项目设置中配置这些环境变量。3.2 Notion API数据获取层详解lib/notion.ts是这个项目的大脑。我们来看几个核心函数。数据库查询import { Client } from ‘notionhq/client’; import { cache } from ‘react’; // Next.js 14 的缓存API const notion new Client({ auth: process.env.NOTION_TOKEN }); // 使用Next.js缓存避免在同一个渲染周期内重复请求 export const getDatabase cache(async () { const response await notion.databases.query({ database_id: process.env.NOTION_DATABASE_ID!, filter: { and: [ { property: ‘Published’, // 假设有一个“Published”复选框属性 checkbox: { equals: true, }, }, ], }, sorts: [ { property: ‘Date’, // 按日期排序 direction: ‘descending’, }, ], }); return response.results; });这里使用了filter来只获取已发布Published为真的文章并用sorts按日期降序排列。cache是Next.js 14引入的API能智能地缓存函数结果在同一个请求周期内避免重复调用提升性能。获取页面内容和块export const getPage async (pageId: string) { return await notion.pages.retrieve({ page_id: pageId }); }; export const getBlocks async (blockId: string) { const blocks []; let cursor: string | undefined; // Notion API返回的块列表可能分页需要循环获取 do { const { results, next_cursor } await notion.blocks.children.list({ block_id: blockId, start_cursor: cursor, }); blocks.push(...results); cursor next_cursor ?? undefined; } while (cursor); return blocks; };getBlocks函数展示了如何处理Notion API的分页。一个复杂的页面可能包含成百上千个块API一次只返回有限数量默认100通过next_cursor可以获取下一页。3.3 核心渲染组件NotionPage这是项目的灵魂所在负责将扁平的Block列表渲染成嵌套的React组件树。其核心逻辑是递归。// components/NotionPage.tsx 简化版 const NotionPage ({ blocks }: { blocks: any[] }) { return ( div {blocks.map((block) ( NotionBlockRenderer key{block.id} block{block} / ))} /div ); }; const NotionBlockRenderer ({ block }: { block: any }) { const { type, id } block; const value block[type]; switch (type) { case ‘paragraph’: return ParagraphBlock value{value} id{id} /; case ‘heading_1’: case ‘heading_2’: case ‘heading_3’: return HeadingBlock type{type} value{value} id{id} /; case ‘image’: return ImageBlock value{value} id{id} /; case ‘bulleted_list_item’: case ‘numbered_list_item’: // 列表项需要特殊处理因为它们可能包含嵌套的子块 return ListItemBlock type{type} value{value} id{id} /; case ‘code’: return CodeBlock value{value} id{id} /; // ... 处理更多Block类型 default: console.warn(Unsupported block type: ${type}); return null; } };每个具体的块渲染组件如ParagraphBlock则负责解析该类型块特有的数据。例如ParagraphBlock需要处理富文本数组其中可能包含加粗、斜体、链接、颜色等样式。// components/NotionText.tsx const NotionText ({ text }: { text: any[] }) { if (!text) return null; return text.map((value, index) { const { annotations: { bold, italic, code, strikethrough, underline, color }, text, } value; const Tag code ? ‘code’ : ‘span’; const style: React.CSSProperties {}; if (color ! ‘default’) style.color var(--notion-${color}); // 可以映射到CSS变量 return ( Tag key{index} style{style} className{clsx( bold ‘font-bold’, italic ‘italic’, strikethrough ‘line-through’, underline ‘underline’, code ‘font-mono bg-gray-100 px-1 rounded’ )} {text.link ? ( a href{text.link.url} className“text-blue-600 hover:underline” {text.content} /a ) : ( text.content )} /Tag ); }); };这里使用了clsx库来条件组合Tailwind CSS类名是一种非常高效的做法。4. 高级功能实现与定制化指南4.1 实现增量静态再生ISR与实时预览虽然SSG速度极快但内容更新需要重新构建整个站点。对于更新频繁的站点这可能不够理想。Next.js的增量静态再生ISR提供了完美的解决方案。ISR实现 在getStaticProps中除了返回props 还可以返回一个revalidate字段单位秒。export async function getStaticProps({ params }) { const post await getPageBySlug(params.slug); const blocks await getBlocks(post.id); return { props: { post, blocks }, revalidate: 60, // 每隔60秒允许下一个请求重新生成此页面 }; }这样页面在构建后仍然是静态的但每隔60秒如果有新的请求到来Next.js会在后台重新运行getStaticProps获取最新数据生成新的页面并替换旧版本。用户访问的始终是快速的静态页面而内容可以在后台更新。实时预览Preview Mode 有时在Notion中编辑后你想立即在网站上看到效果而不是等待ISR周期或重新构建。Next.js的预览模式可以实现这一点。创建一个API路由例如pages/api/preview.ts 它设置预览模式Cookie。在getStaticProps中检查预览模式。如果启用则绕过缓存直接请求最新数据。在Notion中你可以设置一个“立即预览”按钮点击后调用这个API并重定向到文章页面。这为内容编辑者提供了所见即所得的体验。4.2 深度样式定制与主题系统项目默认使用Tailwind CSS这给了我们极大的定制自由。但直接修改组件类名可能不够系统化。建议建立一套设计令牌Design Tokens或主题变量。步骤一扩展Tailwind配置在tailwind.config.js中定义你的颜色、字体、间距等主题变量。module.exports { theme: { extend: { colors: { ‘primary’: ‘#0070f3’, ‘secondary’: ‘#ff4081’, ‘notion-gray’: ‘#f7f6f3’, // 模仿Notion的背景色 }, fontFamily: { ‘sans’: [‘Inter’, ‘system-ui’, ‘sans-serif’], // Notion使用的字体 }, }, }, }步骤二创建可复用的组件变体不要在每个NotionBlockRenderer的子组件里写死类名。可以创建一套组件映射。// components/ui/typography.tsx export const H1 ({ children, className }) ( h1 className{text-4xl font-bold mt-8 mb-4 ${className}}{children}/h1 ); export const H2 ({ children, className }) ( h2 className{text-3xl font-semibold mt-6 mb-3 ${className}}{children}/h2 ); // ... 其他文本组件然后在HeadingBlock中直接使用H1或H2。步骤三支持暗色模式利用Tailwind CSS的dark:变体。body class“bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100”在组件中p className“text-gray-700 dark:text-gray-300”.../p你可以在站点头部添加一个切换按钮用JavaScript切换html元素上的class“dark”。4.3 SEO与性能优化实战一个用Notion驱动的网站在SEO和性能上完全可以做到顶尖水平。SEO优化Next.js Head组件在每个页面pages/[slug].tsx中使用next/head动态设置titlemeta name“description” 以及Open Graph标签用于社交媒体分享。import Head from ‘next/head’; export default function PostPage({ post }) { return ( Head title{post.properties.Title.title[0].plain_text} | 我的博客/title meta name“description” content{post.properties.Excerpt?.rich_text[0]?.plain_text || ‘’} / meta property“og:title” content{post.properties.Title.title[0].plain_text} / meta property“og:image” content{post.cover?.external?.url || post.cover?.file?.url || ‘/default-og.png’} / /Head {/* 页面内容 */} / ); }生成站点地图Sitemap在pages/sitemap.xml.js中创建一个API路由动态生成包含所有文章链接的XML站点地图。规范链接Canonical URL在Head中设置link rel“canonical” href{当前页面完整URL} / 避免重复内容。性能优化Next.js Image组件Notion中的图片URL是外链。务必使用next/image组件来优化。import Image from ‘next/image’; Image src{imageUrl} alt{altText} width{1200} // 指定宽度和高度以优化布局偏移CLS height{630} layout“responsive” // 或 “intrinsic” “fixed” priority{true} // 对于首屏关键图片可以预加载 /这会自动实现图片的懒加载、WebP格式转换、尺寸优化等。字体优化使用next/font来托管和优化自定义字体如Inter消除布局偏移并提升加载速度。代码分割Next.js默认已做得很好了。确保你的大型第三方库如某些图表库被动态导入dynamic import。缓存策略对于Notion API的响应可以在getStaticProps中使用内存缓存如LRU Cache或部署平台提供的边缘缓存如Vercel的ISR减少API调用次数和延迟。5. 部署、监控与常见问题排查5.1 部署平台选择与配置首选Vercel作为Next.js的创建者Vercel提供了最无缝的体验。连接你的Git仓库后它会自动检测Next.js项目并进行优化构建。环境变量在Vercel项目设置的Environment Variables中填入NOTION_TOKEN和NOTION_DATABASE_ID。构建命令通常为npm run build或next build。输出目录Next.js项目不需要指定。触发构建你可以配置一个GitHub Action或使用Vercel的Deploy Hooks。更优雅的方式是使用Notion的Webhooks需通过第三方服务中转如Zapier或Make.com当数据库有更新时自动调用Vercel的Deploy Hook URL触发重新构建。备选Netlify配置类似同样支持环境变量和自动部署。Netlify的Forms和Functions功能也很强大。静态导出Static Export如果你希望部署到任何静态托管服务如GitHub Pages Cloudflare Pages可以运行next export命令。但请注意这会生成纯静态HTML将无法使用ISR、API路由等需要Node.js运行时的功能。对于纯内容博客这通常是可行的。5.2 监控与日志网站上线后监控是必不可少的。错误监控集成Sentry或LogRocket。在_app.tsx中初始化Sentry可以捕获前端React错误和后端Next.js API路由的错误。性能监控使用Vercel Analytics、Google Lighthouse CI或商业方案如SpeedCurve持续监控核心Web指标LCP FID CLS。API健康检查Notion API偶尔会有不稳定。可以设置一个简单的Cron Job例如使用GitHub Actions定期调用一个检查用的API路由该路由尝试获取一篇已知文章如果失败则发送告警如通过邮件、Slack。5.3 常见问题与解决方案实录以下是我在多次使用和部署nextjs-notion-starter-kit过程中遇到的一些典型问题及解决方法。问题现象可能原因解决方案构建失败错误信息包含Notion API error: object_not_found1.NOTION_DATABASE_ID错误。2. Notion集成Integration未被分享到该数据库。1. 仔细核对数据库ID确保从正确的URL提取。2. 进入Notion数据库页面点击“Share”或“Add connections”确保你的集成已被添加。页面能构建但文章内容空白或格式错乱1. Notion页面内容结构复杂有未支持的Block类型。2.getBlocks函数分页逻辑有误未获取全部块。3. 样式CSS未正确加载或冲突。1. 在NotionBlockRenderer的default分支中添加日志查看不支持的类型并考虑实现或忽略它。2. 检查getBlocks中的分页循环逻辑确保cursor被正确处理。3. 检查浏览器控制台是否有CSS加载错误确保Tailwind CSS已正确编译引入。本地开发正常部署后图片不显示1. Notion图片链接是内部链接需要Notion登录才能访问。2. 部署环境如Vercel的IP被Notion限制。1.这是最常见的问题Notion的图片链接file类型是临时的且受权限保护。解决方案是在getBlocks获取数据后遍历所有image类型的块将其file.url通过一个代理API路由进行中转或者使用next/image的loader属性配置一个自定义图片优化服务如将图片先上传到Cloudinary或Imgix。社区有相关方案需要额外处理。ISR不生效页面内容不更新1.revalidate值设置过大。2. 部署平台的ISR支持问题。3. 页面访问量过低始终未触发再生。1. 将revalidate设置为一个合理的值如601分钟。2. 确保部署在支持ISR的平台Vercel Netlify等。3. 可以手动访问页面并加上?force-reloadtrue之类的参数然后在代码中监听此参数强制刷新数据。网站打开速度慢尤其是图片多时1. 图片未优化尺寸过大。2. 未使用next/image。3. 字体文件过大或未优化。1. 强制使用next/image组件。2. 配置next/image的loader指向一个图片CDN如Vercel自身或Cloudinary进行自动优化。3. 使用next/font加载字体并只加载需要的字重。一个关于图片代理的实操心得 直接使用Notion的图片URL在外网是不可靠的。我采用的稳定方案是在Next.js中创建一个API路由/api/proxy-image?url...。这个路由的任务是接收Notion的图片URL。使用服务器的环境变量NOTION_TOKEN来获取图片数据因为服务器有权限。将图片数据流式传输stream回客户端。 然后在图片组件的src中不使用原始Notion URL而是使用这个代理路由的地址。虽然增加了一次服务器中转但保证了图片的稳定可访问性并且仍然可以利用next/image进行格式和尺寸优化。另一个关于数据库筛选的坑 如果你的Notion数据库有很多属性并且在API查询中使用了复杂的筛选器filter可能会遇到查询超时或返回结果不完整。Notion API对复杂查询的支持有限。我的经验是尽量保持筛选条件简单。如果需要复杂查询可以考虑在获取全部数据后在Next.js服务端内存中进行过滤和排序但这只适用于数据量不大的情况。对于大型数据库更优的设计是直接在Notion中利用视图View的筛选和排序功能然后通过API获取特定视图下的页面这相当于把过滤逻辑交给了Notion。这个项目就像一个精密的乐高套装提供了所有核心模块。你的创造力决定了最终建筑的样貌。你可以把它扩展成一个多作者博客平台、一个产品文档中心、甚至是一个简单的电商产品目录。其核心价值在于解放了内容创作者让技术栈成为默默无闻的基石而非需要反复打理的盆景。当你下次在Notion中流畅地写完一篇文档并看到它自动、完美地呈现在你自己的网站上时你会感受到这种工作流带来的巨大愉悦感。