サイトが文字化けするときの直し方 — 宣言は3か所にあり、勝つのは1つ

先に結論

サイトが文字化けしているとき、やってはいけないのは「ブラウザの設定を確認してください」と案内することです。 Chromeにはエンコードを切り替える機能がもうありません。 閲覧者側では直せないので、直すのは配信する側だけです。

そして、文字コードの宣言は3か所にあります。食い違ったとき、勝つのは1つだけです。 Chrome 152 で11パターン動かして確かめました。

表1:宣言が食い違ったときの優先順位(実測)
順位宣言の場所書かれる場所
1位BOMファイルの先頭3バイト(目に見えない)
2位HTTPレスポンスヘッダContent-Type: text/html; charset=utf-8
3位<meta charset>HTMLの <head> の中
—どれも無い場合UTF-8として読まれる(詳しく)

症状から当たりをつけてください。

表2:症状と原因
症状原因
<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)を取りました。

表3:組み合わせと結果(Chrome 152.0.7977.167で実測)
HTTPヘッダmetaBOM中身読まれた文字コード表示
utf-8なしなしUTF-8UTF-8正常
なしutf-8なしUTF-8UTF-8正常
なしなしなしUTF-8UTF-8正常
shift_jisutf-8なしUTF-8Shift_JIS化ける
utf-8shift_jisなしUTF-8UTF-8正常
shift_jisなしありUTF-8UTF-8正常
なしshift_jisありUTF-8UTF-8正常
なしなしなしShift_JISUTF-8化ける

4行目が、この記事で一番伝えたいところです。 <meta charset="utf-8"> と正しく書いてあるのに、 HTTPヘッダが shift_jis だと化けます。 HTMLをいくら直しても直りません。

優先順位

実測から、順位がはっきりします。

表4:どちらが勝つか(実測)
対決勝ったほう根拠にした行
HTTPヘッダ vs metaHTTPヘッダ表3の4行目と5行目。どちらもHTTPの値になった
BOM vs HTTPヘッダBOM表3の6行目。HTTPが shift_jis でもUTF-8で読まれた
BOM vs metaBOM表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行目がそれにあたります。

表5:宣言が無いときの挙動(実測)
ファイルの中身読まれた文字コード表示
UTF-8UTF-8正常
Shift_JISUTF-8化ける

宣言が無いと、UTF-8として読まれます。 中身がUTF-8なら偶然うまくいき、Shift_JISなら化けます。

「指定しないとShift_JISで読まれる」は、もう逆

2010年代前半に書かれた解説には、 「charsetを指定しないと、既定のShift_JISで読まれてしまう」という説明がよく出てきます。 当時の日本語環境ではそのとおりでした。

いまは逆です。実測のとおり、宣言が無ければUTF-8として扱われます。

表6:前提が入れ替わっている
宣言が無いとき困るファイル
当時の説明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> の先頭近くに書いてあるはずなのに、 その前に別のものが出力されているパターンです。

表7:宣言より前に出力されるもの
出力されるものよくある原因
デバッグ出力フレームワークのデバッグ関数、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 の原因と直し方 に書きました。

どこを直すか

優先順位のとおり、上から順に潰します。下だけ直しても上が勝ちます。

表8:直す場所
場所直し方
① 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() を出す方法があります。

化け方を見れば、元の文字コードが分かる

化けた文字列そのものが手がかりになります。 「文字化けテスト」という同じ文字列を、組み合わせを変えて変換したものを並べます。

表9:化け方のサンプル(同じ文字列を変換したもの)
ファイルの中身読まれ方見え方
UTF-8Shift_JIS譁・蟄怜喧縺代ユ繧ケ繝・
UTF-8windows-1252文字化�テスト
UTF-8EUC-JP���絖������鴻��
Shift_JISUTF-8������e�X�g
EUC-JPUTF-8ʸ�����▥��
EUC-JPShift_JISハクサ�イス、ア・f・ケ・ネ

見た目の特徴がはっきり分かれるので、逆引きできます。

表10:見分け方
見えているもの起きていること直す場所
見慣れない漢字と半角カナが混ざる(譁・蟄・怜・喧…)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分で原因が決まります。

表11:切り分け
順やること分かること
1curl -sI <URL> | grep -i content-typecharset= が付いていれば、それが勝っている
2ブラウザのコンソールで document.characterSet最終的に何で読まれたか
3head -c 3 <file> | xxdBOMの有無
4file <file> または nkf -g <file>ファイルの実際の文字コード
5HTMLのソースを表示して <head> の先頭を見る宣言より前に出力がないか
6化けるページと化けないページを比べる1ページだけなら、そのページ固有の出力が原因

1と2がずれていたら、どこかに別の宣言があります。 2が意図した値なのに化けているなら、文字コードの問題ではなくデータそのものが壊れています。

AIに任せるときの注意

表12: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)、 本文として表示されたり、他の処理を壊したりします。 付けないほうが無難です。

まとめ

データベース側の文字化けは 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パターンを配信して実測したものです。 ブラウザのバージョンや他のブラウザでは挙動が異なることがあります。 サーバー設定を変更する前に、必ず控えを取ってください。