サイトが文字化けするときの直し方 — 宣言は3か所にあり、勝つのは1つ
先に結論
サイトが文字化けしているとき、やってはいけないのは「ブラウザの設定を確認してください」と案内することです。 Chromeにはエンコードを切り替える機能がもうありません。 閲覧者側では直せないので、直すのは配信する側だけです。
そして、文字コードの宣言は3か所にあります。食い違ったとき、勝つのは1つだけです。 Chrome 152 で11パターン動かして確かめました。
| 順位 | 宣言の場所 | 書かれる場所 |
|---|---|---|
| 1位 | BOM | ファイルの先頭3バイト(目に見えない) |
| 2位 | HTTPレスポンスヘッダ | Content-Type: text/html; charset=utf-8 |
| 3位 | <meta charset> | HTMLの <head> の中 |
| — | どれも無い場合 | UTF-8として読まれる(詳しく) |
症状から当たりをつけてください。
| 症状 | 原因 |
|---|---|
<meta charset> を直したのに変わらない | HTTPヘッダが別の値を送っている(詳しく) |
| 特定のページだけ化ける | そのページだけ宣言より前に出力がある(詳しく) |
| サイト全体が化ける | サーバーの既定の charset 設定 |
| 古いページだけ化ける | ファイルがShift_JISのまま。宣言が無いとUTF-8扱いになる(詳しく) |
先頭に のような文字が出る | BOMが本文として表示されている |
| 一部の文字だけ化ける | 文字コードの問題ではなくデータ側の問題(FAQ) |
答えは utf-8 の一択です。
MDNは <meta charset> について
「その値は大文字小文字の区別なく "utf-8" と一致する必要があります」
「UTF-8 が HTML5 文書で唯一の有効なエンコーディングだからです」と書いています。
Shift_JIS や ISO-2022-JP を指定して直す、という解き方はありません。
宣言は3か所にある。勝つのは1つ
11パターンを実測した
HTTPヘッダ・meta・BOM の組み合わせを変えたページをローカルで配信し、
Chrome 152 が最終的にどの文字コードで読んだか(document.characterSet)を取りました。
| HTTPヘッダ | meta | BOM | 中身 | 読まれた文字コード | 表示 |
|---|---|---|---|---|---|
utf-8 | なし | なし | UTF-8 | UTF-8 | 正常 |
| なし | utf-8 | なし | UTF-8 | UTF-8 | 正常 |
| なし | なし | なし | UTF-8 | UTF-8 | 正常 |
shift_jis | utf-8 | なし | UTF-8 | Shift_JIS | 化ける |
utf-8 | shift_jis | なし | UTF-8 | UTF-8 | 正常 |
shift_jis | なし | あり | UTF-8 | UTF-8 | 正常 |
| なし | shift_jis | あり | UTF-8 | UTF-8 | 正常 |
| なし | なし | なし | Shift_JIS | UTF-8 | 化ける |
4行目が、この記事で一番伝えたいところです。
<meta charset="utf-8"> と正しく書いてあるのに、
HTTPヘッダが shift_jis だと化けます。
HTMLをいくら直しても直りません。
優先順位
実測から、順位がはっきりします。
| 対決 | 勝ったほう | 根拠にした行 |
|---|---|---|
HTTPヘッダ vs meta | HTTPヘッダ | 表3の4行目と5行目。どちらもHTTPの値になった |
| BOM vs HTTPヘッダ | BOM | 表3の6行目。HTTPが shift_jis でもUTF-8で読まれた |
BOM vs meta | BOM | 表3の7行目 |
Chrome の開発者向けドキュメントにも、同じ順番の記述があります。
document.characterSet は
まずHTTPヘッダの値になり、無ければ <meta> の charset 属性の値になる、と説明されています。
「metaを直したのに直らない」の正体
HTTPヘッダは、HTMLの中には書いてありません。 だからソースを見ても気づけません。開発者ツールで確認します。
# コマンドラインで見るのが一番早い
curl -sI https://example.com/ | grep -i content-type
# こう返ってくれば、HTMLの meta は無視されている
Content-Type: text/html; charset=Shift_JIS
開発者ツールなら、Networkタブで該当のドキュメントを選び、
Response Headers の Content-Type を見ます。
charset= が付いていたら、それが答えです。
HTMLの meta は読まれません。サーバー側の設定を直す必要があります。
宣言が無いとどうなるか
実測
HTTPヘッダにも meta にも何も書かず、BOMも無い場合です。表3の3行目と8行目がそれにあたります。
| ファイルの中身 | 読まれた文字コード | 表示 |
|---|---|---|
| UTF-8 | UTF-8 | 正常 |
| Shift_JIS | UTF-8 | 化ける |
宣言が無いと、UTF-8として読まれます。 中身がUTF-8なら偶然うまくいき、Shift_JISなら化けます。
「指定しないとShift_JISで読まれる」は、もう逆
2010年代前半に書かれた解説には、 「charsetを指定しないと、既定のShift_JISで読まれてしまう」という説明がよく出てきます。 当時の日本語環境ではそのとおりでした。
いまは逆です。実測のとおり、宣言が無ければUTF-8として扱われます。
| 宣言が無いとき | 困るファイル | |
|---|---|---|
| 当時の説明 | Shift_JIS として読まれる | UTF-8 のファイル |
| いま(実測) | UTF-8 として読まれる | Shift_JIS のファイル |
古い静的ページだけが化けるという症状は、たいていこれです。
昔はShift_JISで保存しておけば勝手に読まれていたものが、いまは読まれません。
ファイルをUTF-8に変換して、<meta charset="utf-8"> を書くのが正解です。
閲覧者側では直せない
エンコードの切り替えは無くなった
「表示メニューからテキストエンコーディングを切り替えてください」という案内を見かけますが、 いまのChromeにその項目はありません。 Chrome 55(2016年)でエンコードメニューが削除された、と複数の技術者が記録しています。
つまり「ブラウザの設定を確認してください」は、答えになっていません。 問い合わせへの返信にこれを書くと、相手は何もできません。 直せるのは配信している側だけです。
ISO-2022-JPの自動判定も削除された
さらに新しい動きがあります。Chrome公式のリリースノートに、こう書かれています。
「HTMLにおける ISO-2022-JP の文字セット自動判定を削除」
「ISO-2022-JP の文字セット自動判定には既知のセキュリティ上の問題があります。
利用が非常に少なく、Safariも ISO-2022-JP の自動判定に対応していないことから、
Chrome 139 でサポートを削除しました」
— Chrome 139 リリースノート(要約)
「ISO-2022-JP を指定すれば直る」という解説は、方向が逆です。 ブラウザは日本語の文字コードを推測する機能を減らしています。 推測に頼る設計から離れて、正しく宣言するしかありません。
宣言より前に、何も出力しない
「1ページだけ化ける」ときに一番多い原因です。
<meta charset> は <head> の先頭近くに書いてあるはずなのに、
その前に別のものが出力されているパターンです。
| 出力されるもの | よくある原因 |
|---|---|
| デバッグ出力 | フレームワークのデバッグ関数、var_dump、print の消し忘れ |
| 警告・notice | 本番で警告表示がオンのまま |
| BOM | ファイルがUTF-8 BOM付きで保存されている |
| 閉じタグの後の空行 | PHPファイル末尾の ?> のあとの改行 |
| インクルードしたファイルの先頭の空白 | 複数ファイルを読み込む構成で起きやすい |
BOMは厄介です。目に見えないのに本文として扱われ、 しかも文字コードの宣言としては最優先で効きます(表4)。 「BOMがあるからUTF-8で読まれて正常」なのか「BOMが本文に出てレイアウトが崩れている」のかは別の話なので、 BOM付きで保存しないのが無難です。
先頭バイトを見れば確認できます。
# 先頭にBOM(ef bb bf)が付いていないか
head -c 3 index.html | xxd
# 付いていればこう出る
00000000: efbb bf ...
これと同じ構造の問題は、APIでも起きます。
JSONを返すはずの場所で警告が先に出力されると、
Unexpected token '<' になります。
読み方は Unexpected token '<' ... is not valid JSON の原因と直し方 に書きました。
どこを直すか
優先順位のとおり、上から順に潰します。下だけ直しても上が勝ちます。
| 場所 | 直し方 |
|---|---|
| ① BOM | エディタで「UTF-8(BOMなし)」として保存し直す |
| ② HTTPヘッダ(Apache) | AddDefaultCharset UTF-8、または AddType 'text/html; charset=UTF-8' .html |
| ② HTTPヘッダ(Nginx) | charset utf-8; |
| ② HTTPヘッダ(PHP) | header('Content-Type: text/html; charset=UTF-8'); を出力より前に |
| ③ meta | <meta charset="utf-8"> を <head> の先頭に置く |
| ファイルの中身 | UTF-8に変換する(iconv -f SHIFT_JIS -t UTF-8) |
<meta charset> には置き場所の決まりがあります。MDNによれば
「文書の最初の1024バイトの中に完全に収まっている必要があります」。
<head> を開いたらすぐ書く、と覚えておけば外しません。
サーバーの設定を変える前に、必ず控えを取ってください。
.htaccess は書き損じると全ページが落ちます。
共有サーバーで設定ファイルを触れない場合は、アプリ側で header() を出す方法があります。
化け方を見れば、元の文字コードが分かる
化けた文字列そのものが手がかりになります。 「文字化けテスト」という同じ文字列を、組み合わせを変えて変換したものを並べます。
| ファイルの中身 | 読まれ方 | 見え方 |
|---|---|---|
| UTF-8 | Shift_JIS | 譁・蟄怜喧縺代ユ繧ケ繝・ |
| UTF-8 | windows-1252 | æ–‡å—化ã�‘テスト |
| UTF-8 | EUC-JP | ���絖������鴻�� |
| Shift_JIS | UTF-8 | ������e�X�g |
| EUC-JP | UTF-8 | ʸ�����▥�� |
| EUC-JP | Shift_JIS | ハクサ�イス、ア・f・ケ・ネ |
見た目の特徴がはっきり分かれるので、逆引きできます。
| 見えているもの | 起きていること | 直す場所 |
|---|---|---|
| 見慣れない漢字と半角カナが混ざる(譁・蟄・怜・喧…) | UTF-8 を Shift_JIS として読んでいる。一番多い | 宣言を utf-8 に |
| ラテン文字が並ぶ(ã・æ・å…) | UTF-8 を欧文の文字コードとして読んでいる | 宣言を utf-8 に |
| � だらけで、英数字だけ読める | Shift_JIS を UTF-8 として読んでいる。古いファイルで起きる | ファイルをUTF-8に変換 |
| 半角カナだらけ(ハクサ…) | EUC-JP を Shift_JIS として読んでいる | 宣言とファイルを揃える |
| 全体は読めるが一部だけ � | 文字コードの問題ではない。データが壊れている | データの保存経路を調べる |
いちばん上が圧倒的に多いです。 「譁」「蟄」「縺」のような日常で使わない漢字が並んでいたら、UTF-8をShift_JISとして読んでいると思って、 まず HTTPヘッダ を疑ってください。
置換文字(�)が1つも無いのに化けている場合は、欧文の文字コードで読まれています。 欧文の文字コードはすべてのバイトに文字が割り当てられているので、 「読めない」判定が起きず、全部が別の文字になります。
切り分けの手順
上から順にやれば、3分で原因が決まります。
| 順 | やること | 分かること |
|---|---|---|
| 1 | curl -sI <URL> | grep -i content-type | charset= が付いていれば、それが勝っている |
| 2 | ブラウザのコンソールで document.characterSet | 最終的に何で読まれたか |
| 3 | head -c 3 <file> | xxd | BOMの有無 |
| 4 | file <file> または nkf -g <file> | ファイルの実際の文字コード |
| 5 | HTMLのソースを表示して <head> の先頭を見る | 宣言より前に出力がないか |
| 6 | 化けるページと化けないページを比べる | 1ページだけなら、そのページ固有の出力が原因 |
1と2がずれていたら、どこかに別の宣言があります。 2が意図した値なのに化けているなら、文字コードの問題ではなくデータそのものが壊れています。
AIに任せるときの注意
| 頼むこと | 向くか | 理由 |
|---|---|---|
curl -sI の出力を貼って「どこが勝っている?」 | 向く | ヘッダを読むだけ。優先順位で決まる |
| 化けた文字列を貼って「元は何の文字コード?」 | 向く | 化け方のパターンから推測できる |
| 「Apacheでcharsetを指定する設定を書いて」 | 向く | 定型。ただし適用は自分でやる |
| 「文字化けの直し方を教えて」 | 向かない | 「ブラウザのエンコード設定を確認」が返る。その機能は無い |
| 「どの文字コードを指定すればいい?」 | 向かない | Shift_JISやISO-2022-JPを勧められることがある。答えは utf-8 一択 |
| BOMの有無の判定 | 向かない | 貼り付けた時点で消える。自分で先頭バイトを見る |
判定の目安です。答えに「ブラウザの設定」「ISO-2022-JP」「Shift_JISを指定」が出てきたら、情報が古いと思ってください。
よくある質問
meta charset を直したのに変わりません
HTTPヘッダが別の値を送っています(詳しく)。
curl -sI で Content-Type を確認してください。
charset= が付いていれば、HTMLの meta は無視されます。
ブラウザのエンコード設定はどこですか
Chromeにはありません。Chrome 55で削除されています(詳しく)。 閲覧者側では直せないので、配信側を直してください。
Shift_JIS や ISO-2022-JP を指定すれば直りますか
直りません。MDNは <meta charset> の値は
「"utf-8" と一致する必要がある」としています。
Chromeは ISO-2022-JP の自動判定をセキュリティ上の問題を理由に Chrome 139 で削除しました(詳しく)。
1ページだけ化けます
そのページだけ宣言より前に出力があります(表7)。 デバッグ出力、警告、BOM、ファイル先頭の空白を疑ってください。
古いページだけ化けます
ファイルがShift_JISのままで、宣言が無いためです。 宣言が無いときの既定はUTF-8に変わりました(表6)。 昔は勝手に読まれていたものが、いまは読まれません。
一部の文字だけ化けます
全体が読めているなら、文字コードの宣言は合っています。 データそのものが、保存の時点で壊れています。 データベースに入れるときに化けている場合は MySQLの「Incorrect string value」の直し方 を見てください。
先頭に見慣れない文字が出ます
BOMが本文として表示されています。
head -c 3 で ef bb bf が出るか確認し、
「UTF-8(BOMなし)」で保存し直してください(表8)。
BOMを付けておけば安全ですか
文字コードの判定としては最優先で効きますが(表4)、 本文として表示されたり、他の処理を壊したりします。 付けないほうが無難です。
まとめ
- 宣言は3か所にある。強い順に BOM → HTTPヘッダ →
<meta charset>(Chrome 152で実測)。 metaを直しても直らないのは、HTTPヘッダが勝っているから。curl -sIで確認する。- 宣言が無いとUTF-8として読まれる。「指定しないとShift_JISになる」は、いまは逆。
- 閲覧者側では直せない。Chromeはエンコードの切り替えを削除している。
- Chromeは ISO-2022-JP の自動判定をセキュリティ上の理由で削除した(Chrome 139・公式)。推測に頼らない。
- MDNは
<meta charset>の値を "utf-8" と一致する必要があるとしている。答えは1つしかない。 <meta charset>は最初の1024バイト以内に置く。- 1ページだけ化けるなら、宣言より前の出力(デバッグ・警告・BOM・空白)を疑う。
- 切り分けはヘッダ →
document.characterSet→ BOM → ファイルの実体の順。
データベース側の文字化けは MySQLの「Incorrect string value」の直し方、
出力が混ざってJSONが壊れる話は Unexpected token '<' ... is not valid JSON の原因と直し方、
.htaccess を触るならサーバーの話として
レンタルサーバーおすすめ8選 も参考にしてください。
表3・表4・表5の値は、2026年10月25日に Chrome 152.0.7977.167 で ローカルサーバーから11パターンを配信して実測したものです。 ブラウザのバージョンや他のブラウザでは挙動が異なることがあります。 サーバー設定を変更する前に、必ず控えを取ってください。