Post Thumbnail b62ee253-fb37-4dea-9fdd-576ca4a19e27
Notion CMS
利用 Notion 來管理部落格上的文章

發布於 2022/9/24 • 最後修改於 2022/9/27

Notion 作為一款出色的筆記軟體,受到許多人的喜愛。在它提供的眾多功能中,有一項特別引人注目,那就是強大的資料庫功能。

既然 Notion 的資料庫能在 Title 屬性裡儲存頁面,那我們何不來嘗試利用這項特性把它變成部落格的 CMS (Content management system) 呢?

流程

  1. 建立 Notion 資料庫,設定屬性
  2. 利用 Notion Integrations 和 Notion SDK 取得資料
  3. 透過 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 完成遠端同步,上傳和編輯文章再也不用把整個專案下載到本地了,整個體驗流暢了許多