# Notion で書いた記事を Astro ブログへ自動公開するワークフロー

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

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](/ja/posts/astro-vs-wordpress-markdown-github-actions-blog/) に、repository から直接公開する流れは [Markdown + GitHub Actions の公開フロー](/ja/posts/markdown-blog-publish-workflow/) に整理しています。

この構成は、その二つの間に 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 はむしろ扱いやすいです。

## 全体の流れ

現在の考え方は次です。

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

このサイトは現在、

```text
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 しやすい書き方」に寄せておく方が、自動化全体は安定します。

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

```text
# タイトル

最初の 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 の反復作業自動化をつなぎ、日常的に使える程度まで摩擦を下げるための仕組みです。

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