Remix + Cloudflare WorkersでCSS読み込み問題を解決: パスエイリアスとSSRの落とし穴
Remix + Cloudflare WorkersでCSS読み込み問題を解決: パスエイリアスとSSRの落とし穴
📝 概要
コミット 0c1c665 でCSSが突然読み込まれなくなる問題が発生しました。この記事では、問題の発見から原因特定、そして解決に至るまでのデバッグプロセスを詳細に記録します。特に、Remix + Cloudflare Workers環境特有の注意点について解説します。
🎯 問題の発見
症状
- コミット
0c1c665以降、CSSが読み込まれない - ビルドは成功するが、ブラウザでスタイルが適用されない
- 開発サーバー起動時にエラーが発生
初期調査
git show 0c1c665 --statこのコミットで変更されたファイル:
app/entry.client.tsx: CSSインポートの追加app/entry.server.tsx: レンダリング方式の変更app/styles/globals.css:@import文の削除
🔍 根本原因の特定
原因1: パスエイリアスの解決失敗
問題のコード (app/entry.client.tsx):
import "~/styles/globals.css";
import "~/styles/service-name/layer2.css";
import "~/styles/blog/layer2.css";なぜ問題なのか:
entry.client.tsxはクライアントサイドでのみ実行される- SSR時にはこれらのCSSが含まれない
~エイリアスがビルド時に解決されない場合がある
正しいアプローチ:
import "./styles/globals.css";
import "./styles/service-name/layer2.css";
import "./styles/blog/layer2.css";相対パスを使用することで、Viteが確実にパスを解決できます。
原因2: レンダリング環境の不整合
問題のコード (app/entry.server.tsx):
// Node.js用のレンダリング
import { renderToPipeableStream } from "react-dom/server";
import { PassThrough } from "node:stream";なぜ問題なのか:
- プロジェクトはCloudflare Workers向けに設定
vite.config.tsでssr.noExternal: trueが設定されているwrangler.tomlにCloudflare Workers設定が存在- Node.js APIは使用できない
正しいアプローチ:
// Cloudflare Workers用のレンダリング
import { renderToReadableStream } from "react-dom/server";
import type { AppLoadContext, EntryContext } from "@remix-run/cloudflare";
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
loadContext: AppLoadContext
) {
const body = await renderToReadableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
signal: controller.signal,
onError(error: unknown) {
if (!controller.signal.aborted) {
console.error(error);
}
responseStatusCode = 500;
},
}
);
return new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});
}🔧 解決策
ステップ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.jsonのdevスクリプトを更新:
- "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 -20CSSの内容:
- Googleフォント
- Tailwind CSS
globals.cssのカスタム変数service-name/layer2.cssblog/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が
@importをfileローダーで処理しようとする - CSSファイルとして認識されない
2. Cloudflare Workers vs Node.js
開発環境とデプロイ環境の整合性が重要:
| 環境 | レンダリングAPI | モジュールシステム |
|---|---|---|
| Node.js | renderToPipeableStream |
CommonJS/ESM |
| Cloudflare Workers | renderToReadableStream |
ESM only |
教訓:
vite.config.tsとwrangler.tomlの設定を確認- 開発環境をデプロイ環境に合わせる
wrangler pages devを使用する
3. パスエイリアスの使用
TypeScriptのパスエイリアス設定(tsconfig.json):
{
"compilerOptions": {
"paths": {
"~/*": ["./app/*"]
}
}
}これは型チェック用であり、ビルド時の解決は保証されません。
ベストプラクティス:
- CSSインポートには相対パスを使用
- TypeScript/JSXコードでは
~エイリアスを使用可能 vite-tsconfig-pathsプラグインが解決を支援
4. デバッグのアプローチ
効果的なデバッグステップ:
git showで変更を確認
git show <commit-hash>ビルド出力を確認
npm run build # CSSファイルがバンドルされているか確認ビルド済みファイルを検証
grep -n 'from "~' build/index.js # パスエイリアスが解決されていない場合に検出curlでHTMLとCSSを確認
curl -s http://localhost:3000/ | grep stylesheet curl -s http://localhost:3000/assets/entry-xxx.css | head -20設定ファイルの整合性確認
vite.config.tswrangler.tomlpackage.json(devスクリプト)
🎓 まとめ
今回のCSS読み込み問題から学んだ重要なポイント:
- 環境の整合性: 開発環境とデプロイ環境を一致させる
- パス解決: CSSには相対パスを使用し、ビルド時の解決を確実にする
- SSR考慮: クライアントサイドだけでなく、SSR時の動作も考慮する
- 段階的デバッグ: git diff → build → 検証の順で問題を特定
- ドキュメント確認: Remix、Vite、Cloudflare Workersのドキュメントを参照
🔗 参考リソース
- Remix Styling Documentation
- Vite CSS Code Splitting
- Cloudflare Workers Compatibility
- Wrangler Pages Dev
📌 関連コミット
- 問題のコミット:
0c1c665 - 修正コミット:
523247d
git show 523247d
教訓: 環境の違いを理解し、適切なツールと設定を使用することで、多くのデバッグ時間を節約できます。