.htaccessでスマホとPCを振り分ける — コードは動くが、前提が3つ崩れている

.htaccessでスマホとPCを振り分ける — コードは動くが、前提が3つ崩れている

先に結論

「先に結論」の内容を図にまとめたもの

「.htaccess でスマホを別URLに飛ばす」という方法は、いまも動きます。 手元で確かめました。動かないから困る、という話ではありません。

問題は、その方法が前提にしていたことが3つとも変わったことです。

表1:前提が変わった3点
当時の前提いま
評価されるページPC版が本体スマホ版がインデックスとランキングに使われる(詳しく)
UA文字列機種もOSも読み取れた固定値に縮小された(詳しく)
端末の種類スマホかPCの2択タブレット・折りたたみ・デスクトップ表示(詳しく)

だから、判断はこうなります。

表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文字列に当てました。

表3:UA文字列と判定結果(Chrome 152で実測)
UA文字列上のルールの判定
デスクトップMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ... Chrome/152.0.7977.167 Safari/537.36PC扱い(正しい)
モバイル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:評価されるのはスマホ版

「前提1:評価されるのはスマホ版」の内容を図にまとめたもの

振り分けを入れる動機は、たいてい「PC版をきちんと作って、スマホには簡易版を出す」でした。 この優先順位が逆転しています。

「モバイルファーストインデックスとは、Google がスマートフォンエージェントでクロールした モバイル版のサイトコンテンツを使用してインデックス登録とランキングを行う方法です」
— モバイルファースト インデックスに関するおすすめの方法(Google 検索セントラル)

つまり、スマホ版に載っていない情報は、Googleから見れば存在しません。 PC版にだけ詳しい説明があっても、インデックスに使われるのはスマホ版のほうです。

表4:振り分けている場合に確認すること
確認まずいケース
スマホ版の本文量PC版より少ない。削った分は評価されない
スマホ版の構造化データPC版にしか入っていない
スマホ版のタイトル・説明文PC版と違う、または省略されている
スマホ版の画像PC版より小さい・少ない
スマホ版の内部リンクPC版にあるリンクが無い

「スマホ版は軽くしたいから削る」が、そのまま評価を削ることになります。 当時は逆で、PC版さえ充実していればよかったので、ここが一番大きな変化です。

前提2:UA文字列は縮小された

「前提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」と書いてあっても、本当にそうとは限りません。

表5:UA文字列から読めなくなったもの
読みたかったものいまの状況判定に使えるか
スマホかどうかMobile の語は残っている使える
機種名固定値になる使えない
OSのバージョン固定値になる使えない
ブラウザの細かいバージョンマイナー番号がゼロになる(実測)使えない

「スマホかどうか」の2値判定だけなら、まだ動きます。 壊れるのは「iPhone 15以降だけ」「Android 12以上だけ」といった細かい分岐です。 そういうコードが混ざっていたら、静かに誤判定しています。

代わりの手段がある

Chromeは、UA文字列を読む代わりの仕組みを用意しています。 サーバーに届くヘッダーで、モバイルかどうかが分かります。

「Sec-CH-UA-Mobile ヘッダーは、ブラウザがモバイル端末上にあるかどうかを示します。 ?1 がモバイル、?0 がモバイルでないことを表します」
— 同上

表6:UA文字列とClient Hintsの比較
UA文字列Sec-CH-UA-Mobile
判定の形長い文字列を正規表現で探す?1 か ?0 かを見るだけ
端末が増えたとき正規表現を足す必要があるそのまま動く
対応ブラウザ全部Chromium系のみ
JavaScriptからnavigator.userAgentnavigator.userAgentData.mobile

3行目が弱点です。Chromium系以外には届きません。 だから単独では置き換えられず、UA文字列との併用になります。 手間が減るわけではない、というのが正直なところです。

手元で確認した値を載せておきます。

表7:navigator.userAgentData の値(Chrome 152で実測)
.mobile.platform
デスクトップfalsemacOS
モバイルtrueAndroid

前提3:2択では足りない

「前提3:2択では足りない」の内容を図にまとめたもの

当時のコードが iPad をわざわざ除外していたことが、この問題を物語っています。 「スマホでもPCでもないもの」が、最初から存在していました。

表8:2択に収まらない端末と状況
端末・状況振り分けると何が起きるか
タブレット画面は広いのに簡易版が出る(または逆)
「デスクトップ用サイトを表示」を選んだスマホ利用者の意思を無視して戻される
折りたたみ端末開くと広いが、判定は変わらない
スマホから共有された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>
表9:昔のコードから変えたところ
変更点理由
1Android → Android.+MobileAndroidタブレットを巻き込まない。スマホのUAにだけ Mobile が入る
2Sec-CH-UA-Mobile を先に見るUA文字列より確実。届かない相手にはUAで判定
3301 → 302301はブラウザが記憶する。あとで設計を変えても戻せなくなる
4トップページだけ → パスを引き継ぐ深いページを共有されたとき、同じ内容に着地させる
5Cookieでも解除できるようにクエリだけだとリンクを辿った先で戻される
6Vary を追加Googleの記載。キャッシュ事故を防ぐ

踏みやすい罠

表10:振り分けでよくある事故
症状原因
無限リダイレクトリダイレクト先でも条件が成立している。!^/sp/ の除外漏れ
PC版に戻れない301で記憶されている。利用者側のキャッシュを消さないと直らない
CDN越しに逆の版が出るVary が無い
全ページがトップに飛ぶRewriteRule ^$ のままでパスを引き継いでいない
CSSや画像まで飛ぶ静的ファイルを除外していない
POSTが壊れるリダイレクトでメソッドが変わる(301・302はPOSTをGETにする)

2行目は特に厄介です。 301 は「恒久的」の意味なので、ブラウザが記憶します。 サーバー側を直しても、一度飛ばされた利用者は戻ってこられません。 振り分けは 302 にしてください。

.htaccess を書き換える前に、必ず控えを取ってください。 書き損じると全ページが 500 になります。 そもそも .htaccess が有効かどうかはサーバーの設定次第なので、 レンタルサーバーおすすめ8選 も参考にしてください。

振り分けをやめる

「振り分けをやめる」の内容を図にまとめたもの

既にあるものを捨てる判断は難しいので、段階を分けて考えます。

表11:やめるまでの段取り
段階やること効果
1スマホ版の中身をPC版と揃えるここが最優先。評価されるのはスマホ版(詳しく)
2Vary を付けるキャッシュ事故が止まる
3301を302に変えるあとで方針を変えられる状態に戻す
4PC版をレスポンシブにするURLが1つで足りるようになる
5スマホ版URLをPC版へ301で寄せるここで初めて301。統合は恒久的なので

5段階目まで行かなくても、1と2だけで実害はかなり減ります。 全面的な作り直しは大きな仕事なので、順番を間違えないことのほうが大事です。

AIに任せるときの注意

「AIに任せるときの注意」の内容を図にまとめたもの
表12: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つなら、この記事に書いた問題は全部起きません。

まとめ

リダイレクトでPOSTがGETに変わる話は Vue 3 で axios の POST が思ったとおりに動かない、 同じ .htaccess で文字コードを指定する話は サイトが文字化けするときの直し方、 サーバー選びは レンタルサーバーおすすめ8選 に書きました。

UA文字列と判定結果は、2026年10月25日に Chrome 152.0.7977.167 で実際に取得して確認したものです (モバイル側は開発者ツールの端末エミュレーションによるものです)。 UA文字列の形式は変更が続いています。 .htaccess を書き換える前に必ず控えを取り、本番ではないところで試してください。 書き損じると全ページが表示できなくなります。