← back

animetana.com

2026-03-20

animetana.com

日本からアニメプラットフォームを作る

日本に住んでいる。アニメ、漫画、ライトノベル、ビジュアルノベル——これらはネットで拾った趣味じゃない。毎日囲まれている文化そのものだ。漫画のフロアが丸ごとある書店。コンビニに並ぶラノベ。秋葉原まで20分。色々なアニメ系サイトを何年も使ってきたが、どれも肥大化しているか、遅いか、明らかにこの文化を本気で好きじゃない人が作ったものばかりだった。だから自分で作ることにした。

animetana.comがその結果だ。アニメ、漫画、ライトノベル、ビジュアルノベルのプラットフォーム。以下は、技術的にどう構築したか、何を選んだか、そしてなぜそうしたかの記録。

モノレポ

プロジェクトはpnpmワークスペースのモノレポで構成されている。複数のサービスがapps/に、共有コードがpackages/に入っている。

apps/
  api/        — Hono REST API + Drizzle ORM
  web/        — Astro フロントエンド + React アイランド
  workers/    — Node.js バックグラウンドジョブ(cron実行)
packages/
  shared/     — データベーススキーマ、TypeScript型定義

全サービスが@animetana/sharedからインポートする。データベーススキーマは一箇所で定義し、APIルートハンドラ、ワーカージョブ、型生成など全てで使い回す。テーブルにカラムを追加すれば、TypeScriptコンパイラが全サービスの更新箇所を検出する。サービス間のズレも、古い型定義も発生しない。

早い段階でリポジトリを分割することも検討した。しなくて正解だった。モノレポならgit pull一回、依存関係ツリー一つ、CIパイプライン一本。一人で開発するプロジェクトで複数リポジトリ間の変更を調整するオーバーヘッドは、純粋な無駄だ。

フロントエンド — Astro + React

フロントエンドはAstroとReactコンポーネントで動いている。ここでの鍵はAstroのアイランドアーキテクチャだ。animetana.comのほとんどのページはコンテンツ中心——アニメの詳細、キャラクター一覧、ランキング。大部分が静的HTMLで、あらすじの表示やカバー画像の描画にJavaScriptランタイムは不要だ。

Reactがハイドレーションされるのは、本当にインタラクティビティが必要な箇所だけ。検索コンポーネント、トラッキングボタン、フィルターパネル、ユーザーメニュー。それ以外はゼロJSのHTMLとして配信される。初期表示が速く、操作が必要な部分はレスポンシブに動く。

ルーティングはAstroのファイルベース。各ページが.astroファイルで、ビルド時またはリクエスト時にデータを取得し、シェルをレンダリングし、必要な箇所にReactコンポーネントを差し込む。メンタルモデルはシンプル——Astroがページを担当し、Reactがウィジェットを担当する。

スタイリングは単一のグローバルCSSファイル。CSS-in-JSもユーティリティフレームワークも使っていない。サイトには統一されたビジュアルアイデンティティがあり、それを完全にコントロールしたい。スタイルシートが一つなら、全てのスタイルルールを一箇所で確認でき、自信を持ってリファクタリングでき、コンポーネント単位で散らばったスタイルによる詳細度の衝突も避けられる。

API — Hono + Drizzle ORM

APIはHonoで構築している。Honoは軽量なTypeScript Webフレームワークで、あらゆるJavaScriptランタイム上で動作する。高速で、TypeScript推論が優秀で、API表面は最小限。RustやCを書いている身としては、デコレータやマジックで制御フローを隠すフレームワークには全く我慢できない。Honoはルーターとミドルウェアサポートを提供し、あとは邪魔しない。

データベースアクセスは全てDrizzle ORMが担う。スキーマ定義からTypeScript型を直接生成するため、クエリはエンドツーエンドで完全に型付けされる。select文を書けば、戻り値の型が実際のカラムと一致する。insert文を書けば、コンパイラが必須フィールドを強制する。データ層にany型が漏れ出すことはない。

スキーマはpackages/shared/src/db/schema/にあり、APIとワーカー間で共有される。両サービスが同じテーブル定義をインポートするため、APIがあるカラム型を想定しながらワーカーが別の型を想定する、という事態は起こり得ない。

ルートハンドラはドメインごとに整理されている。メディア、トラッキング、認証、ユーザー。各ファイルがHonoアプリをエクスポートし、メインルーターにマウントされる。構造はフラットで予測可能。エンドポイントの定義場所を見つけるのに数秒しかかからない。

データベース — PostgreSQL

PostgreSQLが唯一のデータベースだ。セカンダリストアも、ドキュメントDBも、特定機能のためにボルトオンされたグラフDBもない。アニメのメタデータ、ユーザーアカウント、トラッキングリスト、タグ、エンティティ間のリレーション——全てをPostgresが処理する。

スキーマはDrizzleマイグレーションで管理している。pnpm db:generateでスキーマ変更からマイグレーションファイルを生成し、pnpm db:migrateで適用。Drizzle Studioで開発中のデータ確認もできる。ワークフローは直球——スキーマを変更、生成、マイグレーション、完了。

全てのデータ取り込みジョブはON CONFLICT DO UPDATEによるupsertを使う。これにより全ジョブが冪等になる。同じジョブを二回実行しても結果は同じ。重複行も制約違反もない。

AI翻訳済みコンテンツを保護するパターンがコードベース全体に存在する。複数のテーブルにenrichedブーリアンカラムがある。ジョブが行をupsertする際、CASE WHEN式を使う。既にenrichedなら既存の翻訳コンテンツを保持し、そうでなければ新しいデータで上書きする。これにより、コストのかかったAI翻訳が生テキストで上書きされるのを防ぐ。

キャッシュ — Redis

読み取りが多いエンドポイントの前段にRedisを配置している。アニメ詳細ページ、ランキング、人気作品——これらは常にアクセスされるが、データの更新頻度は低い。これらのレスポンスをRedisにキャッシュすることで、冗長なデータベースクエリを排除し、レスポンスタイムを低く保つ。

キャッシュ無効化の戦略はシンプル。データを更新するワーカージョブが、関連するキャッシュキーも同時にクリアする。ワーカーとAPIが同じRedisインスタンスと共有パッケージ経由の同じキー規約を使うため、調整の問題は起こらない。データ更新、キャッシュクリア、次のリクエストでキャッシュ再構築。TTLの推測も古いデータもない。

ストレージ — AWS S3

全ての画像——カバーアート、キャラクターポートレート、バナー——はS3に格納している。APIが処理済み画像をバケットにアップロードし、cdn.animetana.comのCDN経由で配信する。メディアをデータベースやアプリケーションサーバーから切り離すのは基本だが、animetana.comは画像が多いため特に重要だ。全てのアニメエントリにカバーがあり、多くは複数のキャラクター画像を持つ。S3からCDN経由で配信することで、通常運用時にオリジンサーバーが画像バイトに触れることはない。

ローカル開発では、MinIOがS3互換ストレージを提供し、本番バケットに触れることなくアップロード/配信パイプライン全体が同じように動作する。

バックグラウンドワーカー

ワーカーサービスに本当の複雑さがある。複数の並列パイプラインがcronスケジュールで実行され、データ取り込み、画像処理、AI翻訳を担当する。

翻訳パイプラインはAIを使って英語のあらすじを日本語に翻訳する。時間的にもコスト的にも、システム内で最も高価な処理だ。データベースのenrichedフラグにより、各レコードは一度だけ翻訳される。パイプラインを再実行しても、翻訳済みの行はスキップされる。

ワーカーは独立したDockerコンテナとして動作する。データベースとRedisの接続はAPIと共有するが、プロセスとcronスケジュールは独自のものだ。ワーカージョブがクラッシュしたり長時間走っても、APIのレスポンスタイムには影響しない。単純だが信頼性にとって重要な分離だ。

インフラストラクチャ — Docker + Nginx + Cloudflare

本番環境はap-northeast-1(東京)のEC2インスタンスで稼働している。全てがDocker Composeでコンテナ化されている。PostgreSQL、Redis、API、Webフロントエンド、ワーカーがそれぞれ独自のコンテナで共有Dockerネットワーク上に乗っている。

Nginxがホスト上でリバースプロキシとして動作する。animetana.comはWebコンテナへ、api.animetana.comはAPIコンテナへルーティング。SSLはCloudflareで終端し、サーバー側には自己署名のオリジン証明書を置いている。CloudflareがDNS、静的アセットのCDNキャッシュ、DDoS防御を担当。

デプロイプロセスは意図的にシンプルにしている。mainにプッシュ、サーバーにSSH、pull、影響のあるコンテナをリビルドして再起動。CI/CDパイプラインもKubernetesもオーケストレーターもない。一人で開発するプロジェクトでは、自動デプロイインフラの複雑さが実際のデプロイの複雑さを上回る。docker compose build && docker compose up -dは1分以内に完了し、何が起きているか全て見える。

データベースバックアップはcronで12時間ごとにS3へダンプ。復旧はgunzip | psql一発。シンプルで、テスト済みで、信頼できる。

なぜこのスタックか

全ての判断は二つのことに帰結する。システムを理解可能に保つこと、そして速くすること。

理解可能とは、アーキテクチャ全体を頭の中に保持できるということだ。リポジトリ一つ、データベース一つ、キャッシュ一つ、デプロイ先一つ。マイクロサービスの境界について推論する必要も、結果整合性をデバッグする必要も、メッセージキューを監視する必要もない。午前2時に何かが壊れたら、問題を数時間ではなく数分で見つける必要がある。

速いとは、ユーザーにとって速いということだ。Astroは最小限のJavaScriptを配信する。Redisが冗長なクエリを排除する。S3とCloudflare CDNがエッジから画像を配信する。東京リージョンへの配置は、主要なユーザー層である国内ユーザーがオリジンまで一桁ミリ秒のレイテンシで到達できることを意味する。

普段はRustとCでシステム開発をしている。そのバックグラウンドが、不要な抽象化への耐性の低さにつながっている。このスタックはエンドツーエンドで型安全、1分以内にデプロイ可能、ミリ秒単位でページを配信する。必要なことをこなし、それ以上のことはしない。

アニメ、漫画、ラノベ、ビジュアルノベルが好きなら——animetana.comをチェックしてほしい。

hyourin