Engineering

labee.jpとlabee.devをNext.jsからAstroへ移行した — 48時間の段階移行と一括移行

  • Astro
  • Next.js
  • Cloudflare Workers
  • CSS
  • Tailwind CSS
  • マイグレーション

Cloudflare Workers上で動くコンテンツ中心のサイトに**Reactのランタイムは不要だったため、ラビー合同会社のコーポレートサイト(labee.jp)と開発者ツールサイト(labee.dev)をNext.jsからAstroへ移行しました。Astroの Islands Architecture により、インタラクティブな部分だけにJavaScriptを限定し、それ以外は静的HTMLとして配信できます。さらに、Content Collectionによるfrontmatterのスキーマバリデーションやremarkプラグインの柔軟な拡張など、Markdownベースのコンテンツ管理基盤としてもAstroが適していました。labee.devは48時間**で3段階の段階移行、labee.jpはその経験を踏まえた一括移行です。

ラビー合同会社ラビー合同会社私たちは、自由で革新的なサービスやツールを開発し、テクノロジーとデザインで社会にポジティブな変化をもたらします。 labee.devLabee Dev Toolbox - API-first Developer ToolsAPI-first developer tools for confirming external network state.

Next.jsで困っていたこと

2つのサイトはどちらもCloudflare Workersで動作するコンテンツ中心のサイトです。Next.jsを使っていましたが、フレームワークの方向性とサイトの性質にズレを感じていました。

labee.jpはコーポレートサイトで、インタラクティブなUIはヘッダーのモバイルメニュー程度です。Reactは過剰でした。labee.devはDNS RecordsチェッカーなどのReactコンポーネントを持ちますが、それ以外のページ(ブログ、用語集、ガイド)は静的コンテンツです。

Cloudflare Workers環境固有の制約もありました。Next.jsはVercelでの運用を前提に最適化されており、Workers上ではNode.js互換レイヤーに依存する場面が多くなります。@opennextjs/cloudflareのようなコミュニティアダプターを介してデプロイすることは可能ですが、App Routerの一部機能がWorkers環境では追加の設定やワークアラウンドを要求しました。@opennextjs/cloudflare を挟む必要があり、Next.jsのアップデートへの追従にもラグが生じます。ビルドサイズもWorkerの容量制限を意識する必要があり、React Server Componentsのバンドルが積み重なると無視できません。コンテンツサイトに対してこれらの制約を回避し続けるコストは、フレームワークを変える判断を後押ししました。

ブログ記事の管理にはAstroのContent Collectionが使いたい機能でした。frontmatterのZodバリデーション、globローダーによるファイルベース管理、remarkプラグインの柔軟な拡張。noteで運用していたブログを自社サイトに統合するにあたり、Markdownの処理基盤としてAstroが適していました。

labee.dev — 48時間の段階移行

labee.devは3つのステップで段階的に移行しました。

ステップ1: AstroとSEO最適化の導入

既存のNext.jsプロジェクトにAstroを並行導入しました。SEO最適化が主目的で、ブログやガイドなどの静的コンテンツの生成をAstroに任せる構成を検証しています。この段階ではNext.jsのルーティングとAstroのルーティングが共存しており、Astroが担当するパスだけを切り出す形です。既存のReactコンポーネントやページには手を入れず、影響範囲を新規の静的ページに限定することでリスクを最小化しました。

ステップ2: 全ページのSSR化

すべてのページの静的部分をAstroに移行し、SSR構成に切り替えました。Reactコンポーネントは @astrojs/react インテグレーションを通じてAstroページ内に埋め込んでいます。具体的には、DNS Recordsチェッカーのような対話的なツール画面のコンポーネントを .tsx のまま残し、Astroテンプレートから client:load ディレクティブで読み込む形です。ページのレイアウト(ヘッダー、フッター、サイドバー)はAstroコンポーネントに書き換え、Reactが担う範囲をツールのインタラクティブ部分だけに限定しました。Astroへの移行はReactを捨てることではなく、Reactが必要な部分だけにReactを使う構成への変更です。

ステップ3: ファイル構成の整理

Astroのプラクティスに合わせてファイル構成を整理しました。Next.jsの pages/ ディレクトリ構造からAstroの src/pages/ 構造への再配置、不要になったクライアントコンポーネント(App.tsx やルーティング関連のラッパー)の削除、CSS管理の統合などを行っています。この段階で next.config.js_app.tsx_document.tsx といったNext.js固有のファイルもすべて削除し、依存パッケージからNext.jsとReact DOMを除去しました。

48時間で3段階に分けたのは、各段階でビルドとデプロイを確認し、問題があればすぐに切り戻せるようにするためです。

labee.jp — 一括移行

labee.jpは一括移行しました。

約5,000行の追加と約5,700行の削除で、差し引き約500行のコード削減です。

labee.devでの段階移行の経験があったため、一括でも問題なく進められました。モノレポ構成(apps/web/)を廃止してルート直下のシンプルな構成に変更し、ESLint + PrettierBiomeに置き換えています。

インタラクティブなUIがほぼないため、Reactは導入せずすべてのコンポーネントをpure Astroで実装しました。ヘッダーのモバイルメニューはvanilla JSで十分です。スクロールアニメーションもCSSトランジションとIntersectionObserverの組み合わせで実装しており、framer-motionのようなライブラリは不要でした。

Tailwind CSSの廃止

両サイトとも、移行に合わせてTailwind CSSを廃止しました。

Tailwindのユーティリティクラスはプロトタイピングに有効ですが、コンポーネント数が限られたコンテンツサイトではBEM的なCSS設計の方が見通しがよくなります。サイトのデザインが固まったタイミングで、CSSカスタムプロパティによるデザイントークンシステムに移行しました。

Tailwind CSS v4はCSS-firstのアプローチを採用しており、tailwind.config.ts の代わりにCSS内の @theme ディレクティブでデザイントークンを定義します。従来のJavaScript設定ファイルが不要になり、CSSだけで設定が完結する点は大きな改善です。しかし、@theme で定義したトークンはTailwindのユーティリティクラスを通じて参照する前提であり、カスタムプロパティを直接書くのとは設計思想が異なります。コンテンツサイトではユーティリティクラスの恩恵が薄く、CSSカスタムプロパティを直接定義して直接参照する方がシンプルでした。Tailwind v4のCSS-first approachに触れたことで、逆に「それなら素のCSSカスタムプロパティで十分だ」と判断できた面もあります。

/* src/styles/tokens.css(一部抜粋)*/
:root {
  --color-primary-50: #fffdf0;
  --color-primary-500: #ffb700;
  --color-primary-700: #b36b00;

  --space-1: 4px;
  --space-2: 8px;
  --space-4: 16px;

  --text-sm: 13px;
  --text-base: 15px;

  --layout-max: 1120px;
}

CSSファイルはモジュール化して管理しています。

src/styles/
  globals.css      # @importの集約
  reset.css        # CSSリセット
  tokens.css       # デザイントークン
  base.css         # HTML要素のデフォルト
  layout.css       # ヘッダー・フッター
  components.css   # 共通コンポーネント
  content.css      # 記事コンテンツ
  utilities.css    # ユーティリティ

Tailwindの @apply は不要になり、CSSの可読性が向上しました。デザイントークンは1ファイルに集約されているため、グローバルなスタイル変更も容易です。

Cloudflare Workersへのデプロイ

@astrojs/cloudflare アダプターを使い、output: 'server' でSSRを有効にしています。

// astro.config.mjs(簡略化)
export default defineConfig({
  site: 'https://labee.jp',
  output: 'server',
  adapter: cloudflare({ prerenderEnvironment: 'node' }),
})

prerenderEnvironment: 'node' は、プリレンダリング対象ページのビルドにNode.js環境を使う設定です。Content Collectionの処理などNode.js APIに依存する処理が正常に動作します。

wrangler.jsoncrun_worker_first: true により、リクエストはまずWorkerで処理されます。プリレンダリング済みページは静的HTMLとして配信され、動的ページはWorkerがSSRで生成します。

CIの型チェック

移行後のCIで tsc --noEmit が失敗する問題がありました。astro:content モジュールの型定義が .astro/types.d.ts に生成されるのですが、CIのクリーンな環境ではこのファイルが存在しません。

Astro公式ドキュメントによると、astro check コマンドは内部で astro sync を実行し、型定義ファイルを生成します。tsc の前に astro check を実行する構成に変更しました。

{
  "typecheck": "astro check && tsc --noEmit"
}

labee.devではこの問題が表面化していなかったのは、.astro/ ディレクトリ(types.d.ts を含む)がリポジトリにコミットされていたためです。ローカルで astro devastro build を実行すると .astro/types.d.ts が自動生成され、それがそのままコミットに含まれていました。CIでもこのコミット済みの型定義を参照していたため、astro sync なしでも tsc が通っていたのです。しかし、公式ドキュメントでは .astro/.gitignore に含めるのが推奨されています。生成物をバージョン管理に含めると、開発者間でAstroのバージョン差異がある場合に型定義の不整合が発生するリスクがあります。labee.jpでは最初から .astro/.gitignore に入れていたため問題が顕在化し、astro check を使う正しい構成を採用できました。labee.devも同じ構成に修正しています。

移行後に広がったこと

Astroへの移行後、remarkプラグインを自由に追加できるようになりました。この記事を書いている時点で、リンクカード、用語自動リンク、Mermaidダイアグラムの3つのプラグインが稼働しています。

ブログビルドのたびにOGPを取りに行くリンクカード、CIからDoSになりかけた話Astroのremarkプラグインでリンクカードを自作しました。ビルド時にOGPメタデータを毎回fetchする設計にしたところ、GitHub ActionsからのビルドがDoS的な負荷になりかけたため、キャッシュをリポジトリにコミットする方式に切り替えた経緯を紹介します。 ブログ100件超の用語を正規表現でマッチングしていたら遅くなったのでAho-Corasickに変えたAstroのremarkプラグインで実装した用語自動リンク機能を、正規表現ベースからAho-Corasickアルゴリズムに変更した経緯を解説します。なぜ形態素解析を使わなかったのか、日本語と英語の混在テキストでの単語境界の扱いについても紹介します。 ブログAstroでMermaidを使う — ビルド時変換をやめてクライアントサイドにした理由AstroプロジェクトでMermaidダイアグラムを表示する方法を解説します。rehype-mermaidによるビルド時変換ではなくクライアントサイドレンダリングを選んだ理由、動的インポートでライブラリをページ単位で読み込む設計を紹介します。

noteで運用していたブログも自社サイトに統合し、過去の記事をMarkdownに変換して移行しました。Content CollectionのZodスキーマによるfrontmatterバリデーション、ビルド時の型チェック、CIでの整合性検証。Astroのエコシステムが、コンテンツ管理の基盤として機能しています。