分享一下自己的基于 Notion + Github Action + Astro + Cloudflare 的全云端博客工作流

我一直使用静态页面生成器来创建我的博客。但是这种方式在形式上和写代码没什么太大的区别,因为这两者都需要坐在电脑前、打开 VSCode 或者别的什么 IDE后才能编写文章,写完后还要推送到 Github 仓库,然后等待 CI/CD 流程自动部署。

在我的工作和 VSCode 深度绑定的现在,打开 VSCode 对我来说已经是一种负担了(笑)。如果说工作时打开 VSCode 是再正常不过的一件事,那么在偏向个人和日常的博客写作场景下再使用 VSCode,就显得有点命苦(笑)。

刚好之前使用 Vuepress 搭的博客有点看腻了,打算换博客框架。借着这个机会,研究一下如何搭建一个心智负担更小的博客工作流。

1 技术选型

首先要明确需求:

  • 我不希望打开 VSCode 或者别的什么 IDE 来写文章;
  • 能随时随地写作;
  • 至少有一处能在我控制之下的文件副本;
  • 免费

其实加上这几条限制之后,能用的产品就不多了。首先仓库托管肯定是 Github,部署肯定是 Cloudflare Pages。

博客框架我从 Vuepress 切换到了 Astro。相较于前者,Astro 的自定义程度更高、主题也更多。同时非常感谢 @Dnkk2 制作的 Litos,简洁、优雅、美观。在 Litos 优秀设计的基础上,我进行了大量的改造,以适配博客结构。

「不使用 VSCode 等 IDE 来写作」这个需求还是很容易满足的,Obsidian、Notion、飞书等都能满足需求。但是 Obsidian 不能同步(同步功能需要付费),尽管有 Github 插件可以实现同步,该插件不能在移动端使用;飞书更偏向企业协作。而 Notion 更偏向于个人的同时,还提供了专用的 SDK 和 API 导出指定的文章。还有一个加分项是它提供了和 ChatGPT 的连接器。结合这些优势,我最终选择了 Notion 作为写作窗口。

2 初步工作流设计

虽然每一步都有现成的轮子可以用,但最大的难题在于如何把轮子拼成车。在经过了三天的研究、试错和开发后,最后得到的工作流是这样的:

  1. 在 Notion 上完成写作;
  2. 触发 Github Action 通过 Notion API 拉取文章;
  3. 在 Github Action 完成语法转换和文件保存,然后提交到 Git 仓库中;
  4. Cloudflare Pages 检测到提交行为,自动编译为静态页面并部署

这里我按照实现难度从低到高的顺序来写。

2.1 自动部署

这一步的难度几乎为零。Cloudflare Pages 完全傻瓜化,只需要点点点,就能把 Github 仓库中的 Astro 应用编译成静态页面发布,实在没什么可讲。

2.2 自动拉取文章

自动拉取文章基本只能靠 Github Action 了。原本的构想是自动、定时拉取。例如,在 Github 仓库根目录下维护一个.notion-sync.yml文件:

- last_sync_time: YYYY-MM-DD hh:mm:ss
- entities:
	- 
		name: alice
		last_edited_time: YYYY-MM-DD hh:mm:ss
	- 
		name: bob
		last_edited_time: YYYY-MM-DD hh:mm:ss

然后读取上次同步时间、再读取数据库中所有文章的最后一次编辑时间、检索最后一次编辑晚于上次同步的文章、再更新文章。

但是这样会存在很多边界问题,例如:

  • 检索到待更新的文章,但是由于各种原因拉取失败怎么办?
  • 更新文章需要时间,如果我在更新的过程中编辑了文章怎么办?

而且我并不是时时刻刻都在写文章的,这也就意味着绝大部分时间定时拉取都在空转。尽管 Github Action 和 Notion API 不要钱,但还是不要浪费这些公共资源比较好。

于是我开始研究手动同步。为了把心智负担降低到最小,我打算每次只指定一篇文章来同步,这样就完全杜绝了上述的边界问题,而且还不浪费,控制细粒度也精细了不少。

这在 Github Action 上是完全可行的,它支持在运行工作流前手动输入变量。大概类似于这样:

on:
	workflow_dispatch:
		inputs:
			pageId:
				description: 'Notion page ID (32 chars without hyphens)'
				required: true

Notion Page ID 很方便就可以得到,就是 URL 的后边跟着的那串 32 位 UUID,当然是去掉了连字符之后的。

然后在仓库里创建一个用于拉取文章的脚本:

jobs:  
	sync:    
		runs-on: ubuntu-latest
		
		steps:
			- ...
      - name: Sync page
				run: pnpm tsx scripts/syncNotion.ts
				env:
					NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
					NOTION_PAGE_ID: ${{ inputs.pageId }}

在 Github Action 运行工作流时就要求输入 Page ID:

于是这一步也打通。

2.3 字段映射

我特别喜欢 Notion 的数据库功能。借助数据库可以统一管理所有文章的字段,也就是所有文章的元数据和对应 Markdown 文件的 Frontmatter。

结合需求,我设计了以下字段并映射:

Notion 数据库字段 Frontmatter 字段 备注
title title 文章标题
description description 文章描述
category 文章分类,用于把文章渲染到不同的页面上
tags tags 文章的标签
completed completed 文章是否已经写完
top top 文章是否置顶
create_time createTime 文章的创建时间
last_edited_time lastEditedTime 文章的最后一次编辑时间
id 文章的UUID

2.4 语法转换

这是最难的一步了。尽管标准 Markdown 语法在各种地方都支持,但是在非标准 Markdown 上,各家的语法就五花八门了。例如我想添加 Callout 块当做提示容器,Notion 直接导出到 Markdown 是这样的:

<aside>
...
<aside>

但是 Litos 主题的语法是这样的:

> [!tip]
> ...

再比如 Litos 的增强代码块:

// title='src/utils/math.ts' ins={2, 5, 8} del={1, 4, 7} mark={10, 11, 13-16}
const add = (a: number, b: number): number => a + b
add(1, 3)

const subtract = (a: number, b: number): number => a - b
subtract(3, 1)

const multiply = (a: number, b: number): number => a * b
multiply(2, 3)

const divise = (a: number, b: number): number => a / b
add(6, 3)

const pow = (a: number, b: number): number => a ** b
add(3, 3)

const mod = (a: number, b: number): number => a % b
mod(10, 3)

Notion 根本不支持。

这一步我选择开源项目 notion_to_md 进行语法转换。借助它的自定义转换器,可以任何将 Notion 块转换为我想要的格式:

function installCustomTransformer(n2m: NotionToMarkdown, notion: Client) {
  n2m.setCustomTransformer('code', async (block: any) => {
    return parseEnhanceCodeBlock(block, n2m)
  })

  n2m.setCustomTransformer('callout', async (block: any) => {
    return parseCalloutBlock(block, n2m, notion)
  })
}

由于 Notion 的 Callout 没有类型的问题,这里将读取第一行作为类型。例如一个 Callout 块的内容是:

// tip 温馨提示
不要在空腹的情况下进食

就会被解析为:

> [!tip] 温馨提示
> 不要在空腹的情况下进食

花了点时间编写这两种转换器,终于是把这一步走通了。

Enjoy it!

3 更进一步

现在的触发方式虽然已经很好,但是这个过程还不够轻量无感:我还需要复制文章 ID、打开 Github、输入 ID 后运行工作流。

最理想的方式应该是借助 Notion 提供的 Button 和 Webhook 功能来做。借助这两个功能以及 Cloudflare Workers,就能实现点击页面上的按钮发布文章,整体更加无感:

  1. 点击 Notion Button,触发 Webhook 发送文章 ID;
  2. Cloudflare Workers 接收到文章 ID,自动开始运行 Github Action Workflow;
  3. 全自动、零代码、低心智负担的自动部署流程。

但是 Webhook 是付费功能(笑),我自然是付不起每个月 12 美元的 Plus 订阅的,这个方案可能只能是一个方案了。

38 个赞

膜拜学习!

1 个赞

太有实力了 :partying_face:

太牛啦!

1 个赞

感谢大佬!

1 个赞

欸,太好了,刚好要把blog迁移到astro

1 个赞

其实跟简单的是keystatic cms。不用适配notion。使用github模式,一站式

3 个赞

谢谢佬分享

1 个赞

学习一下

马克马克

1 个赞

非常有用,感谢,这就迁移

感谢大佬

确实,这套流程挺厉害的 :bili_103:

我趣太完美了 又想折腾博客了:face_holding_back_tears:感谢佬分享

编:今天搓好了,免费还是太香了hhhh

太棒了,最近正在探索相关方案就看到了佬的分享:+1::+1::+1::+1::+1:

感觉有点复杂,佬友泰柚实力啦