跳至正文

在 2022 年使用 Notion 管理博客文章

在过去的一周内,我完全重写了我的博客(也就是您正在访问的这个站点!)的前端,其中利用了 Notion 最新的官方 API 访问我管理在 Notion 上的文章。事实上,这个站点上的所有博客文章都是从如下图所示的 Notion 数据库中拉取的:

在本文中,我将向您介绍以下几点内容:

  • 我现在撰写一篇新的博客文章的工作流;
  • 使用 Notion 官方 API 访问博客文章的一些并不深入的技术细节;
  • 现阶段 Notion 官方 API 的一些不足。

撰文工作流

撰写一篇新的博客文章的工作流非常简单。只需要在前文图中所展示的 Notion 数据库中新建一个条目,并填写相应的列即可。在新建一个条目后,Notion 会自动为这个条目创建一个页面(Page)。在这个页面中即可撰写文章内容,并可以利用所有 Notion 提供的富文本支持(图片、链接、代码片段等),如下图所示。

当文章的所有内容撰写完毕后,只需要回到 Notion 数据库界面并勾选 published 列中的勾选框即可。前端服务器将会定期(目前是每 5 分钟)从 Notion 中拉取所有已发布的文章并进行渲染。渲染全部在服务端进行。

Next.js & Vercel

新的博客应用采用 Next.js 前端框架进行编写。Next.js 是一个基于 React 的前端框架,非常简单易学,如果您已经掌握 React,那么您将很容易基于 Next.js 设计新的前端应用程序。Next.js 鼓励在应用编译时直接生成静态页面,但同时也支持服务端渲染(SSR)、客户端渲染(CSR)、混合构建(即应用的一部分在构建时直接生成静态页面、剩余部分采用 SSR 或 CSR 在运行时动态渲染)和增量构建(ISR,即在运行时动态生成可供缓存的静态页面)。利用 Next.js 框架,新的博客应用在构建时将拉取最新的部分文章(目前是 5 篇文章)执行静态渲染,并增量构建剩余的文章。另外,主页、archive 页、友链页等可以静态构建的页面均采用静态渲染,以提升访问速度。

Vercel 是专为 Next.js 设计的应用托管平台。Vercel 可以自动拉取存放 Next.js 应用源代码的 GitHub 仓库,并在构建后托管应用的运行环境。新的博客应用就是托管在 Vercel 平台上的。

Notion Official API

2021 年 Notion 官方发布了一组 beta API 用于外部应用通过 Notion Integrations 访问托管在 Notion 平台上的内容。这一组 beta API 经过了若干轮迭代,目前的最新版本是在 2021 年 8 月 16 日发布的 2021-08-16 版本。你可以在 Notion Developers 网站上找到 Notion beta API 的文档。随着 Notion beta API 一同发布的还有 Notion JavaScript SDK。这是一个 npm 包,提供了 Notion beta API 的 JavaScript binding 以及 TypeScript 定义。

内容的组织结构

Notion 将其托管的所有内容分为三个层级进行管理:databasepage 以及 block。本文的第一张插图中展示的就是一个 database,一个 database 是多个 page 的集合,每个 page 对应于 database 中的一行。本文的第二张插图中展示的则是一个 page,一个 page 是多个 block 的集合。一个 block 负责管理一块具体的富文本内容,例如一个自然段、一个插图、一个小节标题等。在 Notion 的编辑器中用户可以对单个 block 进行移动。下图展示了在 Notion 编辑器中如何对一个表示一个自然段的 block 进行移动。

Notion 支持大量不同种类的富文本内容,因此 block 也有许多种类,分别对应这些不同种类的富文本内容。目前最新的 Notion API 支持 32 种不同的 block,详细情况可以参考 Notion API 的官方文档。

Block 可以有 child block。例如,对于一个多级列表,表示更深一级列表项的 block 便是表示浅一级列表项的 block 的 child block。因此,表示一个 page 的所有内容需要若干 block tree,每个 block tree 的根是 page 中的各个顶层 block。

渲染

由于 block 的设计原因,在渲染一个 page 中的所有 block tree 前需要对这些 block tree 进行些许预处理。目前需要预处理的部分主要有两点。

一是对节标题的预处理。出于语义化考虑,HTML specification 推荐在博客文章、新闻文章等场景中使用 <section> 标签标记文章中的各个小节。例如,在标记前,一篇文章的结构如下:

<h1>Section 1</h1>
<p>...</p>
<h2>Section 1.1</h2>
<p>...</p>
<h2>Section 1.2</h2>
<p>...</p>

在标记后,HTML 结构如下:

<section>
  <h1>Section 1</h1>
  <p>...</p>
  <section>
    <h2>Section 1.1</h2>
    <p>...</p>
  </section>
  <section>
    <h2>Section 1.2</h2>
    <p>...</p>
  </section>
</section>

在上面的例子中,将被渲染为 <h1><h2><p> 标签的 block 处于 block tree 上的同一个层级,因此不经预处理直接渲染将难以知悉 section 的结构。在预处理时,需要将每个表示节标题的 block 与它后续的所有 block 进行合并预处理,直到到达另一个相同或更高层级的节标题 block 为止。

二是对有序或无序列表的预处理。在设计上,Notion 没有提供一个表示整个列表的 block,而是将每个列表项单独表示为一个 block。因此在渲染前需要将这些连续的列表项 block 进行合并预处理,方便后续渲染。

在进行预处理后,渲染的工作便简单起来。只需要对每一种 block 依次渲染为对应的 HTML 即可。

当前 Notion Official API 的局限

当前 Notion API 处于 beta 阶段,还有许多局限,包括:

  • Rate limit 限制为 3 个请求 / 秒,当需要预渲染的页面较多较复杂时,前端应用构建速度可能会因此受到显著影响;
  • Notion API 的分页机制不支持随机访问,因为访问下一页的内容需要使用访问上一页时返回的一个 token;(学到区块链的精髓了属于是)
  • Notion API 的文档有不少细节上的错误;
  • Notion JavaScript SDK 所附带的 TypeScript 类型定义包含数万行,究其原因是几乎所有的类型定义都被内联为 literal 类型,这极大地影响了采用 TypeScript 编写应用的便捷性(因此我几乎重新定义了所有 Notion 提供的数据类型)。
标签:

发表回复