.htaccessでスマホとPCを振り分ける — コードは動くが、前提が3つ崩れている
先に結論
「.htaccess でスマホを別URLに飛ばす」という方法は、いまも動きます。
手元で確かめました。動かないから困る、という話ではありません。
問題は、その方法が前提にしていたことが3つとも変わったことです。
| 当時の前提 | いま | |
|---|---|---|
| 評価されるページ | PC版が本体 | スマホ版がインデックスとランキングに使われる(詳しく) |
| UA文字列 | 機種もOSも読み取れた | 固定値に縮小された(詳しく) |
| 端末の種類 | スマホかPCの2択 | タブレット・折りたたみ・デスクトップ表示(詳しく) |
だから、判断はこうなります。
| 状況 | どうするか |
|---|---|
| これから作る | 振り分けない。レスポンシブにする(Googleが推奨している) |
| 別URLのスマホサイトが既にある | Vary ヘッダーが付いているか確認する(詳しく) |
| スマホ版の中身がPC版より薄い | 最優先で直す。評価されるのはそちら |
| どうしても出し分けたい部分がある | UA文字列ではなく Sec-CH-UA-Mobile を見る(詳しく) |
Google検索セントラルは、 「実装と維持が最も簡単なデザインパターンとしてレスポンシブウェブデザインをおすすめしています」 と書いています。振り分けは、やらずに済むならやらないほうが楽です。
コードは、いまも動く
実際に当ててみた
よく出回っているのは、こういう形です。UA文字列に特定の語が含まれるかで判定します。
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/sp.*$
RewriteCond %{HTTP_USER_AGENT} (iPhone|iPod|Android|BlackBerry|Windows.Phone) [NC]
RewriteCond %{HTTP_USER_AGENT} !iPad [NC]
RewriteRule ^$ /sp/ [R=302,L]
この判定(iPhone|iPod|Android|BlackBerry|Windows.Phone にマッチし、かつ iPad を含まない)を、
Chrome 152 が実際に送っているUA文字列に当てました。
| UA文字列 | 上のルールの判定 | |
|---|---|---|
| デスクトップ | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ... Chrome/152.0.7977.167 Safari/537.36 | PC扱い(正しい) |
| モバイル | Mozilla/5.0 (Linux; Android 14; Pixel 8) ... Chrome/152.0.0.0 Mobile Safari/537.36 | スマホ扱い→リダイレクト(正しい) |
どちらも意図どおりに振り分きました。コード自体は壊れていません。
動くことと、やるべきことは違う
ただし、同じ測定でもう1つ見えたものがあります。
モバイル側のUAは Chrome/152.0.0.0 で、マイナーバージョンがゼロになっています。
デスクトップ側は 152.0.7977.167 まで出ているのに、です。
これはUA文字列の縮小が効いている印です。 「UAから端末の情報を読み取る」という前提そのものが、作り変えられている途中です(詳しく)。
前提1:評価されるのはスマホ版
振り分けを入れる動機は、たいてい「PC版をきちんと作って、スマホには簡易版を出す」でした。 この優先順位が逆転しています。
「モバイルファーストインデックスとは、Google がスマートフォンエージェントでクロールした
モバイル版のサイトコンテンツを使用してインデックス登録とランキングを行う方法です」
— モバイルファースト インデックスに関するおすすめの方法(Google 検索セントラル)
つまり、スマホ版に載っていない情報は、Googleから見れば存在しません。 PC版にだけ詳しい説明があっても、インデックスに使われるのはスマホ版のほうです。
| 確認 | まずいケース |
|---|---|
| スマホ版の本文量 | PC版より少ない。削った分は評価されない |
| スマホ版の構造化データ | PC版にしか入っていない |
| スマホ版のタイトル・説明文 | PC版と違う、または省略されている |
| スマホ版の画像 | PC版より小さい・少ない |
| スマホ版の内部リンク | PC版にあるリンクが無い |
「スマホ版は軽くしたいから削る」が、そのまま評価を削ることになります。 当時は逆で、PC版さえ充実していればよかったので、ここが一番大きな変化です。
前提2:UA文字列は縮小された
機種もOSバージョンも固定値になった
UA判定は「UA文字列に端末の情報が入っている」ことを前提にしています。 Chromeはその情報を減らしました。
Chrome公式は、Androidで固定される値の例としてこう挙げています。
Device Model(機種)=「Pixel 3」/OS Version(OSバージョン)=「10」/Platform=「Android」
— User-Agent Client Hints(Chrome for Developers・要約)
実際の機種が何であれ、UA文字列には固定の値が入ります。 「Pixel 3 / Android 10」と書いてあっても、本当にそうとは限りません。
| 読みたかったもの | いまの状況 | 判定に使えるか |
|---|---|---|
| スマホかどうか | Mobile の語は残っている | 使える |
| 機種名 | 固定値になる | 使えない |
| OSのバージョン | 固定値になる | 使えない |
| ブラウザの細かいバージョン | マイナー番号がゼロになる(実測) | 使えない |
「スマホかどうか」の2値判定だけなら、まだ動きます。 壊れるのは「iPhone 15以降だけ」「Android 12以上だけ」といった細かい分岐です。 そういうコードが混ざっていたら、静かに誤判定しています。
代わりの手段がある
Chromeは、UA文字列を読む代わりの仕組みを用意しています。 サーバーに届くヘッダーで、モバイルかどうかが分かります。
「Sec-CH-UA-Mobile ヘッダーは、ブラウザがモバイル端末上にあるかどうかを示します。
?1 がモバイル、?0 がモバイルでないことを表します」
— 同上
| UA文字列 | Sec-CH-UA-Mobile | |
|---|---|---|
| 判定の形 | 長い文字列を正規表現で探す | ?1 か ?0 かを見るだけ |
| 端末が増えたとき | 正規表現を足す必要がある | そのまま動く |
| 対応ブラウザ | 全部 | Chromium系のみ |
| JavaScriptから | navigator.userAgent | navigator.userAgentData.mobile |
3行目が弱点です。Chromium系以外には届きません。 だから単独では置き換えられず、UA文字列との併用になります。 手間が減るわけではない、というのが正直なところです。
手元で確認した値を載せておきます。
.mobile | .platform | |
|---|---|---|
| デスクトップ | false | macOS |
| モバイル | true | Android |
前提3:2択では足りない
当時のコードが iPad をわざわざ除外していたことが、この問題を物語っています。
「スマホでもPCでもないもの」が、最初から存在していました。
| 端末・状況 | 振り分けると何が起きるか |
|---|---|
| タブレット | 画面は広いのに簡易版が出る(または逆) |
| 「デスクトップ用サイトを表示」を選んだスマホ | 利用者の意思を無視して戻される |
| 折りたたみ端末 | 開くと広いが、判定は変わらない |
| スマホから共有されたPC版URL | 開いた瞬間にスマホ版へ飛ばされ、見たかった箇所に行けない |
| 画面を分割して使っている端末 | 実際の表示幅と判定が合わない |
2行目が一番多い不満です。
利用者がわざわざPC版を選んでいるのに、サーバーが強制的に戻します。
当時は viewmode=pc のようなパラメータを付けて逃がす工夫をしていましたが、
それはつまり、判定が外れることを前提にした作りです。
レスポンシブなら、この表の全部が消えます。 URLが1つで、幅に合わせて並びが変わるだけだからです。
それでも振り分けるなら
Varyヘッダーが要る
既存の別URL構成を残す場合、検索エンジンに「同じURLでも相手によって中身が変わる」と伝える必要があります。 Google検索セントラルの記述です。
「別個の URL を使用する場合、HTTP の user-agent ヘッダーと Vary ヘッダーを利用して、
デバイスに合ったバージョンのサイトにリダイレクトします」
— モバイルファースト インデックスに関するおすすめの方法(Google 検索セントラル)
Vary を付けていない解説が大半です。
これが無いと、キャッシュがスマホ向けの応答をPCに返したり、その逆が起きたりします。
CDNを挟んでいると特に露骨に出ます。
# 振り分けを行うなら、これも一緒に出す
Header append Vary User-Agent
いま書くならこう
<IfModule mod_rewrite.c>
RewriteEngine On
# 1. すでに /sp/ 配下なら何もしない(無限ループ防止)
RewriteCond %{REQUEST_URI} !^/sp/ [NC]
# 2. 利用者がPC版を選んだら尊重する
RewriteCond %{QUERY_STRING} !(^|&)viewmode=pc(&|$) [NC]
RewriteCond %{HTTP_COOKIE} !viewmode=pc [NC]
# 3. Client Hints が来ていればそれを優先する
RewriteCond %{HTTP:Sec-CH-UA-Mobile} =?1 [OR]
RewriteCond %{HTTP_USER_AGENT} (iPhone|iPod|Android.+Mobile|Windows.Phone) [NC]
# 4. 302(一時)にする。301だとブラウザが覚えてしまう
RewriteRule ^(.*)$ /sp/$1 [R=302,L]
</IfModule>
# 5. 相手によって中身が変わることを伝える
<IfModule mod_headers.c>
Header append Vary User-Agent
</IfModule>
| 変更点 | 理由 | |
|---|---|---|
| 1 | Android → Android.+Mobile | Androidタブレットを巻き込まない。スマホのUAにだけ Mobile が入る |
| 2 | Sec-CH-UA-Mobile を先に見る | UA文字列より確実。届かない相手にはUAで判定 |
| 3 | 301 → 302 | 301はブラウザが記憶する。あとで設計を変えても戻せなくなる |
| 4 | トップページだけ → パスを引き継ぐ | 深いページを共有されたとき、同じ内容に着地させる |
| 5 | Cookieでも解除できるように | クエリだけだとリンクを辿った先で戻される |
| 6 | Vary を追加 | Googleの記載。キャッシュ事故を防ぐ |
踏みやすい罠
| 症状 | 原因 |
|---|---|
| 無限リダイレクト | リダイレクト先でも条件が成立している。!^/sp/ の除外漏れ |
| PC版に戻れない | 301で記憶されている。利用者側のキャッシュを消さないと直らない |
| CDN越しに逆の版が出る | Vary が無い |
| 全ページがトップに飛ぶ | RewriteRule ^$ のままでパスを引き継いでいない |
| CSSや画像まで飛ぶ | 静的ファイルを除外していない |
| POSTが壊れる | リダイレクトでメソッドが変わる(301・302はPOSTをGETにする) |
2行目は特に厄介です。
301 は「恒久的」の意味なので、ブラウザが記憶します。
サーバー側を直しても、一度飛ばされた利用者は戻ってこられません。
振り分けは 302 にしてください。
.htaccess を書き換える前に、必ず控えを取ってください。
書き損じると全ページが 500 になります。
そもそも .htaccess が有効かどうかはサーバーの設定次第なので、
レンタルサーバーおすすめ8選 も参考にしてください。
振り分けをやめる
既にあるものを捨てる判断は難しいので、段階を分けて考えます。
| 段階 | やること | 効果 |
|---|---|---|
| 1 | スマホ版の中身をPC版と揃える | ここが最優先。評価されるのはスマホ版(詳しく) |
| 2 | Vary を付ける | キャッシュ事故が止まる |
| 3 | 301を302に変える | あとで方針を変えられる状態に戻す |
| 4 | PC版をレスポンシブにする | URLが1つで足りるようになる |
| 5 | スマホ版URLをPC版へ301で寄せる | ここで初めて301。統合は恒久的なので |
5段階目まで行かなくても、1と2だけで実害はかなり減ります。 全面的な作り直しは大きな仕事なので、順番を間違えないことのほうが大事です。
AIに任せるときの注意
| 頼むこと | 向くか | 理由 |
|---|---|---|
いまの .htaccess を貼って「無限ループの原因は?」 | 向く | 条件の除外漏れは読めば分かる |
| 「このルールが当たるUA文字列の例を出して」 | 向く | 正規表現のテストケース作り |
| 「レスポンシブのCSSに直して」 | 向く | 定型。メディアクエリの機械的な作業 |
| 「スマホとPCを振り分ける方法を教えて」 | 向かない | 301で書いた古い例が返る。301は戻せない |
| 「どのUAを判定に入れればいい?」 | 向かない | 端末名の列挙は増え続ける。UA縮小で前提も変わった |
.htaccess を直接書き換えさせる | 向かない | 書き損じると全ページが落ちる。必ず控えを取って自分で適用する |
判定の目安です。出てきたコードが [R=301,L] になっていたら、そのままでは使わないでください。
振り分けは 302 です。
よくある質問
UA判定はもう動かないのですか
動きます。Chrome 152で実際に当てて確認しました(表3)。 「スマホかどうか」の2値判定は今も通ります。 壊れるのは機種名やOSバージョンを見る細かい分岐です(表5)。
301と302はどちらがいいですか
振り分けは302です。 301はブラウザが記憶するので、サーバー側を直しても利用者が戻ってこられません(表10)。 301を使うのは、スマホ版を廃止してPC版に統合するときだけです。
Varyヘッダーは必要ですか
必要です。Google検索セントラルが別URL構成について 「user-agent ヘッダーと Vary ヘッダーを利用して」と書いています(詳しく)。 無いとキャッシュが逆の版を返すことがあります。
Androidタブレットがスマホ版になります
Android だけで判定しているためです。
スマホのUAには Mobile が入るので、Android.+Mobile にします(表9)。
スマホ版は軽くしたいので内容を削っています
削った分は評価されません。 Googleはスマホ版をインデックスとランキングに使います(詳しく)。 軽くしたいなら、中身ではなく画像やスクリプトを減らしてください。
無限リダイレクトになります
リダイレクト先でも条件が成立しています。
RewriteCond %{REQUEST_URI} !^/sp/ のような除外が抜けていないか確認してください(表10)。
Sec-CH-UA-Mobile だけで判定していいですか
いけません。Chromium系のブラウザにしか届きません(表6)。 UA文字列との併用になります。
いまから新しく作るならどうすべきですか
振り分けないでください。 Googleは「実装と維持が最も簡単なデザインパターン」としてレスポンシブを勧めています。 URLが1つなら、この記事に書いた問題は全部起きません。
まとめ
- UA判定のコードは、いまも動く。Chrome 152に当てて確認した。動かないから困る、という話ではない。
- 前提が3つ変わった。評価されるページ、UA文字列の中身、端末の種類。
- Googleはスマホ版をインデックスとランキングに使う。スマホ版で削った分は、評価からも削られる。
- UA文字列は縮小された。Androidでは機種もOSバージョンも固定値になる(Chrome公式)。細かい分岐は静かに外れる。
- 「スマホかどうか」だけなら
Sec-CH-UA-Mobileが確実。ただしChromium系にしか届かないので併用になる。 - 振り分けるなら
Vary: User-Agentが要る(Google検索セントラル)。付けていない解説が大半。 - 301ではなく302。301はブラウザが記憶するので、直しても戻ってこられない。
Androidだけで判定するとタブレットを巻き込む。Android.+Mobileにする。- やめるなら順番が大事。①スマホ版の中身を揃える ②
Vary③302化 ④レスポンシブ化 ⑤統合。 - これから作るなら振り分けない。Googleがレスポンシブを勧めている。
リダイレクトでPOSTがGETに変わる話は Vue 3 で axios の POST が思ったとおりに動かない、
同じ .htaccess で文字コードを指定する話は サイトが文字化けするときの直し方、
サーバー選びは レンタルサーバーおすすめ8選 に書きました。
UA文字列と判定結果は、2026年10月25日に Chrome 152.0.7977.167 で実際に取得して確認したものです
(モバイル側は開発者ツールの端末エミュレーションによるものです)。
UA文字列の形式は変更が続いています。
.htaccess を書き換える前に必ず控えを取り、本番ではないところで試してください。
書き損じると全ページが表示できなくなります。