# 如何在 Notion 寫文章，再自動同步到靜態部落格

> Canonical: https://vxi.me/zh-tw/posts/notion-to-astro-auto-publish-workflow/
> Locale: zh-tw
> Published: 2026-04-10
> Updated: 2026-04-10
> Markdown: https://vxi.me/zh-tw/posts/notion-to-astro-auto-publish-workflow.md

如果你平時已經習慣在 Notion 裡寫東西，但部落格又是 Astro 這種檔案化靜態站，遲早會碰到一個挺煩的小斷層。

寫稿的時候你在 Notion 裡很順，整理結構、拖圖片、改段落都方便。可一到釋出，你又得把正文搬回儲存庫，補 frontmatter，生成英文版，跑建置，再 push，再等部署。前面像在寫作，後面又突然切回了維運模式。文章少的時候還能忍，發得一多，這種重複動作就會越來越礙事。

我後來把這件事收成了一條固定鏈路：文章繼續在 Notion 裡寫，GitHub Actions 定時或手動去拉頁面，把正文落成儲存庫裡的 Markdown，自動補 slug、生成英文稿，寫回 Astro 內容目錄，本地規則照樣校驗，最後再把生產部署接上。這樣做不是為了追求“全自動”這三個字，而是因為我越來越不想重複做那段機械搬運。

如果你還沒把部落格切到檔案化內容，可以先看這篇 [Astro 部落格 vs WordPress：為什麼我現在更偏向 Markdown + GitHub Actions 這套內容架構](/posts/astro-vs-wordpress-markdown-github-actions-blog/)。如果你現在本來就是在儲存庫裡直接寫 Markdown，那篇 [本地寫 Markdown，git push 後自動上線：我的雙語部落格釋出流程](/posts/markdown-blog-publish-workflow/) 也可以一起看。前者解決“為什麼要走這條路”，後者解決“純儲存庫寫作時怎麼發”，而我這篇更像是在補中間那層：既想保留 Notion 的寫作手感，又不想把 Astro 部落格重新拉回重後台。

這條鏈路最早我也想過直接做成事件驅動。Notion 一發佈，馬上觸發 Worker，再寫儲存庫，再部署，聽起來當然更完整。但真開始往下拆時，我反而把第一版收得很土。原因很簡單，對個人部落格來說，第一版最值錢的不是“快”，而是“壞了以後我能不能一眼看出壞在哪”。所以我沒先上 webhook，也沒做多篇併發，而是老老實實地把規則定成這樣：正文直接寫在 Notion 頁面正文裡，頁面屬性只放標題、摘要、分類、標籤、Slug、狀態這些元資訊，GitHub Actions 每次只處理一篇，預設認 `釋出 = true` 且 `狀態 = Ready`，如果是改舊文，就保留原 slug，再顯式勾上“允許更新已釋出文章”。

這一套看起來不高階，但非常容易排。文章沒進佇列，十有八九是欄位沒勾對；生成英文失敗，就去看翻譯那一層；build 掛了，就回到儲存庫裡的內容 schema；部署沒起，就看後面的 `Deploy Production`。我後來越來越確認，個人部落格和團隊內容系統真的不是一回事。團隊更在意即時觸發、多人稽核、複雜狀態流；我現在更在意的是“今晚寫完一篇，明天早上它能不能穩穩出現在站上，而且別把舊文章撞掉”。在這個目標下，輪詢一點也不丟人，反而很穩。

而這條 Notion -> Astro 鏈路真正跑起來，其實也就下面這一條：

```text
Notion 页面正文
  -> 标记为 Ready + 发布
  -> GitHub Actions 拉 markdown
  -> 自动生成英文标题、description、正文和 slug
  -> 写入 src/content/posts/zh-cn 与 en-us
  -> 执行 build 校验
  -> push 到 main
  -> 显式触发生产部署
  -> 回写 Notion 状态、链接和提交 SHA
```

我最在意的其實就兩件事。第一，Notion 只是寫作入口，不是最終釋出產物，真正上線的文章還是儲存庫裡的 Markdown 檔案。第二，這條鏈路不能因為“自動化”三個字，就把覆蓋保護做沒了。所以儲存庫裡最終還是一個很樸素的結構：

```text
src/content/posts/
├── zh-cn/
│   └── notion-to-astro-auto-publish-workflow.md
└── en-us/
    └── notion-to-astro-auto-publish-workflow.md
```

同一篇文章的中英文共用一個 `path`，這樣 Astro 這一層的內容模型、URL 和聚合頁邏輯都不用臨時打補丁。Notion 那邊要補全的也不是一大堆東西，核心無非就是 `名稱`、`摘要`、`分類`、`標籤`、`Slug`、`釋出`、`狀態`、`自動翻譯英文` 和 `允許更新已釋出文章` 這些欄位。正文則繼續老老實實寫在頁面正文裡，不塞到某個屬性裡。這一點看起來像廢話，實際上挺關鍵。因為我現在腳本取的是頁面 markdown，不是自己去拼 block 樹。正文只要能被 Notion 正常匯出，後面進部落格內容目錄就會穩定很多。

如果你問得再具體一點，其實很多人真正想知道的是：那我在 Notion 裡到底要怎麼寫，後面自動同步到部落格時才不會亂。我的經驗是，儘量把 Notion 當成一個更舒服的 Markdown 編輯器，而不是一個花哨的排版工具。也就是說，標題就老老實實用標題層級，正文就是正常段落，命令和設定一定放程式碼塊，圖片就放在它對應的段落附近，外鏈和影片如果你後面希望部落格能自動轉成卡片或嵌入，最好單獨佔一行。你越依賴 Notion 那些“看起來很漂亮但匯出形態不穩定”的東西，比如複雜列布局、過多摺疊、很重的視覺卡片，後面同步時越容易額外返工。

我現在自己在 Notion 裡寫部落格，腦子裡其實一直有一個“可匯出寫法”。開頭兩三段先把問題和為什麼這樣做說清楚，中間需要命令、設定、目錄結構時直接放 fenced code block，需要圖片就插在相關段落旁邊，參考資料和影片連結如果重要就單獨放一行。你會發現，這樣寫出來的頁面，雖然還在 Notion 裡，但它本質上已經很接近一篇結構乾淨的 Markdown 文章了。這樣一來，後面的“自動同步到靜態部落格”就更像是在搬運一篇已經寫好的源稿，而不是把一個很花的 Notion 頁面硬翻譯成部落格頁面。

一個最保守、也最不容易翻車的 Notion 寫法，大概就是這種感覺：

````text
# 标题

开头两三段，把问题和选择说清楚。

## 某个重点

继续写正文。

```bash
pnpm build
node scripts/notion-auto-publish.mjs process
```

再往下补排错、限制、恢复路径。

https://example.com/some-reference
````

這種寫法的關鍵不是“規範”，而是它順著匯出鏈路來。你不是在賭同步腳本能不能把 Notion 裡的所有視覺細節都復原，而是在一開始就按最容易匯出、最容易落到部落格內容目錄的方式去寫。這個思路一旦轉過來，Notion 寫文章自動同步到靜態部落格這件事，實際會順很多。

## 腳本怎麼拆：三個命令各管什麼

腳本入口我也沒放得很散，主要還是這三個命令：

```bash
node scripts/notion-auto-publish.mjs process
node scripts/notion-auto-publish.mjs finalize-success
node scripts/notion-auto-publish.mjs finalize-failure
```

真正幹活的是 `process`。它先挑出一篇符合條件的 Notion 頁面，拉正文，再去生成英文版，然後把 frontmatter 和正文寫進儲存庫內容目錄。等 build 過了，再走 commit、push 和部署觸發。成功和失敗的回寫則拆到後面兩步去做，這樣每一段職責都比較乾淨，不會變成一個幾百行全塞在一起的大腳本。也正因為這樣，我後來越來越覺得，這條鏈路裡最值錢的反而不是“自動翻譯”，而是它終於不那麼容易把舊文章撞掉了。發文一旦變成自動化，最大的風險通常不是“多點了幾下按鈕”，而是“你以為自己在發新文，系統以為你在改舊文”。尤其標題差不多、slug 又是自動生成的時候，這個坑非常現實。

所以我後來把規則定得很死：新文章最好手工給一個唯一 slug；如果儲存庫裡已經有同 slug 檔案，預設直接失敗；只有明確勾了“允許更新已釋出文章”，才允許覆蓋。換句話說，這條鏈路預設就是偏保守的。它寧可報錯，也不幫你“猜”到底是不是該更新舊文。我現在反而很喜歡這個保守，因為寫部落格不像跑臨時腳本，線上文章被誤覆蓋一次，後面收拾會比重新發一遍麻煩得多。

另外還有一個挺現實的問題，是第三方 OpenAI 相容閘道器並不總是老實。我之前遇到過返回 200 但正文是空的，非流式介面沒內容但流式反而正常，或者 schema 看起來接上了，欄位名卻偏掉的情況。也因為這樣，腳本里後來補了好幾層回退。不是為了寫得花，而是因為一旦把“生成英文”和“生成 slug”塞進自動發文鏈路，這一層就不能太脆。說白了，我現在不是把模型當成一個絕對可信的發文器，而是把它當成這條鏈路裡一個可替換的加工節點。它能幫我補英文版、補標題和 description，但儲存庫 schema、建置校驗和覆蓋保護這些更硬的邊界，還是得留在自己手裡。

## 真正要守住的邊界

真正開始跑以後，我覺得最容易出問題的不是正文，而是邊界沒想清楚。比如你以為自己更新舊文，結果順手換了 slug；你以為正文寫在屬性裡也能發，結果腳本根本不讀；你以為 `Published` 意味著伺服器一定部署完成了，但現在這版實際上表示“內容鏈路已經成功推進到觸發生產部署”；又或者你以為自動發文以後就不需要手動測試了，結果 build 一掛，問題還是要回到儲存庫裡看。

還有一種很常見的誤區，是把“在 Notion 寫文章”理解成“在 Notion 裡怎麼排版都行，後面反正會自動變成部落格”。這件事至少在我現在這條鏈路裡還不是這樣。更準確的說法應該是：你是在 Notion 裡寫一份更舒服的 Markdown 源稿，只不過編輯器從本地檔案換成了 Notion。這個理解一旦對了，很多動作都會自然很多。你不會再執著於把每個塊都做得像海報，而會更在意標題層級有沒有亂、程式碼塊有沒有包對、圖片是不是放在合適位置、重要連結是不是應該獨佔一行。反而是這種“先按可匯出的方式寫”，最後會讓自動同步真正變得省心。

所以我現在更推薦的除錯方式一直很保守。要測某一篇頁面，先手動跑 workflow，把 `page_id` 指過去，再開 `dry_run`。這樣它會把 Notion 拉下來，生成英文版，寫進臨時工作區再 build，但不會真的 push，也不會改線上。先把這一步跑通，再讓它進正式釋出，排錯會輕鬆很多。

如果只想記最少的幾條規則，其實也就這幾條：正文寫在 Notion 頁面正文，不寫在屬性；新文章最好手工填 slug；更新舊文時保留原 slug，並顯式允許更新；先把中文原稿寫到可髮狀態，再讓腳本生成英文；真出錯時別隻看最後一個 `exit 1`，往上看是哪一環先失敗。

這也是為什麼我最後會把這套東西定成現在這個樣子。它不是一個“最先進”的部落格系統，更像是一條足夠清晰、足夠穩、也足夠貼近我自己寫作習慣的內容鏈路。Notion 繼續負責我最順手的寫稿動作，Astro 繼續負責我最想保留的檔案化釋出，而 GitHub Actions 則把中間那段重複勞動吞掉了。

對我來說，這比再多一個後台按鈕有用得多。
