Notes / Web技術

Tailwind v4と@fontsourceの落とし穴。@import同居でwoff2が404になる

Tailwind v4と@fontsourceを同じCSSファイルで@import同居させると、ビルド済みCSSのfont-faceは作られるのに、対応するwoff2ファイルがdistに出力されません。本番で日本語フォントが全部404、ブラウザはHiraginoにフォールバック。Lighthouse緑、Playwright緑のまま見逃される構造と、@import分離による解決。

Tailwind v4 fontsource Vite asset pipeline

新しいAstroサイトをTailwind v4 + @fontsourceで立ち上げて、ビルド成果物をDevToolsのNetworkタブで眺めていたら、想定外の挙動を見つけました。日本語フォントのwoff2ファイルが全部404で返っている。

GET https://example.com/_astro/files/noto-sans-jp-0-400-normal.woff2
  net::ERR_ABORTED 404 (Not Found)

GET https://example.com/_astro/files/noto-serif-jp-112-wght-normal.woff2
  net::ERR_ABORTED 404 (Not Found)

500件近いリクエストが全部404。それなのにLighthouseは93点を取れていて、Playwrightも56/56 pass。ブラウザはHiraginoに静かにフォールバックして表示するので、視覚チェックだけでは見抜きづらい。Tailwind v4 + @fontsourceで新規プロジェクトを始めるなら必ず踏むタイプの落とし穴です。

症状の出方

dist/_astro/を覗くと、状況がはっきりします。

$ find dist/_astro -name "*.woff2" | wc -l
0

$ grep -oE "url\([^)]+\.woff2[^)]*\)" dist/_astro/BaseLayout.css | wc -l
496

ビルド済みCSSには496件のurl(./files/noto-...-normal.woff2)が並ぶ。けれど対応するwoff2ファイルがdist/_astro/files/に1つもない。dist/_astro/files/ディレクトリ自体が生成されていません。

node_modulesにはフォントが入っているか確認:

$ find node_modules/@fontsource node_modules/@fontsource-variable -name "*.woff2" | wc -l
1249

1249件入っている。けれどビルド時にdistへコピーされていない。

原因: Tailwind v4のlightningcssが@importを先に展開する

src/styles/global.cssがこういう構造の時に発生します。

@import "@fontsource/noto-sans-jp/400.css";
@import "@fontsource/noto-sans-jp/500.css";
@import "@fontsource/noto-sans-jp/700.css";
@import "@fontsource-variable/noto-serif-jp/wght.css";

@import "tailwindcss";
@plugin "@tailwindcss/typography";

@theme {
  /* ... */
}

@import "tailwindcss"が同居しているため、@tailwindcss/viteプラグインがlightningcssで全部の@importを一括処理します。そのlightningcssが@fontsource@importを展開する時、font-faceのurl(./files/...)Viteのasset resolverに通さないままCSS出力に書き込みます。

結果:

  • ビルド済みCSSにはurl(./files/noto-sans-jp-0-400-normal.woff2)がそのまま残る
  • Viteは./files/...が何を指すか知らないので、asset処理をスキップ
  • dist/_astro/files/が生成されない
  • ブラウザはCSSが指すパスを取りに行く → 404

通常Viteはurl()参照を解析してassetをコピーし、ハッシュ付きパスに書き換えます。url(/_astro/noto-sans-jp-0-400-normal.HASH.woff2)のような形に。Tailwind v4のlightningcssが先に処理してしまうと、この書き換えが起きません。

修正: @fontsource importを別ファイルに分離

修正は機械的です。@fontsource@importglobal.cssから分離して別ファイルに置く。

src/styles/fonts.cssを新設:

@import "@fontsource/noto-sans-jp/400.css";
@import "@fontsource/noto-sans-jp/500.css";
@import "@fontsource/noto-sans-jp/700.css";
@import "@fontsource-variable/noto-serif-jp/wght.css";

src/styles/global.cssから該当行を削除。

BaseLayout.astroglobal.cssの前にfonts.cssをimport:

---
import '@styles/fonts.css';
import '@styles/global.css';
---

これでビルドし直すと:

$ find dist/_astro -name "*.woff2" | wc -l
496

$ grep -oE "url\(/_astro/[^)]+\.woff2\)" dist/_astro/*.css | head -3
url(/_astro/noto-sans-jp-0-400-normal.CQM38w3s.woff2)
url(/_astro/noto-sans-jp-1-400-normal.BxmNQsBN.woff2)
url(/_astro/noto-sans-jp-2-400-normal.2pr-wf2b.woff2)

496件全部のwoff2がハッシュ付きでdistに出力。CSSのurl()参照も/_astro/<name>.<hash>.woff2に正しく書き換わっています。

fonts.cssにはTailwindの@importが含まれないので、Viteの標準CSS処理パイプラインが正常に動く。lightningcssが介在しません。

このパターンが見逃されやすい理由

自動テストやスコア計測では、このバグはまず捕捉できません。仕組み上どうしても見逃しやすい。

ブラウザの優しいフォールバック。CSSでfont-family: "Noto Sans JP", "Hiragino Kaku Gothic ProN", ...と指定していると、Noto Sans JPがロードできない時にHiraginoが使われます。Hiraginoは美しい日本語フォントなので、見た目の劣化が小さい。

Lighthouseはフォントロードを直接評価しない。LCPやTBTには影響が出ますが、woff2が404でもフォールバックフォントで描画完了は早いので、むしろスコア的には良くなる可能性すらあります。

Playwrightはフォント実体を検証しない。e2eテストは要素の存在、テキスト内容、aria属性などを見ますが、「このフォントファミリーが実際にロードされたか」までは検証しないのが普通。

CSSの404はConsoleに目立たない<link>タグや<script>タグの404はConsoleにエラーとして出ますが、CSS内のurl()参照の404は警告として埋もれるかNetworkタブだけに出ます。

これらが重なるので、コードレビューと自動テストの組み合わせでは捕捉不能。発見には実機ブラウザのNetworkタブを直接覗く必要があります。

リリース前の確認手順

Tailwind v4 + @fontsourceの組み合わせで新規プロジェクトを立ち上げる時、リリース前に下記をやります。

  1. npm run build後にfind dist -name "*.woff2" | wc -lでwoff2のコピー数を確認
  2. 0件ならこの落とし穴に該当、fonts.css分離を実施
  3. デプロイ後、ブラウザでサイトを開きDevToolsのNetworkタブをタイプFontでフィルタ
  4. 全woff2が200で返っているか確認、404があれば実装側の問題

特に3は10秒で済む確認なので、本番反映前の習慣にすると、フォント以外のサイレントな404も同時に拾えます。

教訓: 自動テストが緑でも本番は見る

「自動テストが全部通っているからOK」では検出できない種類のバグがあります。

  • フォントが配信されていなくてもフォールバックで動く
  • スクリプトが配信されていなくてもtry/catchで握り潰される
  • 画像が配信されていなくてもaltテキストで成立する
  • API呼び出しが失敗してもデフォルト値で動く

これらは「サイレントな劣化」で、テストは通るのにユーザー体験が壊れる類のバグ。リリース前に手作業でブラウザを開いてDevToolsのNetworkタブを眺める、というローテクな確認が、自動テストの盲点を埋めます。


Tailwind v4と@fontsourceは、素直に@import同居させると壊れます。@fontsource@importは別ファイルに分離して、Viteの標準pipelineに通すのが正解。Tailwind v4で新規Astroサイトを立ち上げる時のチェックリストに「distにwoff2が出ているか確認」を入れておくと、本番反映前に確実に拾えます。

Webパフォーマンス改善のご相談はお問い合わせから、業務領域の詳細はServicesへ。