Notion で文章を書くのに慣れていても、ブログ側が Astro のような file-based static site だと、執筆と公開の間に小さな断絶ができます。

Notion では段落を並べ、画像を置き、構成を整理するのが楽です。

しかし公開時になると、

  • 本文を repository へ移す
  • frontmatter を補う
  • 多言語版を作る
  • build する
  • push する
  • deployment を待つ

という別作業が始まります。

記事数が少ないうちは問題ありませんが、継続して書くほどこの「機械的な移し替え」が面倒になります。

そこで私は、Notion は執筆入口、Git repository の Markdown は公開成果物と役割を分けました。

GitHub Actions が Notion page を取得し、Markdown へ落とし、必要な metadata と多言語版を作り、Astro の content directory に保存して build、push、production deployment までつなげます。

目的は「何でも自動にすること」ではありません。

手動で繰り返していた搬送作業を減らし、書くことと運用作業の切り替えを少なくすることです。

ブログを file-based にする理由自体は Astro vs WordPress に、repository から直接公開する流れは Markdown + GitHub Actions の公開フロー に整理しています。

この構成は、その二つの間に Notion を執筆 UI として入れる方法です。

最初から複雑な event-driven system にしなかった理由

最初は、Notion で公開した瞬間に webhook / Worker を起動し、repository を更新して deployment まで流す構成も考えました。

技術的にはきれいです。

ただ、個人ブログの第一段階で最も重要なのは speed より壊れたときにどこで止まったか分かることでした。

そのため最初はかなり保守的にしました。

  • 本文は Notion page body に書く
  • page property は title、description、category、tags、slug、status など metadata だけ
  • 一回の workflow で一記事だけ処理
  • 公開 = true かつ 状態 = Ready のページを対象
  • 既存記事更新は同じ slug を維持
  • update 時は明示的に更新許可を付ける

この方が failure を分けやすいです。

記事が queue に入らないなら property を確認する。

翻訳で失敗したなら translation layer。

build で落ちたなら content schema。

deploy が始まらないなら production workflow。

チーム向け CMS ならリアルタイム trigger、多人数 review、複雑な state machine が重要になるかもしれません。

個人ブログで私が欲しかったのは「今夜書いた記事が、既存記事を壊さず、明日には安定して公開されている」ことでした。

その目的なら、単純な manual / polling 型の workflow はむしろ扱いやすいです。

全体の流れ

現在の考え方は次です。

Notion page body
  -> Ready + 公開
  -> GitHub Actions が Markdown を取得
  -> slug / frontmatter を組み立てる
  -> 多言語記事を生成
  -> src/content/posts/<locale>/ に保存
  -> Astro build
  -> main へ push
  -> production deployment
  -> Notion に status / URL / commit SHA を返す

このサイトは現在、

src/content/posts/
├── zh-cn/
├── en-us/
├── zh-tw/
└── ja-jp/

の四言語構成になっています。

同じ記事は全言語で同じ path を使います。

これにより Astro 側の routing、collection、language switch、hreflang を slug ごとに揃えやすくなります。

Notion 側には本文そのものを property へ詰め込まず、page body に普通に書きます。

property は例えば、

  • 名称
  • 摘要
  • 分類
  • 標籤
  • Slug
  • 公開
  • 状態
  • 更新許可

のような metadata に限定します。

Notion は「見た目の自由な CMS」ではなく、快適な Markdown 原稿エディタとして使う

自動同期を安定させたいなら、Notion のすべての visual block をブログで再現しようとしない方が楽です。

私は Notion を「書きやすい Markdown editor」に近い感覚で使っています。

  • 見出しは正しい heading level
  • 本文は普通の paragraph
  • command / config は fenced code block
  • image は関連する段落の近く
  • 重要な external link や video は単独行
  • 複雑な column layout や装飾カードへ依存しない

Notion で見た目がきれいでも、Markdown export したときに同じ構造になるとは限りません。

そのため最初から「export しやすい書き方」に寄せておく方が、自動化全体は安定します。

例えば次の程度で十分です。

# タイトル

最初の 2〜3 段落で、問題と選択理由を説明する。

## 重要なポイント

本文を続ける。

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

その後にトラブルシュート、制約、復旧方法を書く。

https://example.com/some-reference


つまり「Notion の visual page を Astro に完全変換する」のではなく、Notion で既にきれいな source Markdown に近い原稿を書く考え方です。

## スクリプトの責任を分ける

現在の script entry は主に次です。

```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 page を選ぶ
  • 本文 Markdown を取得
  • metadata を検証
  • slug を決める
  • 記事ファイルを作る

ところまで担当します。

その後 build、commit / push、deploy trigger が続きます。

Notion への success / failure 書き戻しを別 command にしているのは、巨大な一つの処理にせず、どこまで成功したかを分けやすくするためです。

自動 publishing で特に危険なのは、翻訳より既存記事の上書き判定です。

新規記事のつもりだったのに自動 slug が既存記事と衝突し、古い記事を更新してしまう方が怖いからです。

そのため私は次を保守的にしています。

  • 新規記事はできるだけ unique slug を明示
  • repository に同 slug があれば default で停止
  • 既存記事更新は元の slug を維持
  • 明示的な update permission があるときだけ overwrite

自動化は「賢く推測して進める」より、曖昧なら止まる方がブログでは安全です。

AI / Translation layer を信用しすぎない

この公開 pipeline では、AI は便利な加工レイヤーですが、最終的な整合性の責任は持たせません。

OpenAI-compatible gateway は実装によって挙動が違います。

例えば過去には、

  • HTTP 200 だが本文が空
  • non-streaming では空だが streaming では返る
  • schema を渡しても field name が期待と違う

といったケースがありました。

そのため translation / metadata generation には fallback が必要です。

ただし、より重要なのは AI の出力をそのまま production truth にしないことです。

最終的な硬い境界は、

  • content schema
  • slug collision protection
  • build
  • Git history
  • deployment validation

に残します。

モデルは title、description、translation を助けますが、repository のルールを超えて勝手に公開できる設計にはしません。

Dry run を先に使う

特定の Notion page を試すとき、いきなり本番 publish しない方が楽です。

workflow へ page_id を指定し、dry_run を使います。

この状態では、

  • Notion page を取得
  • 記事ファイルを生成
  • translation / localization
  • Astro build

までは実行しますが、main へ push せず、production state も変更しません。

この一段があるだけで、Notion property、Markdown export、translation、content schema の問題を deployment 前に分けられます。

実運用で守っている最小ルール

全部覚えなくても、次だけ固定するとかなり安全です。

  • 本文は Notion page body に書く
  • metadata は property
  • 新規記事は unique slug を明示する
  • 既存記事更新では slug を変えない
  • update は明示的に許可する
  • source language を公開可能な状態まで整えてから localization する
  • error 時は最後の exit 1 ではなく、最初に失敗した step を見る
  • build を deployment の gate にする

この workflow は「最も高度な CMS」を作るためのものではありません。

Notion の書きやすさ、Astro の file-based content、GitHub Actions の反復作業自動化をつなぎ、日常的に使える程度まで摩擦を下げるための仕組みです。

私にとっては、管理画面にボタンを増やすより、この方が長く使いやすいです。