裁断して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フィルタを足す