tech / note タブの posts 統合(type 概念の廃止 スコープ1)

起票理由

タブ(tech / note)と記事の type カラムによる分類は、タグ階層のルート(tech/ / misc/)と完全に重複していた。タグ絞り込みがサーバーサイド化されたことで「misc や tech のルートタグで絞る」ことが可能になり、type は冗長な分類軸になったため、タブを posts に統合して type 概念を廃止する。

2段階で進める。

  • スコープ1(本タスク): API から type パラメータ / レスポンスフィールドを廃止し、web を posts タブ化する。DB は無変更(articles.type カラムと tag_article_counts.type は残るが読み取りに使わない)

  • スコープ2(別タスク): DB クリーンアップ。articles.type カラム削除、インデックス (user_id, status, type, published_at, article_id)(user_id, status, published_at, article_id) への張り替え、tag_article_counts の PK から type を除去、frontmatter パーサの type 必須解除、webhook 書き込みパスからの type 除去

設計方針

論点

決定

タブ構成

posts/)と about の2つ。旧 note 相当は タグパネルの misc ルート選択で代替

/type/note のリダイレクト

設定しない(404 で許容)

タグパネルのツリー

root 剥がしをやめ、第1階層にルートタグ(tech / misc)をそのまま表示。ルートタグの選択が旧タブ相当の絞り込みになる

タグの表記

UI・URL ともフルパス(例: ?tags=tech/rust)。相対パス変換(toRelativeTags)は廃止

ファセット集計

tag_article_counts が (user_id, type, tag_id) 単位の前計算のままのため、スコープ1では type 横断を SUM ... GROUP BY tag_id で合算

RSS フィード

従来 tech のみ配信 → 全記事配信に変更(仕様変更として認識済み)

記事詳細の OGP 出し分け

type === 'note'(小カード + サイト共通画像)は misc ルートのタグを持つかどうかで判定に置き換え

変更内容

blog-api

  • GET /users/{name}/articlesGET /users/{name}/articles/tag-facetstype 必須クエリパラメータを廃止(全公開記事が対象になる)

  • 一覧・詳細レスポンスから type フィールドを削除

  • kernel: UsersArticlesRepository::find_published_by_user_name_and_typefind_published_by_user_name にリネームし ArticleType 引数を除去

  • adapter: 一覧 / COUNT / ファセット SQL から AND a.type = ? を除去。フィルタなしファセットは tag_article_counts を type 横断で SUM 集計に変更

  • webhook 書き込みパス(frontmatter の type 必須、tag_article_counts の type 単位再計算)はスコープ1では無変更

apps/web

  • BaseLayout: タブを posts / about の2つに変更

  • /type/note/type/note/page/[page] ルートを削除(リダイレクトなし)

  • lib/api.ts: getArticlesByTypegetArticles(type 引数廃止)、getTagFacets の type 引数廃止、Article / ArticleSummary から type フィールド削除

  • TagFilterProvider / FilteredArticleList / lib/tagFilter.ts: tagRoot 概念と toRelativeTags を廃止し、選択タグ・表示タグをフルパスに統一。buildTagTreeFromFacets はルートタグを第1階層に持つツリーを返す

  • feed/route.ts: 全記事配信に変更、sitemap.ts: 2回フェッチ → 1回に簡素化

  • 記事詳細 generateMetadata: OGP 出し分けを misc ルートタグの有無で判定

  • DESIGN.md / apps/web/CLAUDE.md のタブラベル記述を posts / about に更新

検証

# blog-api
cd apps/blog-api
cargo check --workspace
cargo test --workspace     # 全 pass
cargo clippy --workspace --all-targets

# web
cd apps/web
bun test                   # tagFilter 14 tests pass
bun run type-check
bun run build              # ルート一覧から /type/note が消えることを確認

# リポジトリ全体
bun run lint

注意点 / 残課題

  • タグなし記事はタグ絞り込みで拾えない。旧 note 記事に misc/ タグが付いていない場合、posts 一覧には出るが「misc で絞る」ことができず、記事詳細の OGP も大カード扱いになる。全記事に最低1つルート配下のタグが付いていることを確認すること

  • 一覧クエリはインデックス (user_id, status, type, published_at, article_id) のプレフィックス (user_id, status) までしか効かず、published_at 順は Top-N ソートになる。現状の記事数では問題ないが、スコープ2でインデックスを張り替える

  • /type/note への旧リンクは 404 になる(意図的にリダイレクトを設定していない)

  • デプロイ順は API → web。旧 API は type 必須のため新 web を先に出すと一覧が 400 になる。逆(新 API + 旧 web)は type パラメータが無視されるだけで動く

  • フォローアップ: 旧タブの代替であるルートタグの発見性が低かったため、タグ UI を画面下部固定の FloatingTagFilter(IDE 風ツリー TagFilterTree)に刷新し、ヘッダー・タブ行・一覧を BaseLayout narrow で同一の中央カラムに揃えた。ヘッダーの tags トグルとインラインの TagFilterPanel は廃止