Notionの裁断書籍データをTurso DBに取り込むパイプライン構築
裁断してOCRした実務書を Notion に貯めてきたが、検索のたびに Notion を開くのが面倒で、結局あまり開かなくなっていた。今日は Notion からエクスポートした HTML 群を Turso DB に取り込み、既存の書籍ビューア(yomitoku 経由で取り込んだ書籍と同じ画面)から横断検索できるところまで一気通貫させた。
きっかけ
所得税の仕組みを章ごとにメモしてきた Notion ページがある。裁断した実務書を読みながら、自分の言葉で章立てを書き直したノートだ。これが Notion の中に閉じていて、Claude Code 越しに引けない。
Notion からエクスポートしたら HTML がページ数だけ吐き出された。フォルダを開くと ExportBlock という ZIP 構造で、ルートにCSV、配下に各章のHTMLと画像フォルダ(figures/Untitled-*.png)が並んでいる。
既存の書籍ナレッジベースは yomitoku で PDF → Markdown → Turso の経路だけを想定していた。Notion 経由を新しい入口として足せばいいのか、別建てにすべきか、まず構造を読むところから始めた。
方針決定: R2 は使わず既存パターンに合わせる
最初は画像を Cloudflare R2 に置く案を考えたが、調べたら既存書籍は /api/figures/<book_id>/figures/<filename> のエンドポイントで配信していた。Turso の book_figures テーブルに BLOB として入れて、API で配信している。
R2 を追加すると、認証・URL署名・キャッシュ設定が増える。R2 は捨てて、Notion からの画像も同じ book_figures に流し込む方針にした。yomitoku 由来の書籍と Notion 由来の書籍が同じテーブルに混ざるが、source_type カラムで識別できれば困らない。
Codex レビューを3周回した
計画書を memo/2026-05-12/notion-import-plan.md に書いて、Codex CLI(gpt-5.5)に投げた。
1周目: 「ファイル名から章番号を推定」する案にツッコミが入った。「ルートCSVにステータス・章立・概要が全件揃っているなら、ファイル名から推定する理由は何か」と返ってきた。確かに CSV を grep したら章立てが全部入っていた。ファイル名推定の案を捨て、CSV のプロパティを正とする方針に書き直した。
2周目: importer のテストカバレッジが薄いと指摘された。replace_book_with_chunks の挙動(既存書籍を消してから入れ直す)が境界ケースで怪しい、と。テストを 26 件から 30 件に増やした。
3周目: 致命的な指摘ゼロ。GO サインが出た。
毎回 resume --last を付けないと文脈が飛ぶのを忘れて、2周目で一回 Codex に「これは何のレビュー?」と聞き返された。学習。
実装の核: 純粋関数と副作用シェルの分離
importer は HTML を読んで → 整形して → DB に書き込む。整形ロジックは純粋関数に切り出して、DB 書き込みは薄いシェルに集約した。
# 純粋関数: HTML → 整形済みチャンク
def transform_notion_html(html: str, book_id: str) -> Chunk:
...
# 副作用シェル: トランザクションで一括差し替え
def replace_book_with_chunks(book_id: str, chunks: list[Chunk]) -> None:
with db.transaction():
db.execute("DELETE FROM book_chunks WHERE book_id = ?", [book_id])
db.execute("DELETE FROM book_figures WHERE book_id = ?", [book_id])
for chunk in chunks:
db.execute(...)
純粋関数側にテストを集中させたら、画像URL書き換え・タイトル抽出・TOC除去のロジックを画面なしで詰められた。
画面で違和感を拾う係
書き込みまで通ったので、Nuxt の別ポート(3100)で起動して agent-browser で開いた。
最初の画面: 画像が全部リンクになっていて、クリックすると Notion の元URL に飛ぶ。HTML に <a href="..."> の画像ラッパーが残っていた。ラッパーを除去して、画像クリックでモーダル拡大表示に乗るようにした(既存書籍と同じ挙動)。
次の違和感: ページタイトルが本文の冒頭に重複して出ていた。Notion の HTML は <title> タグと、本文先頭の <h1> で同じ文字列が2回出る構造だった。当初は本文先頭の <h1> を削る雑なパッチを当てかけたが、それだと別のページで <h1> が落ちる。<title> を text 化して別カラム(chunk_title)に格納し、本文HTMLからは <title> も TOC も丸ごと除去する形に根本対応した。
右側の目次に h1(ページタイトル)が含まれていなかったのも、chunk_title を目次の先頭に差し込む形で直した。p.{pageNum} の横にセクション名を出すと、目次から飛んだときに「今どこにいるか」が分かるようになった。
agent-browser でスクリーンショットを撮って、画像クリック→モーダル拡大まで動くのを確認できた。
最後にスラッシュコマンド化
このパイプライン、また別の Notion エクスポートで使う。.claude/commands/notion-import.md を作って、CLAUDE.md に追記した。次回は「/notion-import <export-dir>」だけで走る。
学びメモ
- 計画段階のファイル名推定は罠: メタデータが既にどこかに揃っていないか先に grep する。今回はCSVに全部入っていた
- Codex レビューは3周回しても1日で終わる: gpt-5.5 で
-m指定、resume --lastで文脈継続、瑣末な指摘は無視のルールを徹底すると速い - タイトル重複は雑なパッチに走らない:
<h1>を消す案に飛びかけたが、<title>側を text 化する根本対応にして良かった。別ページで<h1>が落ちる事故を未然に防げた - 画面で違和感を拾う係は人間: 画像のリンク化・タイトル重複は、テストでは検知できなかった。3100 ポートで開いて目で見る工程を省くと事故る
明日以降
- 別の Notion ノート(会計の章別メモ)を
/notion-importで取り込む - yomitoku 由来の書籍と Notion 由来の書籍を、書籍一覧画面でバッジ分けして区別する
- 全文検索で
source_typeフィルタを足す