Cloudflare Pages/WorkersでMD/MDXを文字化けなく配信するには_headersを使う

ブラウザでの文字ばけを防ぐため、public/_headers で強制的に UTF-8 を流す

Cloudflare Pages(Functions/Workers 実行)で、/blog/20250810.mdx のように Markdown/MDX の生テキストを配信したら、日本語が文字化けしました。ローカルの astro dev では再現せず、本番だけ壊れるやつ。

原因は単純で、レスポンスの Content-Type(MIME と charset)の扱いが微妙だったから。text/mdx のような独自っぽい MIME を返したり、charset 指定が弱いと、中継やブラウザ側で勝手に推測されて化けます。

今回の対処は Pages 標準のヘッダー設定ファイル public/_headers を使って、対象パスに強制的に Content-Type: text/plain; charset=utf-8 を付ける、です。Workers のコードをいじらなくてもいいのがポイント。

参考: https://developers.cloudflare.com/workers/static-assets/headers/

補足(このサイトの前提)

このブログでは、各記事の元原稿(生の MDX)をそのまま公開しています。例えば、通常のページ /blog/20250810 に対応して、/blog/20250810.mdx で原稿テキストを配信しています。生 MDX を直接ブラウザに見せるという性質上、Content-Typecharset の指定が弱いと簡単に文字化けします。今回の _headers はその前提を踏まえた対処です。


やったこと(結論)

  1. public/_headers を用意し、.mdx 直配信のパスにヘッダーを付ける
  2. ついでに X-Content-Type-Options: nosniff とキャッシュも良き感じに

public/_headers の中身はこんな感じ。

# /blog/*.mdx は「生のテキスト」として UTF-8 で配信する
/blog/*.mdx
  Content-Type: text/plain; charset=utf-8
  X-Content-Type-Options: nosniff
  Cache-Control: public, max-age=300, s-maxage=31536000, immutable

これだけで、Pages 経由のレスポンスに上記ヘッダーが必ず乗るようになります。Astro のエンドポイントで Response を返していても、Pages の _headers が最終的な上書きをしてくれるため、CDN 側での推測(sniff)が抑制され、文字化けが止まります。


背景メモ

  • ローカルでは OK、本番(Workers 経由)でだけ文字化け → 中継や CDN が Content-Type を解釈できず、勝手に推測した可能性が高い。
  • text/mdx は一般的でないため、text/plain; charset=utf-8 に寄せたら安定した。
  • 本ブログの生MD/MDX配信はビルドで静的化されるため、実行時に個別エンドポイントで細かく調整することはできない。Cloudflare Pages の _headers を使う前提。

Astro側で直接書くならこんな感じという参考コード。ただし、このブログの「生MD/MDX直配信」には適用できません(ビルドで静的ファイルになり、実行時のエンドポイントが存在しないため)。必ず _headers を使います。

export const GET = async () => {
  const body = "# Hello MDX\n日本語テスト 🐤";
  return new Response(body, {
    headers: {
      "content-type": "text/plain; charset=utf-8",
      "x-content-type-options": "nosniff",
    },
  });
};

まとめ

Pages/Workers で「プレーンな Markdown/MDX をそのまま見せたい」場合、public/_headers による Content-Type: text/plain; charset=utf-8 の強制が一番手軽で安全でした。特にこのブログ構成では、Astro のビルドで /blog/*.mdx が静的出力(例: dist/blog/20250810.mdx)され、_worker.js/pages/blog/... のエンドポイントは生成されません。そのため実行時にヘッダーを細かく調整する余地はなく、Cloudflare Pages の _headers で上書きするのが必須です。