Notion-As-Blog — 在 Notion 写作,用 Next.js 发布

Notion-As-Blog

Write in Notion. Publish with Next.js.
一个精致、可自托管的博客模板,基于 Notion、Next.js 16 和 Tailwind CSS 4 构建。

在线文档 · 复制模板 · 快速开始 · English · 한국어 · 简体中文 · 日本語

Next.js TypeScript Tailwind CSS Notion License

--- ## 为什么选择 Notion-As-Blog? Notion-As-Blog 可以把复制后的 Notion 模板变成一个面向生产环境的博客:页面速度快、Notion 图片稳定可用、SEO 友好、支持深色模式、搜索、RSS、站点地图、作者资料,并且整个发布流程都保留在 Notion 中。
以 Notion 为中心的写作体验
直接在 Notion 中创建文章、页面、标签、分类、系列、作者、缩略图和摘要。
生产环境默认配置
内置 Next.js App Router、静态生成、图片代理、RSS、sitemap、robots.txt、动态 OG 图片和 Organization JSON-LD。
开箱即用的漂亮界面
响应式布局、深色模式、文章动画、置顶文章轮播、Giscus 评论和可定制品牌设置。
适合自托管
可部署到 Vercel、Docker 或你自己的 Node.js 主机,并明确配置环境变量和缓存行为。
--- ## 预览 以下截图来自已部署的官方文档站点。 | 浅色 | 深色 | |:---:|:---:| | ![文档首页浅色模式](docs/screenshots/home-desktop.png) | ![文档首页深色模式](docs/screenshots/home-dark.png) |

文档文章页
移动端文档首页

--- ## 功能特性 - **Notion 作为 CMS** — 直接在 Notion 中撰写和管理文章。 - **多作者支持** — 可选的 Authors data source,支持头像、简介、角色和社交链接。 - **分类、标签和系列** — 支持分类页、标签筛选和系列文章上一篇/下一篇导航。 - **全文搜索** — 内置搜索 API,支持即时下拉结果和独立搜索页。 - **深色模式** — 使用轻量内置偏好设置 hook,支持跟随系统主题。 - **SEO 优化** — Open Graph、动态 OG 图片、sitemap、robots.txt、RSS、canonical URL 和 Organization JSON-LD。 - **稳定的 Notion 图片** — 使用签名图片代理处理会过期的 Notion 上传文件 URL。 - **Giscus 评论** — 在文章详情页使用 GitHub Discussions 评论。 - **响应式 UI** — 使用 Tailwind CSS 4 构建的移动优先布局。 - **自定义品牌** — 可配置站点名称、logo、favicon、颜色、字体、页脚链接、社交链接、分类和文案。 - **Docker 就绪** — 包含多阶段生产 Dockerfile 和 compose 示例。 - **按需重新验证** — 通过受保护的 revalidation endpoint 立即刷新内容。 --- ## 快速开始 ### 1. 复制 Notion 模板 打开下面的公开 Notion 页面,点击 **Duplicate**,将完整模板复制到你自己的 Notion 工作区。 > **[复制 Notion-As-Blog 模板](https://welcometogyuminworld.notion.site/Notion-As-Blog-30ab152141a480309a9ede1f8cac4cc7?source=copy_link)** 模板包含一个 **Posts** data source、一个可选的 **Authors** data source,以及文档式示例内容。复制后,所有数据库行都属于你的工作区,因此可以安全地编辑或删除。 ### 2. 创建 Notion integration 1. 打开 [My Integrations](https://www.notion.so/profile/integrations),点击 **New integration**。 2. 填写名称,例如 `notion-as-blog`。 3. 选择你复制模板所在的工作区。 4. 复制 **Internal Integration Secret** — 这就是你的 `NOTION_API_KEY`。 ### 3. 将 integration 连接到数据库 1. 在 Notion 中打开 **Posts** 数据库页面。 2. 点击右上角 **···** → **Connections** → 找到你的 integration 并点击 **Connect**。 3. 如果你想使用完整作者资料,也对 **Authors** 数据库重复同样操作。 ### 4. 获取 data source ID `NOTION_DATA_SOURCE_ID` 需要的是 **Notion data source ID**。在较新的 Notion API 中,它可能不同于 URL 中显示的 database ID。 打开复制后的 Posts 数据库,连接 integration 后,复制 Posts data source ID 并填入 `NOTION_DATA_SOURCE_ID`。如果需要作者资料,也为 Authors data source 重复此操作。如果误用了 database ID,构建可能会因 `object_not_found` 失败。 ### 5. 克隆并配置 ```bash git clone https://github.com/catuscio/notion-as-blog.git cd notion-as-blog npm install cp .env.example .env.local ``` 编辑 `.env.local`: ```env NOTION_API_KEY=secret_xxxxxxxxxxxxxxxxxxxxx NOTION_DATA_SOURCE_ID=your_posts_data_source_id NOTION_AUTHORS_DATA_SOURCE_ID=your_authors_data_source_id TOKEN_FOR_REVALIDATE=any_random_secret_string NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX ``` ### 6. 本地运行 ```bash npm run dev ``` 打开 [http://localhost:3000](http://localhost:3000) 查看你的博客。 --- ## 在 Notion 中写文章 ### 创建新文章 1. 在 Notion 中打开 **Posts** 数据库 2. 新增一行并填写属性 3. 在页面正文中撰写内容 — 支持标题、列表、代码块、图片等 4. 准备发布时,将 `status` 设为 **Public** ### Posts 数据库字段 | 字段 | 类型 | 必填 | 描述 | |---|---|---|---| | **title** | Title | 是 | 文章标题 | | **slug** | Rich text | 否 | URL slug(例如 `my-first-post`)。为空时会根据页面 ID 自动生成 | | **status** | Select | 是 | 发布状态(见下文) | | **type** | Select | 否 | `Post`(默认)或 `Page` | | **date** | Date | 是 | 发布日期。文章按此字段排序 | | **category** | Select | 是 | 必须与 `brand.ts` 中定义的分类名称一致 | | **tags** | Multi-select | 否 | 用于筛选的自由标签(例如 `Next.js`、`React`) | | **series** | Select | 否 | 系列名称。选择相同系列的文章会被分组,并显示上一篇/下一篇导航 | | **author** | People | 否 | Notion 工作区成员。完整作者资料会通过 Authors data source 的 `people` 属性匹配 | | **summary** | Rich text | 否 | 显示在文章卡片和 SEO meta 中的简短描述 | | **thumbnail** | Files & media | 否 | 封面图片(上传文件或粘贴外部 URL) | ### Status 值 | 值 | 显示在列表中 | 可通过直接 URL 访问 | |---|---|---| | `Public` | 是 | 是 | | `PublicOnDetail` | 否 | 是 — 同时排除搜索索引的 unlisted 内容,并不提供访问控制 | | `Draft` | 否 | 否 | | `Private` | 否 | 否 | `PublicOnDetail` 对 `Post` 和 `Page` 使用相同规则。它不会出现在信息流、搜索、分类、标签、系列、作者页面、RSS、站点地图、结构化数据、评论或构建时路由生成中。任何知道 URL 的人仍可访问和分享;敏感内容请使用身份验证。 ### Type 值 | 值 | 描述 | |---|---| | `Post` | 标准博客文章。显示在首页列表、分类页和搜索中 | | `Page` | 独立页面(例如落地页)。不显示在文章列表中 | ### 使用系列 要将多篇文章归为一个系列,请在这些文章中选择相同的 `series` 值(例如 `Next.js Blog Tutorial`)。博客会在每篇文章详情页渲染系列上一篇/下一篇导航,并提供 `/series/[name]` 页面。 ### Authors 数据库(可选) 如果你希望在 Notion People 名称之外提供更丰富的作者资料,请创建(或使用模板中的)Authors data source。文章会通过 Notion 用户 ID 链接到作者资料行:将作者行的 `people` 属性设置为与文章 `author` 属性中选择的同一个 Notion 用户。 | 字段 | 类型 | 描述 | |---|---|---| | **name** | Title | 显示在作者卡片和作者页上的名称 | | **people** | People | 此资料代表的 Notion 工作区用户。用于将文章匹配到作者资料 | | **role** | Rich text | 职位或角色(例如 `Frontend Engineer`) | | **bio** | Rich text | 简短个人简介 | | **avatar** | Files & media | 头像 | | **email** | Rich text | 邮箱地址 | | **github** | URL | GitHub 主页 URL | | **x** | URL | X(Twitter)主页 URL | | **linkedin** | URL | LinkedIn 主页 URL | | **website** | URL | 个人网站 URL | --- ## 自定义 所有全站设置都在 `src/config/brand.ts` 中: ### 站点信息 ```ts name: "My Blog", title: "A Developer Blog", highlight: "Developer", // 标题中高亮的词 description: "Your blog description.", url: "https://your-domain.com", since: 2025, // 页脚版权起始年份 lang: "en", ``` ### Logo 与 Favicon ```ts logo: { image: "", // Logo 图片路径(相对于 /public)。"" = 仅显示文字 showNameWithLogo: true, // 在 logo 图片旁显示博客名称 png: "/logo.png", // 用于 JSON-LD 和 RSS feed ogWhite: "/logo-white.png", // 用于 OG 图片的白色 logo 叠加层 favicon: "", // 自定义 favicon 路径。"" = 自动生成字母图标 }, ``` ### 颜色 通过 `colors` 对象中的 HSL 值自定义颜色主题。浅色和深色模式都可以配置。每个主题由 5 个基础值构成: - **brand** — 强调色(按钮、链接、焦点环) - **bg** — 页面背景 - **text** — 正文文本 - **surface** — 卡片和弱化区域背景 - **edge** — 边框和输入框描边 ### 字体 ```ts fonts: { sans: { stack: 'Inter, -apple-system, BlinkMacSystemFont, system-ui, sans-serif', }, mono: { family: "JetBrains Mono", cdn: "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap", preconnect: ["https://fonts.googleapis.com", "https://fonts.gstatic.com"], }, og: { family: "Pretendard", url: "https://cdn.jsdelivr.net/.../Pretendard-Bold.otf", }, }, ``` - **sans** — 正文字体栈。非拉丁语言可在前面添加 Web 字体(例如 `'Pretendard, -apple-system, ...'`) - **mono** — 代码块等宽字体,通过 Google Fonts CDN 加载 - **og** — 动态 OG 图片生成使用的字体(`.otf` 或 `.ttf` URL) ### 分类 分类名称 **必须与** Notion Posts 数据库中的 Select 值一致: ```ts categories: [ { name: "Development", slug: "development", color: "orange", icon: "dns", description: "..." }, { name: "Design", slug: "design", color: "teal", icon: "palette", description: "..." }, { name: "Product", slug: "product", color: "green", icon: "work", description: "..." }, ], ``` ### 社交链接 显示在页脚的社交媒体图标链接。将某个值留为 `""` 即可隐藏该图标。 ```ts social: { github: "https://github.com/your-username", twitter: "", instagram: "", facebook: "", youtube: "", linkedin: "", threads: "", tiktok: "", naverBlog: "", }, ``` ### 页脚链接 当前页脚刻意保持简洁,由 `src/components/layout/Footer.tsx` 渲染。它会显示: - Home - About - Template,链接来自 `brand.templateUrl` - 任何 `brand.social` URL 不为空的社交图标 如果需要更大的页脚导航,请编辑 `Footer.tsx`,并保持标签文案位于 `src/config/copy.ts`。 ### SEO ```ts keywords: ["Next.js", "blog", "frontend"], // — 留空 [] 则不输出 organization: { // Google Knowledge Panel 使用的 Organization JSON-LD(可选) name: "Your Company", url: "https://your-domain.com", logo: "/logo.png", // ... address, contactPoint, sameAs, etc. }, ``` ### Giscus 评论 设置 [Giscus](https://giscus.app/) 并填写配置: ```ts giscus: { repo: "your-username/your-repo", repoId: "R_...", category: "Announcements", categoryId: "DIC_...", mapping: "pathname", // 文章如何映射到 discussions strict: "0", // 严格标题匹配 reactionsEnabled: "1", // 显示 reaction 按钮 emitMetadata: "0", // 输出 discussion metadata inputPosition: "bottom", // 评论输入框位置 }, ``` ### Newsletter CTA 将 `enabled` 设为 `true`,即可在首页列表底部显示订阅区。实际订阅逻辑需要你自行实现。 ```ts newsletter: { enabled: false, headline: "Stay ahead of the curve", description: "Join developers receiving the best content...", placeholder: "Enter your email address", cta: "Subscribe", disclaimer: "No spam, unsubscribe anytime.", }, ``` ### 文章动画 ```ts postAnimation: { enabled: true, // 文章详情页标题打字机效果 + slide-up reveal }, ``` ### 行为设置 ```ts postsPerPage: 10, // 每页文章数量 slideshow: { intervalMs: 5000 }, // 置顶文章轮播自动切换间隔(毫秒) reading: { wordsPerMinute: 200 }, // 阅读时间计算(英文 200–250,CJK 500–600) search: { dropdownLimit: 10, // 搜索下拉结果最大数量 pageLimit: 30, // /search 页面最大结果数量 }, ``` ### 缓存 ```ts cache: { revalidate: 1800, // ISR 间隔秒数(默认 30 分钟) feedTtl: 3600, // RSS Cache-Control max-age(默认 1 小时) authorsRevalidate: 300, // 作者缓存重新验证(秒,默认 5 分钟) }, ``` ### Notion 图片代理 Notion 托管文件 URL 会过期,因此上传到 Notion 的文件会通过以下稳定签名 URL 渲染: ```txt /api/notion-image?... ``` 该 API route 会在请求时解析当前 Notion 文件 URL,并使用 CDN 缓存 header 代理文件内容。默认代理缓存 TTL 为 3300 秒,安全低于 Notion 1 小时签名 URL 的过期时间: ```env NOTION_IMAGE_CACHE_SECONDS=3300 NOTION_IMAGE_SIGNING_SECRET=optional-separate-secret ``` 外部图片 URL 会直接渲染。如果设置了 `NOTION_IMAGE_SIGNING_SECRET`,请确保预渲染页面和 `/api/notion-image` 运行的任何位置都使用同一个值;否则构建时生成的签名图片 URL 可能在运行时无法验证。 --- ## 按需重新验证 博客会缓存 Notion 数据以提升性能。当你在 Notion 中更新文章后,可以触发即时刷新: ```bash curl -X POST https://your-domain.com/api/revalidate \ -H "Authorization: Bearer YOUR_TOKEN_FOR_REVALIDATE" ``` 你也可以把它配置为 Notion automation,或来自外部服务的 webhook。如果不触发 revalidation,内容会每 30 分钟自动刷新。 --- ## 部署 ### Vercel(推荐) 1. 将仓库推送到 GitHub 2. 在 [Vercel](https://vercel.com/new) 导入项目 3. 在 Project Settings → Environment Variables 添加环境变量 4. 部署 ### Docker ```bash # 使用 docker compose 构建并运行 docker compose up -d # 或手动构建(静态生成阶段需要 NOTION_API_KEY) docker build -t notion-as-blog \ --build-arg NOTION_API_KEY=your_key \ --build-arg NOTION_DATA_SOURCE_ID=your_data_source_id \ . docker run -p 3000:3000 \ -e NOTION_API_KEY=your_key \ -e NOTION_DATA_SOURCE_ID=your_data_source_id \ -e NOTION_AUTHORS_DATA_SOURCE_ID=your_authors_data_source_id \ -e TOKEN_FOR_REVALIDATE=your_revalidate_token \ -e NOTION_IMAGE_SIGNING_SECRET=your_image_signing_secret \ -e NOTION_IMAGE_CACHE_SECONDS=3300 \ notion-as-blog ``` --- ## 环境变量 | 变量 | 必需 | 描述 | |---|---|---| | `NOTION_API_KEY` | 是 | Notion integration API key | | `NOTION_DATA_SOURCE_ID` | 是 | Notion Posts data source ID | | `NOTION_AUTHORS_DATA_SOURCE_ID` | 否 | Notion Authors data source ID | | `TOKEN_FOR_REVALIDATE` | 否 | 按需 revalidation 的 secret token(`/api/revalidate`) | | `NOTION_IMAGE_SIGNING_SECRET` | 否 | 稳定 Notion 图片代理 URL 的 HMAC secret。未设置时默认使用 `NOTION_API_KEY` | | `NOTION_IMAGE_CACHE_SECONDS` | 否 | 代理 Notion 文件的 CDN cache TTL。上限为 3300 秒 | | `NEXT_PUBLIC_GA_ID` | 否 | Google Analytics measurement ID | --- ## License [MIT](LICENSE)