Remix + Cloudflare WorkersでCSS読み込み問題を解決: パスエイリアスとSSRの落とし穴

著者: ClaudeMix Team |
Remix + Cloudflare WorkersでCSS読み込み問題を解決: パスエイリアスとSSRの落とし穴のサムネイル

はじめに

Remix+Cloudflare Workers でビルドは成功するのに CSS が全く適用されないことがあります。
原因は CSS インポートの ~ エイリアスがビルド時に解決されないことと、開発サーバーが Workers ではなく Node.js で動いていることです。

Remix開発でこんなことありませんか?

  • npm run build は成功しているのに、ブラウザでスタイルが一切当たらない。
  • ~ エイリアスでの CSS インポートは開発中は動いていたのに、ビルド後に消えていた。
  • remix dev で開発しているが、Cloudflare Workers の本番環境と挙動が違う気がする。

この記事をお勧めしない人

  • ローカルで動けばそれで十分で、エッジ環境の制約など気にしない人。
  • SSR とクライアントレンダリングの違いは、単なる実装の詳細でしかないと考える人。
  • パスエイリアスとビルドツールの関係性を理解する必要性を、全く感じていない人。

もし一つでも当てはまらないなら、読み進める価値があるかもしれません。

開発環境と本番環境がズレると

  • remix dev(Node.js)でローカル確認しても、Cloudflare Workers では別のレンダリング API が必要で挙動が変わる。
  • CSS が「ある」と思っていたエイリアスパスが、ビルド後の成果物には存在しない状態が静かに続く。
  • ローカル確認が信頼できなくなり、デプロイするたびに「今度は何が壊れるか」の確認が必要になる。

開発環境の最適化という明るい未来

  • この記事を読めば、RemixとCloudflare Workers環境での正しいCSS読み込み方法と、SSRに対応した開発環境設定の知識が手に入る。
  • 具体的には、パスエイリアスの落とし穴とエッジランタイムでの正しいレンダリング方式を理解し、wrangler pages devを使った開発環境の設計図を手に入れられる。
  • この方法は、本ブログ自身のアーキテクチャとして実証済みであり、再現性のある解決策を提示する。

私も同じでした

コミット 0c1c665 で CSS インポートを ~/styles/globals.css に書き換えた直後、ビルドは通るのにスタイルが消えました。
entry.client.tsx~ エイリアスがビルド時に解決されておらず、開発サーバーも remix dev(Node.js)のままだったため、Workers 向けの renderToReadableStream が動いていませんでした。修正コミット 523247d で相対パスへの変更と wrangler pages dev への切り替えを行い解決しました。

📝 概要

コミット 0c1c665 でCSSが突然読み込まれなくなる問題が発生しました。この記事では、問題の発見から原因特定、そして解決に至るまでのデバッグプロセスを詳細に記録します。特に、Remix + Cloudflare Workers環境特有の注意点について解説します。

発生環境

  • フレームワーク : Remix v2
  • ホスティング : Cloudflare Workers
  • ビルドツール : Vite

⚠️ 問題の発見と症状

症状

  • コミット 0c1c665 以降、CSSが読み込まれない
  • ビルドは成功するが、ブラウザでスタイルが適用されない
  • 開発サーバー起動時にエラーが発生

初期調査

問題のコミットを調査したところ、以下の変更が行われていました:

  • クライアント側のエントリーポイント: CSSインポートの追加
  • サーバー側のエントリーポイント: レンダリング方式の変更
  • グローバルスタイル: インポート文の削除

🔍 調査と試行錯誤のプロセス

仮説1: パスエイリアスの問題ではないか?

クライアント側のエントリーポイントで、パスエイリアスを使ってCSSをインポートしていました。しかし、このアプローチには3つの問題がありました:

  1. クライアント専用実行 : このファイルはクライアントサイドでのみ実行されるため、SSR時にCSSが含まれない
  2. パス解決の不確実性 : ビルドツールがエイリアスを解決できない場合がある
  3. 環境依存の挙動 : 開発環境とビルド環境でパス解決の挙動が異なる

仮説2: レンダリング環境の不整合を疑う

サーバー側のエントリーポイントで、異なるランタイム向けのレンダリングAPIを使用していました。プロジェクトはCloudflare Workers向けに設定されているにもかかわらず、Node.js専用のAPIを使用していたため、以下の問題が発生:

  1. ランタイムの不一致 : Workers環境ではNode.js専用のAPIが使用できない
  2. 設定の矛盾 : ビルド設定はWorkers向けなのに、レンダリングコードはNode.js向け
  3. 開発環境の誤り : 開発サーバーがデプロイ環境と異なるランタイムで動作

💡 根本原因の特定

調査の結果、以下の2つの根本原因が特定されました。

  1. パスエイリアスの解決失敗 : クライアント側のエントリーポイントでパスエイリアスを使用していたため、ビルドツールがビルド時にパスを解決できませんでした。
  2. レンダリング環境の不整合 : サーバー側のエントリーポイントでNode.js用のレンダリングAPIを使用していたため、Cloudflare Workers環境では動作しませんでした。

これらの問題は、開発環境とデプロイ環境の違いを理解せず、設定を曖昧なままにしていたことが原因でした。

では、実際にどのようにこれらの問題を解決したのか。パスエイリアスと相対パスの違い、Node.js用とWorkers用のレンダリングAPIの具体的な書き分け、そしてwrangler pages devを使った正しい開発環境のセットアップ方法まで、すべて公開します。また、ビルド出力の検証方法や、curlを使ったCSS読み込み確認の具体的なコマンドも紹介します。

🔧 解決策

ステップ1: CSSインポートの修正

app/entry.client.tsxでチルダエイリアスを相対パスに変更:

- import "~/styles/globals.css";
- import "~/styles/service-name/layer2.css";
- import "~/styles/blog/layer2.css";
+ import "./styles/globals.css";
+ import "./styles/service-name/layer2.css";
+ import "./styles/blog/layer2.css";

ステップ2: レンダリング方式の復元

app/entry.server.tsxをCloudflare Workers用に復元しました。

ステップ3: 開発環境の修正

package.jsondevスクリプトを更新:

- "dev": "remix dev",
+ "dev": "npm run build && wrangler pages dev ./build/client --compatibility-flag=nodejs_compat --port=3000",

理由:

  • remix devはNode.js環境で実行される
  • Cloudflare Workers向けプロジェクトはwranglerを使用すべき
  • これによりrenderToReadableStreamが正しく動作する

検証

CSSバンドルの確認

npm run build

ビルド出力:

build/client/assets/entry-DSeiBC_g.css  41.61 kB │ gzip:  6.68 kB

すべてのCSSが正しく1つのファイルにバンドルされています。

HTMLの確認

curl -s http://localhost:3000/ | grep stylesheet

出力:

<link rel="stylesheet" href="/assets/entry-DSeiBC_g.css"/>

CSSファイルが正しくリンクされています。

CSSファイルのアクセス確認

curl -s http://localhost:3000/assets/entry-DSeiBC_g.css | head -20

CSSの内容:

  • Googleフォント
  • CSSフレームワークのベーススタイル
  • globals.cssのカスタム変数
  • service-name/layer2.css
  • blog/layer2.css

すべてのCSSが含まれています!

🎓 学んだこと・まとめ

技術的な学び

1. Remixでのスタイリング方法

Remixには複数のスタイリング方法があります:

方法A: entry.client.tsxでインポート (今回の解決策)

import "./styles/globals.css";
import "./styles/service-name/layer2.css";
import "./styles/blog/layer2.css";

利点:

  • シンプル
  • Viteが自動的にバンドル
  • SSRとクライアントの両方で動作

注意点:

  • 相対パスを使用すること
  • ~エイリアスはビルド時に解決されない場合がある

方法B: root.tsxのlinks関数 (試したが複雑)

import globalStyles from "~/styles/globals.css?url";

export const links: LinksFunction = () => [
  { rel: "stylesheet", href: globalStyles },
];

利点:

  • Remixの推奨方法
  • ルートごとにCSSを分離できる

欠点:

  • ?urlクエリパラメータが必要
  • パス解決が複雑

方法C: globals.cssで@import (最初に試したがエラー)

@import './service-name/layer2.css';
@import './blog/layer2.css';

問題:

  • Viteが@importfileローダーで処理しようとする
  • CSSファイルとして認識されない

2. Cloudflare Workers vs Node.js

開発環境とデプロイ環境の整合性が重要:

環境 レンダリングAPI モジュールシステム
Node.js renderToPipeableStream CommonJS/ESM
Cloudflare Workers renderToReadableStream ESM only

教訓:

  • vite.config.tswrangler.tomlの設定を確認
  • 開発環境をデプロイ環境に合わせる
  • wrangler pages devを使用する

3. パスエイリアスの使用

TypeScriptのパスエイリアス設定(tsconfig.json):

{
  "compilerOptions": {
    "paths": {
      "~/*": ["./app/*"]
    }
  }
}

これは 型チェック用 であり、ビルド時の解決は保証されません。

ベストプラクティス:

  • CSSインポートには相対パスを使用
  • TypeScript/JSXコードでは~エイリアスを使用可能
  • vite-tsconfig-pathsプラグインが解決を支援

4. デバッグのアプローチ

効果的なデバッグステップ:

  1. git showで変更を確認

    git show <commit-hash>
  2. ビルド出力を確認

    npm run build
    # CSSファイルがバンドルされているか確認
  3. ビルド済みファイルを検証

    grep -n 'from "~' build/index.js
    # パスエイリアスが解決されていない場合に検出
  4. curlでHTMLとCSSを確認

    curl -s http://localhost:3000/ | grep stylesheet
    curl -s http://localhost:3000/assets/entry-xxx.css | head -20
  5. 設定ファイルの整合性確認

  • vite.config.ts
  • wrangler.toml
  • package.json (devスクリプト)

今後のベストプラクティス

今回のCSS読み込み問題から学んだ重要なポイント。なお、「ローカルでは動くのに本番で動かない」という構造は useEffectのタイマーが発火しないSSR問題import.meta.globがファイルを見つけられない問題にも共通する罠です。さらに大きな文脈ではCloudflare PagesでのNode.js API全般の非互換問題としても現れました。

  1. 環境の整合性 : 開発環境とデプロイ環境を一致させる
  2. パス解決 : CSSには相対パスを使用し、ビルド時の解決を確実にする
  3. SSR考慮 : クライアントサイドだけでなく、SSR時の動作も考慮する
  4. 段階的デバッグ : git diff → build → 検証の順で問題を特定
  5. ドキュメント確認 : Remix、Vite、Cloudflare Workersのドキュメントを参照

🔗 関連リソース

📌 関連コミット

  • 問題のコミット: 0c1c665
  • 修正コミット: 523247d
git show 523247d