hamakyo.log
open main menu

Notion同期のSlugと公開タグをGemini 3.1 Flash-Liteに任せた

/ 9 min read
last update date:

Notionで書いたメモをブログへ同期する仕組みを改善した。今回手を入れたのは、記事URLに使うSlugと、ブログに表示するタグの決め方だ。

これまでは記事タイトルやNotion上のタグを、そのまま同期処理へ持ち込もうとしていた。しかし、日常のメモとブログ記事を同じデータベースで管理していると、この方法は思ったより扱いづらい。

日常のメモと公開記事が同じ場所にある

Notionでは、Allの中にMemosTagsのデータベースを置いている。思いついたことはMemosへ入れ、書きかけのものにはINBOXを付ける。ブログに出したい記事にはStudy.Logも付ける、という運用だ。

ここへPublishedのようなタグやStatusを追加する案も考えた。しかし、Memosには日記や一時的なメモも大量に入っている。その中でブログ専用の公開状態を持たせると、普段のメモ管理に別の都合が混ざってしまう。

そこで役割を次のように分けた。

  • Study.Logはブログ同期を開始するためのトリガー
  • INBOXは書きかけを表す内部タグとして残す
  • ブログに表示する分類タグはAIが別途選ぶ
  • Slugは初回同期時に生成し、その後は固定する

公開状態を増やすのではなく、既存の運用に同期処理を合わせる方針にした。

公開タグはTags DBの許可リストから選ぶ

AIに自由にタグを作らせると、表記揺れや似たタグが増えていく。そこでTags DBへブログ表示というチェックボックスを追加した。

Geminiへ渡す候補は、このチェックが有効なタグだけに限定する。さらにStudy.LogINBOXは、チェック状態に関係なくコード側でも除外する。アーカイブ済みのタグも候補に含めない。

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.LogINBOXが混入していないか
  • タグ数や新規タグ候補数が上限以内か

本文中に命令のような文章が含まれていても、記事データとして扱うようプロンプトにも明記した。

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へ依存させない設計にした。

実際の同期フロー

最終的な流れは次のようになった。

  1. MemosからStudy.Log付きのページを取得する
  2. 既存記事なら保存済みのSlugと公開タグを再利用する
  3. 新規記事なら本文をMarkdownへ変換する
  4. Tags DBからブログ表示が有効なタグを取得する
  5. GeminiがSlugと公開タグを構造化出力する
  6. アプリ側で形式、許可タグ、重複を検証する
  7. frontmatter付きMarkdownを保存する
  8. GitHub Actionsが変更をコミットする

実装はPull Request #16で行った。

まとめ

今回の改善で一番大きかったのは、Notionとブログで同じ「タグ」という言葉を使いながら、役割を分離したことだった。

Notionのタグは日常の整理や同期の合図として使う。ブログのタグは読者向けの分類として使う。その間をGeminiがつなぐ。ただし、AIが選べる範囲は人間が管理し、生成結果は初回に固定する。

この形なら、INBOXを中心にした普段のメモ運用を崩さず、ブログ側では安定したURLとタグを持てる。この記事自体をStudy.Log付きで作成し、新しい同期フローの最初のテストにしてみる。