---
title: "Cloudflare Pages で llms.txt が文字化けしたので _headers で charset を書いた"
description: "Cloudflare Pages は配信ファイルの拡張子から `Content-Type` を推測しますが、`text/plain` などには `charset` が付きません。`public/_headers` で `/llms.txt` `/robots.txt` `/sitemap.xml` `/docs/*.md` などに `charset=utf-8` を明示して、日本語の文字化けを直した手順をまとめます。"
category: "Web"
tags: ["Cloudflare Pages","Cloudflare Workers","llms.txt","HTTP","DevOps"]
publishedAt: "2026-05-22"
lastmod: "2026-05-22"
---

[Cloudflare Pages](https://developers.cloudflare.com/pages/) や [Cloudflare Workers の静的アセット配信](https://developers.cloudflare.com/workers/static-assets/) に `llms.txt` を置いたら、`curl -I` で覗いたときの `Content-Type` が `text/plain` だけで、`charset=utf-8` が付いていないことに気づきました。一部のクライアントが UTF-8 を仮定してくれず、日本語が文字化けして読まれていました。`public/_headers` に対象パスを書き並べて回避しています。

```text
# public/_headers
/llms.txt
  Content-Type: text/plain; charset=utf-8

/robots.txt
  Content-Type: text/plain; charset=utf-8

/sitemap.xml
  Content-Type: application/xml; charset=utf-8

/docs/*.md
  Content-Type: text/markdown; charset=utf-8

/ja/docs/*.md
  Content-Type: text/markdown; charset=utf-8
```

この記事では、Cloudflare Pages の既定 `Content-Type` に `charset` が付かないことに気づいた経緯、[`public/_headers`](https://developers.cloudflare.com/pages/configuration/headers/) に書き足した内容、そして `llms.txt` 以外で同じ対処を入れた対象を扱います。

## llms.txt が日本語で文字化けして返ってきた

うちのプロダクトサイトでは、LLM 向けの目次ファイルとして `/llms.txt` を配信しています。中身は日本語と英語が混在した Markdown ライクなテキストで、ファイルは UTF-8 で書いています。

デプロイ後、`curl -I https://example.com/llms.txt` でレスポンスヘッダーを見ると、`Content-Type: text/plain` だけが返ってきました。本文を素直に Latin-1 として解釈するクライアント、たとえば charset 推定をしない単純な HTTP クライアントや一部の LLM 取得経路から見ると、UTF-8 のバイト列が別エンコーディングとして読まれて日本語が崩れます。Web ブラウザーは BOM やヒューリスティックで救うことがありますが、LLM 側で取りに来る経路はブラウザーほど親切ではありません。`llms.txt` を置いた目的が「LLM に読ませる」ことなので、`charset` を曖昧にしておく状態は放置できませんでした。

## Cloudflare Pages の既定 Content-Type には charset が付かない

Cloudflare Pages は、配信するファイルの拡張子からよく知られた MIME type を推測して `Content-Type` を返します。`.txt` なら `text/plain`、`.xml` なら `application/xml` のような形です。ただし `charset` パラメーターは付与しません。HTTP の `text/*` で `charset` が省略された場合、MIME の規定 ([RFC 2046](https://www.rfc-editor.org/rfc/rfc2046.html)) では US-ASCII、旧 HTTP/1.1 ([RFC 2616](https://www.rfc-editor.org/rfc/rfc2616.html#section-3.7.1)) では ISO-8859-1 を既定として扱う経緯があり、後に [RFC 6657](https://www.rfc-editor.org/rfc/rfc6657.html) でこの既定は見直されました。それでもクライアント実装には ASCII / Latin-1 を仮定する経路が残っており、UTF-8 で書いた日本語はそのまま別エンコーディングのバイト列として解釈されてしまいます。

サイト側のファイルが UTF-8 で書かれていても、HTTP レイヤーで `charset=utf-8` を宣言しない限り、受け取る側にはその情報が届きません。

## `public/_headers` で charset を明示する

Cloudflare Pages には、ビルド出力のルート（Astro や Next.js などフレームワーク経由なら `public/`）に [`_headers`](https://developers.cloudflare.com/pages/configuration/headers/) ファイルを置くと、配信時にレスポンスヘッダーを上書きする仕組みがあります。今回はここに対象パスごとの `Content-Type` を書き直しました。

```text
# public/_headers
/llms.txt
  Content-Type: text/plain; charset=utf-8

/robots.txt
  Content-Type: text/plain; charset=utf-8

/sitemap.xml
  Content-Type: application/xml; charset=utf-8

/docs/*.md
  Content-Type: text/markdown; charset=utf-8

/ja/docs/*.md
  Content-Type: text/markdown; charset=utf-8
```

`/docs/*.md` のようにワイルドカードでパターンマッチでき、配下の `.md` ファイルにまとめて適用できます。インデントは半角スペース2つ固定で、見た目より厳格です。タブを混ぜたり1スペースだと無視されることがあるので、エディターのインデント設定は気にしておきます。

`_headers` は静的アセットに対してのみ効きます。Pages Functions や Workers が動的に返すレスポンスはこのファイルの対象外で、関数側の `Response` のヘッダーに直接 `Content-Type` を載せます。今回は静的アセットだけで完結する話なので、`_headers` だけで間に合いました。

## llms.txt 以外で同じ対処を入れた対象

文字化けの懸念があるのは `llms.txt` だけではありません。同じ理屈で、サイト直下の静的テキストファイル全般が対象になります。`_headers` に追加した対象は次のとおりです。

- `/llms.txt` — LLM 向けの目次・サマリー
- `/robots.txt` — クローラー向け
- `/sitemap.xml` — XML だが Cloudflare の既定は `application/xml` で `charset` なし
- `/docs/*.md` — 公開している Markdown 原稿（多言語ロケール別パスも分けて書く）

`.html` は手元で見た限り `text/html; charset=utf-8` で返ってくるケースが多く、追加せずに済んでいます。`.json` も `application/json` が UTF-8 を前提にした扱いを受けることが多いため、急いで足す必要はありません。気にする必要があるのは、扱いがはっきり決まらず charset が省略されがちな `.txt` `.md` `.xml` 系です。

::card[https://labee.jp/posts/astro-og-image]

確認は `curl -I` でレスポンスヘッダーを見れば一発です。Cloudflare のキャッシュ更新を待つ必要があるので、デプロイ直後は `Cache-Control: no-cache` を付けて取り直す癖をつけておきます。

```bash
curl -sI -H 'Cache-Control: no-cache' https://example.com/llms.txt | grep -i 'content-type'
# => content-type: text/plain; charset=utf-8
```

`charset=utf-8` が並んでいれば、クライアント側が UTF-8 でデコードしてくれる状態に戻っています。