このサイトは Bojin Li の個人サイトです。これまで年技術スタック2019GitHub Pages + Jekyll + Disqus、Huxpro テーマ2020Vercel + Jekyll + Disqus、Huxpro テーマ2021Vercel + React 17 + Tailwind CSS + Vite + TypeScript2022Vercel + Next.js 12 + Notion API + Giscus + p5 + three.js2024Vercel + Next.js 14/15 + Tailwind CSS + shadcn/ui2026(現在のバージョン、Haku)Cloudflare Workers + TanStack Start + Vite + Tailwind CSS + Durable ObjectsHaku についてHaku は Yohaku を参考にしています。余白や紙の質感、細かなアニメーションが生む雰囲気など、Yohaku の読書体験を大切にする姿勢を引き継ぎ、リッチテキスト記事も同じように美しく表示します。名前の Haku は Yohaku の一部を残したもので、ちょうど私の名前にある「帛」の日本語の読みでもあります。Yohaku は Next.js ベースですが、このサイトのデプロイ先は Cloudflare Workers で、中国本土特有のネットワーク環境にもできる限り最適化する必要がありました。そこで TanStack Start、TanStack Router、Vite を採用し、Durable Objects も導入しました。Worker 上の SSR 性能を、できる限り低レイヤーまで踏み込んで最適化でき、Next.js の重さや、Vercel 以外のプラットフォームで動かす際の不確実さも避けられます。バックエンドについては、Yohaku 向けの機能にはこのサイトで使わないものが多く、Haku 独自のカスタマイズや最適化も少なくありません。そのため、Yohaku を参考に CMS を書き直し、プロトコルは Yohaku と組み合わせて使う mx-space にできる限り合わせています。全体構成モノレポ内は、Web、Core、Admin の 3 つのアプリに分かれています。アプリ役割技術デプロイ先Webトップページ、記事、固定ページなどの公開サイトReact、TanStack Start / Router、Vite、Tailwind CSSCloudflare WorkersCoreコンテンツ、メディア、認証、コメントなどのバックエンドサービスNestJS、Fastify、Drizzle、PostgreSQL、Redis自前のサーバー、Dokploy で管理Adminコンテンツ管理画面React、Vite、CodeMirror、Haklex / Lexical自前のサーバー、Dokploy で管理3 つのアプリは、TypeScript と Zod による共通のデータ定義で連携しています。この定義を使い、リクエストとレスポンスの境界でもデータを検証します。サーバーサイドレンダリングHaku はサーバーサイドレンダリングを使っています。初めてページを開くときは、Worker がルートと言語に応じて必要なデータを取得し、内容を含む HTML を生成して、ストリームとしてブラウザに送ります。スクリプトの読み込みが終わるのを待たずに、先に内容を読めます。その後、ブラウザで hydration を行い、サーバーが生成したページを引き継いで、ナビゲーションやメニュー、コメントなどの操作を有効にします。以後のサイト内の移動は TanStack Router が処理し、次のページに必要なデータだけを読み込みます。移動のたびにページ全体を再読み込みする必要はありません。サーバー側の描画とブラウザでの引き継ぎがずれないように、Router、QueryClient、Jotai store はサーバーのリクエストごとに独立して作り、ブラウザ側にもそれぞれのインスタンスを持たせています。そのため、別の読者のデータが混ざることはありません。ページ表示時のアニメーションもルートと合わせて管理し、初回表示、通常の移動、戻る・進むによる復元で、それぞれ動作を変えています。データのプリフェッチやバックグラウンド更新で、アニメーションが勝手に再生されることもありません。キャッシュサイト設定やナビゲーション、トップページの各種集計情報は、すべての読者に共通するため共有できます。これらは Cloudflare の Durable Objects に公開スナップショットとして保存し、同じデータへの同時読み込みは 1 回にまとめています。スナップショットが期限切れになった場合は、保持している古い内容を先に返し、バックグラウンドで更新する stale-while-revalidate 方式で待ち時間を減らしています。また、新しいコンテンツを公開すると、古いスナップショットを期限切れとして扱います。キャッシュの更新時にはバージョンも確認するため、先に始まったリクエストが遅れて完了しても、古いデータで新しいデータを上書きすることはありません。記事本文はこの共有キャッシュには入れず、毎回、公開済みのコンテンツから読み込みます。HTML とサーバー関数のレスポンスも private にしています。トップページに表示する各種アクティビティや統計も、考え方は同じです。バックエンドで定期的に集計するため、読者がトップページを開くたびに外部サービスへ問い合わせる必要はありません。外部サービスが一時的に遅くなっても、本文の表示まで遅くなることはありません。表示速度の最適化Haku では、表示速度を上げるために次の工夫をしています。サイトの初期データをできるだけ小さくし、ナビゲーションと現在のページに必要な情報だけを送ります。無関係なページの内容は含めません。ルートの loader とクエリキャッシュを連携させ、1 回のページ移動で同じデータを繰り返し読み込むのを減らします。リッチテキスト、地図、3D などの重い依存モジュールは、用途ごとに分割して必要になったときだけ読み込みます。本文に使うフォントや言語、描画モジュールをあらかじめ用意し、再読み込みや hydration の際に不整合が起きないようにします。背景や 3D のアニメーションの動作を制御し、ページが非表示のときや、読者が動きを減らす設定にしているときは停止するか、動きのない表示に切り替えます。記事を読むためのリソースをできるだけ圧迫しないようにしています。また、Chrome 開発者ツールで通信速度を制限し、SSR と初期表示を繰り返し検証しました。運用中のエラーや性能の問題を特定するため、Sentry も導入しています。
このサイトは Bojin Li の個人サイトです。
これまで
年
技術スタック
2019
GitHub Pages + Jekyll + Disqus、Huxpro テーマ
2020
Vercel + Jekyll + Disqus、Huxpro テーマ
2021
Vercel + React 17 + Tailwind CSS + Vite + TypeScript
2022
Vercel + Next.js 12 + Notion API + Giscus + p5 + three.js
2024
Vercel + Next.js 14/15 + Tailwind CSS + shadcn/ui
2026(現在のバージョン、Haku)
Cloudflare Workers + TanStack Start + Vite + Tailwind CSS + Durable Objects
Haku について
Haku は Yohaku を参考にしています。余白や紙の質感、細かなアニメーションが生む雰囲気など、Yohaku の読書体験を大切にする姿勢を引き継ぎ、リッチテキスト記事も同じように美しく表示します。名前の Haku は Yohaku の一部を残したもので、ちょうど私の名前にある「帛」の日本語の読みでもあります。
Yohaku は Next.js ベースですが、このサイトのデプロイ先は Cloudflare Workers で、中国本土特有のネットワーク環境にもできる限り最適化する必要がありました。そこで TanStack Start、TanStack Router、Vite を採用し、Durable Objects も導入しました。Worker 上の SSR 性能を、できる限り低レイヤーまで踏み込んで最適化でき、Next.js の重さや、Vercel 以外のプラットフォームで動かす際の不確実さも避けられます。
バックエンドについては、Yohaku 向けの機能にはこのサイトで使わないものが多く、Haku 独自のカスタマイズや最適化も少なくありません。そのため、Yohaku を参考に CMS を書き直し、プロトコルは Yohaku と組み合わせて使う mx-space にできる限り合わせています。
全体構成
モノレポ内は、Web、Core、Admin の 3 つのアプリに分かれています。
アプリ
役割
技術
デプロイ先
Web
トップページ、記事、固定ページなどの公開サイト
React、TanStack Start / Router、Vite、Tailwind CSS
Cloudflare Workers
Core
コンテンツ、メディア、認証、コメントなどのバックエンドサービス
NestJS、Fastify、Drizzle、PostgreSQL、Redis
自前のサーバー、Dokploy で管理
Admin
コンテンツ管理画面
React、Vite、CodeMirror、Haklex / Lexical
自前のサーバー、Dokploy で管理
3 つのアプリは、TypeScript と Zod による共通のデータ定義で連携しています。この定義を使い、リクエストとレスポンスの境界でもデータを検証します。
サーバーサイドレンダリング
Haku はサーバーサイドレンダリングを使っています。初めてページを開くときは、Worker がルートと言語に応じて必要なデータを取得し、内容を含む HTML を生成して、ストリームとしてブラウザに送ります。スクリプトの読み込みが終わるのを待たずに、先に内容を読めます。
その後、ブラウザで hydration を行い、サーバーが生成したページを引き継いで、ナビゲーションやメニュー、コメントなどの操作を有効にします。以後のサイト内の移動は TanStack Router が処理し、次のページに必要なデータだけを読み込みます。移動のたびにページ全体を再読み込みする必要はありません。
サーバー側の描画とブラウザでの引き継ぎがずれないように、Router、QueryClient、Jotai store はサーバーのリクエストごとに独立して作り、ブラウザ側にもそれぞれのインスタンスを持たせています。そのため、別の読者のデータが混ざることはありません。ページ表示時のアニメーションもルートと合わせて管理し、初回表示、通常の移動、戻る・進むによる復元で、それぞれ動作を変えています。データのプリフェッチやバックグラウンド更新で、アニメーションが勝手に再生されることもありません。
キャッシュ
サイト設定やナビゲーション、トップページの各種集計情報は、すべての読者に共通するため共有できます。これらは Cloudflare の Durable Objects に公開スナップショットとして保存し、同じデータへの同時読み込みは 1 回にまとめています。スナップショットが期限切れになった場合は、保持している古い内容を先に返し、バックグラウンドで更新する stale-while-revalidate 方式で待ち時間を減らしています。また、新しいコンテンツを公開すると、古いスナップショットを期限切れとして扱います。キャッシュの更新時にはバージョンも確認するため、先に始まったリクエストが遅れて完了しても、古いデータで新しいデータを上書きすることはありません。
記事本文はこの共有キャッシュには入れず、毎回、公開済みのコンテンツから読み込みます。HTML とサーバー関数のレスポンスも private にしています。
トップページに表示する各種アクティビティや統計も、考え方は同じです。バックエンドで定期的に集計するため、読者がトップページを開くたびに外部サービスへ問い合わせる必要はありません。外部サービスが一時的に遅くなっても、本文の表示まで遅くなることはありません。
表示速度の最適化
Haku では、表示速度を上げるために次の工夫をしています。
また、Chrome 開発者ツールで通信速度を制限し、SSR と初期表示を繰り返し検証しました。運用中のエラーや性能の問題を特定するため、Sentry も導入しています。