
- Development
- Web
Notion 作為一款出色的筆記軟體,受到許多人的喜愛。在它提供的眾多功能中,有一項特別引人注目,那就是強大的資料庫功能。
既然 Notion 的資料庫能在 Title 屬性裡儲存頁面,那我們何不來嘗試利用這項特性把它變成部落格的 CMS (Content management system) 呢?
流程
- 建立 Notion 資料庫,設定屬性
- 利用 Notion Integrations 和 Notion SDK 取得資料
- 透過 GitHub Actions 自動同步並更新網站文章
實際操作
Notion Database
首先,我們需要先建立一個資料庫,並且設定基本屬性
完成之後,前往 Notion Integrations 新增 Integration,必須勾選的只有 Read content,記得 Workspace 要選擇方才建立資料庫的那個
接著,回到一開始的資料庫頁面,在設定裡連結 Integration
這樣就完成 Notion 部分的設定了
TypeScript 腳本
接下來,我們要使用 TypeScript 來撰寫自動讀取文章並將其解析成 Markdown 的腳本
依賴列表,請自行安裝
- @notionhq/client
- notion-to-md
- yargs
首先,引用並初始化等等會用到的模組
import { Client } from ‘@notionhq/client’;
import { NotionToMarkdown } from “notion-to-md”;
import * as fs from ‘fs’;
const yargs = require(‘yargs/yargs’);
import { hideBin } from ‘yargs/helpers’;
const argv = yargs(hideBin(process.argv)).argv;
const notionClient = new Client(
{ auth: argv[‘notion-secret’] }
);
const notionToMarkdown = new NotionToMarkdown(
{ notionClient: notionClient }
);
接著,從 Notion 資料庫取得資料
async function fetchBlogPosts(databaseId: string): Promise<void> {
let data = await notionClient.databases.query( // 從資料庫取得所有資料
{
database_id: databaseId
}
);
data.results.forEach(async (rawPostData) => { // 讀取每一筆資料
let postData = JSON.parse(JSON.stringify(rawPostData)); // 解析資料
if (!postData.properties.Public.checkbox) { // 文章不是 Public 時跳過
return;
}
});
}
之後利用 notion-to-md 將資料解析為 Markdown
async function fetchBlogPosts(databaseId: string): Promise<void> {
let data = await notionClient.databases.query(
{
database_id: databaseId
}
);
data.results.forEach(async (rawPostData) => {
let postData = JSON.parse(JSON.stringify(rawPostData))
if (!postData.properties.Public.checkbox) {
return;
}
let rawMarkdown = await notionToMarkdown.pageToMarkdown(postData.id); // 將頁面資料解析成 Markdown
// 利用 string.replace() 修復 Toggle list 和 Double space line break 無法正常顯示的問題
let markdownString = notionToMarkdown.toMarkdownString(rawMarkdown).replace(/^ \<\/details\>$/mg, ”</details>”).replace(/^ $/mg, “<br/>”);
});
}
為了要讓部落格讀懂這個文章的屬性(標題、簡介、封面圖等),我們需要把剛剛在資料庫裡設定的屬性解析成 Markdown 的 Frontmatter
function parseMarkdownFrontmatter( // 將資料轉換為 Frontmatter 格式並回傳字串
title: string,
description: string,
thumbnail: string,
createdAt: string,
lastEditedAt: string,
tags: Array<string>
): string {
return `---
layout: ”../../layouts/BlogPost.astro”
title: ”${title}”
description: ”${description}”
thumbnail: ”${thumbnail}”
createdAt: ”${createdAt}”
lastEditedAt: ”${lastEditedAt}”
tags: ${JSON.stringify(tags)}
---\n`
}
function parsePostTags(rawTags: Array<Object>): Array<string> { // 將 Notion 的 Tags 處理成 Frontmatter 接受的格式
let result: Array<string> = [];
rawTags.forEach((rawTag) => {
result.push(
JSON.parse(JSON.stringify(rawTag)).name
);
});
return result;
}
async function fetchBlogPosts(databaseId: string): Promise<void> {
let data = await notionClient.databases.query(
{
database_id: databaseId
}
);
data.results.forEach(async (rawPostData) => {
let postData = JSON.parse(JSON.stringify(rawPostData));
if (!postData.properties.Public.checkbox)
return;
}
let rawMarkdown = await notionToMarkdown.pageToMarkdown(postData.id);
let markdownString = notionToMarkdown.toMarkdownString(rawMarkdown).replace(/^ \<\/details\>$/mg, ”</details>”).replace(/^ $/mg, “<br/>”);
let markdownFrontmatter = parseMarkdownFrontmatter( // 將資料傳入 Frontmatter 處理函式
postData.properties.Title.title[0].plain_text,
postData.properties.Description.rich_text[0].plain_text,
postData.properties.Thumbnail.files[0].external.url,
postData.created_time,
postData.last_edited_time,
parsePostTags(postData.properties.Tags.multi_select) // 將 Tags 傳入處理函式
);
});
}
最後,串接 File system 來儲存處理完成的 Markdown 文件並呼叫處理函式
async function fetchBlogPosts(databaseId: string): Promise<void> {
console.log(“Cleaning posts folder…”);
fs.rmSync(__dirname + `/../src/pages/posts/`, { recursive: true, force: true });
fs.mkdirSync(__dirname + `/../src/pages/posts/`, { recursive: true });
console.log(“Fetching Notion data…”);
let data = await notionClient.databases.query(
{
database_id: databaseId
}
);
data.results.forEach(async (rawPostData) => {
let postData = JSON.parse(JSON.stringify(rawPostData));
console.log(`Parsing post ${postData.id}…`);
if (!postData.properties.Public.checkbox) {
console.log(`Post ${postData.id} is private, skipping…`);
return;
}
let rawMarkdown = await notionToMarkdown.pageToMarkdown(postData.id);
let markdownString = notionToMarkdown.toMarkdownString(rawMarkdown).replace(/^ \<\/details\>$/mg, ”</details>”).replace(/^ $/mg, “<br/>”);
let markdownFrontmatter = parseMarkdownFrontmatter(
postData.properties.Title.title[0].plain_text,
postData.properties.Description.rich_text[0].plain_text,
postData.properties.Thumbnail.files[0].external.url,
postData.created_time,
postData.last_edited_time,
parsePostTags(postData.properties.Tags.multi_select)
);
console.log(`Writting post ${postData.id}…`);
fs.writeFileSync(__dirname + `/../src/pages/posts/${postData.id}.md`, markdownFrontmatter + markdownString);
});
}
fetchBlogPosts(argv[‘database-id’]);
這樣 TypeScript 腳本的部分也完成了!我們已經可以利用編譯出的 JavaScript 來抓取文章了
node .\sync-notion-posts.js —notion-secret <secret> —database-id <dbid>
設定 GitHub Actions
既然都做到這了,不如連最後一哩路也自動化吧!下面將使用 GitHub Actions 來定時同步文章
因為 GitHub Actions 需要較長的篇幅來解釋,所以先直接提供可以使用的範本,有機會再詳細介紹 GitHub Actions 的使用
name: Sync Posts
on:
schedule: [{cron: “0 */6 * * *”}]
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-node@v3
with:
node-version: 16
- name: Install dependencies
run: yarn
- name: Sync Notion posts
run: |
node ./tools/sync-notion-posts.cjs —notion-secret ${{ secrets.NOTION_SECRET }} —database-id ${{ secrets.DATABASE_ID }}
- name: Commit files
run: |
git config —local user.email “41898282+github-actions[bot]@users.noreply.github.com”
git config —local user.name “github-actions[bot]”
git commit -m “Sync posts at ${{ github.event.repository.updated_at }}” -a
- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref }}
結語
至此我們就完成 Notion CMS 的設計與部屬了!現在你隨時都能透過 Notion 新增、編輯文章,並且透過 GitHub Actions 完成遠端同步,上傳和編輯文章再也不用把整個專案下載到本地了,整個體驗流暢了許多