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

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

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

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

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

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

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

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

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

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 寫法,大概就是這種感覺:

# 标题

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

## 某个重点

继续写正文。

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

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

https://example.com/some-reference

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

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

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

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 則把中間那段重複勞動吞掉了。

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