個人開発で「喫煙所検索アプリ」をリリースした際、iOS版のApp StoreリンクとAndroid版のGoogle Playリンクを出し分ける必要がありました。「ユーザーのデバイスに応じてリダイレクト先を変えたい」——シンプルに見えるこの要件が、実装してみると意外な落とし穴だらけでした。
iPadからのアクセスがなぜかPCと判定される、Bot(クローラー)のアクセスで想定外のリダイレクトが発生する、Client ComponentではUser-Agentが取得できない——。
この記事では、Next.js(App Router)でのUser-Agent判定について、公式ドキュメントには載っていない実務で本当に嵌るポイントを中心に解説します。
User-Agentとは#
User-Agent(ユーザーエージェント) とは、ブラウザがサーバーにリクエストを送る際に自動的に付与するHTTPヘッダーです。OS、ブラウザ、デバイスの情報が含まれています。
# iPhoneの場合
Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 ...
# Androidの場合
Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 ...
# PCの場合(macOS Chrome)
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ...この文字列をパースすることで、アクセス元のデバイスを判定できます。
Next.jsでUser-Agentを取得する3つの方法#
Next.js(App Router)でUser-Agentを取得する方法は3つあります。
| 方法 | 実行タイミング | 主な用途 |
|---|---|---|
| Server Component | ページ描画時(サーバー側) | デバイスに応じたUI出し分け |
| Middleware | リクエスト受信時(エッジ) | デバイスに応じたリダイレクト |
| API Route | API呼び出し時 | デバイス情報をJSONで返す |
方法1:Server Componentで判定する#
最もシンプルな方法です。headers() 関数でリクエストヘッダーを取得します。
import { headers } from "next/headers";
// デバイス判定ユーティリティ
function getDeviceType(userAgent: string): "ios" | "android" | "web" {
if (/iPhone|iPod/.test(userAgent)) return "ios";
if (/Android/.test(userAgent)) return "android";
return "web";
}
export default async function Page() {
const headersList = await headers();
const ua = headersList.get("user-agent") ?? "";
const device = getDeviceType(ua);
return (
<div>
<p>あなたのデバイス: {device}</p>
{device === "ios" && (
<a href="https://apps.apple.com/app/xxx">App Storeで開く</a>
)}
{device === "android" && (
<a href="https://play.google.com/store/apps/details?id=xxx">
Google Playで開く
</a>
)}
</div>
);
}⚠️
headers()は Server Component専用のAPI です。Client Component("use client"を宣言したコンポーネント)では使用できません。
方法2:Middlewareでリダイレクトする#
ユーザーがページにアクセスした瞬間にリダイレクトしたい場合は、Middlewareを使います。
// middleware.ts(プロジェクトルートに配置)
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
// /app ページへのアクセスのみ対象
const ua = request.headers.get("user-agent") ?? "";
if (/iPhone|iPod/.test(ua)) {
return NextResponse.redirect(
"https://apps.apple.com/app/your-app-id"
);
}
if (/Android/.test(ua)) {
return NextResponse.redirect(
"https://play.google.com/store/apps/details?id=your.app.id"
);
}
// PC / その他はそのまま表示
return NextResponse.next();
}
export const config = {
matcher: ["/app/:path*"], // 特定パスのみに適用
};matcher を必ず設定する理由#
matcher を設定しないと、画像やCSS、APIルートなどすべてのリクエストにMiddlewareが実行されます。これはパフォーマンスの低下やGooglebotのクロール阻害につながるため、必ず対象パスを限定してください。
方法3:ユーティリティ関数として共通化する#
実務では判定ロジックを関数化し、複数箇所から呼び出せるようにします。
// lib/device.ts
export type DeviceType = "ios" | "android" | "tablet" | "web";
export function getDeviceType(userAgent: string): DeviceType {
const ua = userAgent.toLowerCase();
// iPadの判定(後述の落とし穴を参照)
if (/ipad/.test(ua)) return "tablet";
// iPhoneの判定
if (/iphone|ipod/.test(ua)) return "ios";
// Androidタブレットの判定("Android" を含むが "Mobile" を含まない)
if (/android/.test(ua) && !/mobile/.test(ua)) return "tablet";
// Androidスマホの判定
if (/android/.test(ua)) return "android";
return "web";
}実務で嵌る落とし穴3選#
落とし穴1:iPadがPCと判定される問題#
iPadOS 13以降、iPadのSafariはデスクトップ版のUser-Agentを送信するようになりました。
# iPadOS 13以降のUA(macOSと区別がつかない!)
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 ...つまり、User-Agentだけでは iPadとMacBookを区別できません。
対策:タッチイベントとの併用#
// Client Component側で判定する場合
function isIPad(): boolean {
return (
navigator.userAgent.includes("Macintosh") &&
"ontouchend" in document
);
}サーバーサイドでは完全な判定は不可能なため、iPadの判定が必須な場合はClient Componentでの判定を併用してください。
落とし穴2:Botのアクセスでリダイレクトしてしまう#
GooglebotなどのクローラーもUser-Agentを持っています。Middleware でBot判定を行わないと、Googleのクローラーまでアプリストアにリダイレクトされ、ページがインデックスされないという事態になります。
// Botを除外する判定を追加
function isBot(ua: string): boolean {
return /Googlebot|bingbot|Slurp|DuckDuckBot|Baiduspider|YandexBot|facebookexternalhit|Twitterbot/i.test(ua);
}
export function middleware(request: NextRequest) {
const ua = request.headers.get("user-agent") ?? "";
// Botはリダイレクトしない
if (isBot(ua)) return NextResponse.next();
// 以下、通常のデバイス判定...
}落とし穴3:User-Agentは偽装可能#
User-Agentはブラウザの設定やDevToolsで簡単に変更できます。そのため、セキュリティに関わる判定(認証・認可)にUser-Agentを使ってはいけません。
あくまでUXの最適化(UIの出し分け、ストアへの誘導)に留めるべきです。
Client Hints APIとの比較#
User-Agentの代替として、GoogleはChrome 89以降で User-Agent Client Hints(UA-CH) を推進しています。
| 比較項目 | User-Agent | Client Hints API |
|---|---|---|
| ブラウザ対応 | すべてのブラウザ | Chrome/Edge系のみ |
| 情報の精度 | OS・ブラウザ・デバイスが混在 | 構造化されたデータで取得 |
| プライバシー | 詳細情報が常に送信される | 必要な情報のみリクエスト |
| 将来性 | 段階的に情報が削減される予定 | Googleが推進する後継仕様 |
// Client Hints の利用例(Middleware)
export function middleware(request: NextRequest) {
const platform = request.headers.get("sec-ch-ua-platform"); // "Android", "iOS", "macOS" 等
const isMobile = request.headers.get("sec-ch-ua-mobile"); // "?1" or "?0"
// Chrome/Edge系でのみ取得可能
if (platform && isMobile) {
// 構造化データで正確に判定できる
}
}現時点での結論:Safari(iOS)がClient Hintsに対応していないため、iOSの判定にはUser-Agentが依然として必須です。Chrome系ブラウザのみを対象とする場合はClient Hintsを優先し、全ブラウザ対応が必要な場合はUser-Agentをベースにしましょう。
Chrome DevToolsでテストする方法#
実機がなくても、Chrome DevToolsでUser-Agentを変更してテストできます。
- F12(またはCmd+Option+I)でDevToolsを開く
- Network conditions タブを開く(見つからない場合は
...→ More tools) - User agent セクションで「Use browser default」のチェックを外す
- プルダウンから任意のデバイス(iPhone, Android等)を選択
- ページをリロードして動作を確認
これにより、サーバーサイドの判定ロジックが正しく動作するか、実機なしで検証できます。
まとめ#
| ポイント | 内容 |
|---|---|
| 取得方法 | Server Component: headers() / Middleware: request.headers |
| 判定対象 | iPhone, Android, iPad, PC, Bot |
| 注意点 | iPadOS 13以降はMacと同じUA。Bot除外は必須 |
| テスト | Chrome DevToolsのNetwork conditionsでUA変更 |
| 将来 | Client Hints APIが後継だがSafari未対応 |
User-Agentは万能ではありませんが、正しく使えばUXの大幅な向上につながります。落とし穴を理解した上で、適材適所で活用してください。
よくある質問(FAQ)#
Q. Client ComponentでUser-Agentを取得できますか?#
navigator.userAgent でクライアント側のUser-Agentを取得できます。ただし、SSR時にはwindowオブジェクトが存在しないため、useEffect 内で取得する必要があります。
Q. User-Agent判定の精度はどの程度ですか?#
一般的なiPhone・Androidスマホの判定は99%以上の精度で可能です。ただし、iPadOS 13以降のiPadや、カスタムブラウザ(Brave等)では誤判定が発生する可能性があります。
Q. Next.js Pages Routerでも同じ方法が使えますか?#
Pages Routerでは getServerSideProps の context.req.headers['user-agent'] でUser-Agentを取得できます。Middlewareの使い方はApp Routerと同じです。











