Notion同期のSlugと公開タグをGemini 3.1 Flash-Liteに任せた
Notionで書いたメモをブログへ同期する仕組みを改善した。今回手を入れたのは、記事URLに使うSlugと、ブログに表示するタグの決め方だ。
これまでは記事タイトルやNotion上のタグを、そのまま同期処理へ持ち込もうとしていた。しかし、日常のメモとブログ記事を同じデータベースで管理していると、この方法は思ったより扱いづらい。
日常のメモと公開記事が同じ場所にある
Notionでは、Allの中にMemosとTagsのデータベースを置いている。思いついたことはMemosへ入れ、書きかけのものにはINBOXを付ける。ブログに出したい記事にはStudy.Logも付ける、という運用だ。
ここへPublishedのようなタグやStatusを追加する案も考えた。しかし、Memosには日記や一時的なメモも大量に入っている。その中でブログ専用の公開状態を持たせると、普段のメモ管理に別の都合が混ざってしまう。
そこで役割を次のように分けた。
Study.Logはブログ同期を開始するためのトリガーINBOXは書きかけを表す内部タグとして残す- ブログに表示する分類タグはAIが別途選ぶ
- Slugは初回同期時に生成し、その後は固定する
公開状態を増やすのではなく、既存の運用に同期処理を合わせる方針にした。
公開タグはTags DBの許可リストから選ぶ
AIに自由にタグを作らせると、表記揺れや似たタグが増えていく。そこでTags DBへブログ表示というチェックボックスを追加した。
Geminiへ渡す候補は、このチェックが有効なタグだけに限定する。さらにStudy.LogとINBOXは、チェック状態に関係なくコード側でも除外する。アーカイブ済みのタグも候補に含めない。
AIの役割はタグを発明することではなく、管理済みの候補から記事に合うものを0〜4個選ぶことだ。既存タグでは表現できない場合だけ、新しいタグ名を提案させる。その提案はGitHub ActionsのSummaryへ出すだけで、Notionには自動追加しない。
この「候補は人間が管理し、選択だけAIに任せる」という境界が、運用を安定させるうえで重要だった。
GeminiにはSlugとタグをまとめて生成させる
モデルにはgemini-3.1-flash-liteを使った。当初予定していたgemini-2.5-flash-liteは、新しいAPIプロジェクトから利用すると404になったため、Googleが後継として案内している安定版へ更新した。新規記事のタイトル、Markdown本文、公開可能なタグ一覧を渡し、次の形式で返させる。
{
"slug": "gemini-powered-notion-blog-sync",
"publicTags": ["notion", "astro"],
"newTagSuggestions": []
}
出力はJSON Schemaで固定する。ただし、形式がJSONだからといって、その値をそのまま信用はしない。
同期処理側でも次の検証を行う。
- Slugが英小文字・数字・ハイフンだけで構成されているか
- Slugが3〜60文字に収まっているか
- 既存記事のSlugと重複していないか
- 公開タグが許可リストに存在するか
Study.LogやINBOXが混入していないか- タグ数や新規タグ候補数が上限以内か
本文中に命令のような文章が含まれていても、記事データとして扱うようプロンプトにも明記した。
AIは初回同期時だけ呼び出す
Slugを同期のたびに生成すると、記事タイトルを少し変えただけでURLまで変わってしまう。これは避けたい。
新規記事を初めて同期したときだけGeminiを呼び出し、確定した値をMarkdownのfrontmatterへ保存する。
notionId: "NotionのページID"
slug: "gemini-powered-notion-blog-sync"
tags: ["notion", "astro"]
2回目以降はnotionIdで既存記事を見つけ、保存済みのSlugとタグを再利用する。Notion上でタイトルを変更しても、ファイルやURLは増えない。
既存のブログ記事についても、現在のファイル名をSlugとして維持する。今回の改善を導入しただけで過去記事のURLが変わらないようにした。
Geminiが失敗しても同期は止めない
外部APIを同期の必須条件にすると、一時的な障害だけで記事更新全体が止まってしまう。
Geminiの呼び出しにはタイムアウトを設定し、失敗時は一度だけ再試行する。それでも成功しない場合は、タイトルとNotion IDから決定的なSlugを生成し、公開タグは空のまま同期を続ける。
AIは品質を上げるために使うが、同期処理そのものの可用性はAIへ依存させない設計にした。
実際の同期フロー
最終的な流れは次のようになった。
- Memosから
Study.Log付きのページを取得する - 既存記事なら保存済みのSlugと公開タグを再利用する
- 新規記事なら本文をMarkdownへ変換する
- Tags DBから
ブログ表示が有効なタグを取得する - GeminiがSlugと公開タグを構造化出力する
- アプリ側で形式、許可タグ、重複を検証する
- frontmatter付きMarkdownを保存する
- GitHub Actionsが変更をコミットする
実装はPull Request #16で行った。
まとめ
今回の改善で一番大きかったのは、Notionとブログで同じ「タグ」という言葉を使いながら、役割を分離したことだった。
Notionのタグは日常の整理や同期の合図として使う。ブログのタグは読者向けの分類として使う。その間をGeminiがつなぐ。ただし、AIが選べる範囲は人間が管理し、生成結果は初回に固定する。
この形なら、INBOXを中心にした普段のメモ運用を崩さず、ブログ側では安定したURLとタグを持てる。この記事自体をStudy.Log付きで作成し、新しい同期フローの最初のテストにしてみる。