本記事で紹介する実装例では404ページの恒久的なリダイレクト実装を、Nuxtの/server/plugins自動登録およびNitroのライフサイクルで組み込む際のリスク・落とし穴を丁寧に解説します。
尚、パブリック公開するアプリ開発においてはGoogle検索エンジンに優しい404ハンドリングを意識しなけばいけませんが、SPA開発時はまぁ無視しても良いかなとは感じます。それらの前提も踏まえ、ご覧いただけましたらと思います。
引用:Server Plugins - Nuxt Directory Structure v4
Nuxtは~/server/plugins内のファイルを自動でNitroプラグインとして登録し、Nitroのライフサイクルにフック可能と記載されています
実装を読み解く
以下は、404発生時に強制的に存在するページに恒久的にリダイレクトさせるコード実装例です。前述の、Nuxt4のプラグイン機能を使用しています。
// server/plugins/redirect-404.ts
import { getRequestURL, sendRedirect } from 'h3'
import type { H3Event } from 'h3'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('error', async (error: any, ctx) => {
const event = (ctx as any)?.event as H3Event | undefined
if (!event) return
const status = error?.statusCode ?? error?.status ?? 0
if (status === 404) {
const url = getRequestURL(event)
const accept = event.node.req.headers['accept'] || ''
const method = event.node.req.method || 'GET'
const isHtml = typeof accept === 'string' && accept.includes('text/html')
const isApi = url.pathname.startsWith('/api')
const isRoot = url.pathname === '/'
if (method === 'GET' && isHtml && !isApi && !isRoot) {
return await sendRedirect(event, '/', 302)
}
}
})
})
ポイントは次の通りです。
「error」フックでSSRレベルに介入する理由
Nitroの「error」フックは、サーバー処理中に投げられたエラー(H3エラーを含む)を一元的に捕捉できます。ここにロジックを置くと、Vueのレンダリングより前にHTTPレスポンスを決められるため、サーバー側で即時リダイレクトする設計と相性が良いです。
const event = (ctx as any)?.event as H3Event | undefined
if (!event) return
404のみを対象に限定している理由
status === 404
で分岐しており、他の500系や権限系には触れていません。404だけをリダイレクト対象にすることで、想定外のエラーまでトップに飛ばして原因究明を難しくするリスクを避けています。
if (status === 404) {
「getRequestURL」でのURL判定
getRequestURL(event)
はH3の提供するAPIで、リバースプロキシ(例:Cloudflare、ALBなど)の「X-Forwarded-*」ヘッダーを考慮して完全なURLを生成します。多段プロキシ配下での挙動を安定させるには、このヘルパーを使うのが妥当です。
const url = getRequestURL(event)
「Accept」ヘッダーで「HTML」だけに限定
Accept
にtext/html
が含まれるGETだけを対象にし、APIやアセットの誤作動を避けています。Nuxtのデフォルトでも、致命的なエラー時はAccept: application/json
ならJSON、そうでなければフルスクリーンエラーページを返すため、この判定はフレームワークの方針にも整合しています 。
引用:Error Handling – Nuxt v4
Accept: application/json
ヘッダーのときはJSONを、そうでなければエラーページを返す旨が明記されています
302で送る理由と「sendRedirect」の挙動
sendRedirect(event, '/', 302)
は、Locationヘッダーを付与しデフォルト302で返すユーティリティです。ヘッダーが無視される環境向けに簡易なHTMLのメタリフレッシュもボディに含むため、互換性が高いのが特徴です。恒久移転なら301や308を選ぶ設計もあり得ますが、今回の「存在しないURL→ホーム」は通常一時的扱いのため302が無難です。
「/api」と「/」を除外する安全策
APIはJSONクライアントやWebhookなど非HTML文脈が多く、トップへ飛ばすとクライアントエラーになります。またトップ自身で404が出た場合のループを避けるためルートパスを弾いています。
ここが肝心:SEO観点での是非
サーバー側で404をホームへリダイレクトする実装は、UX上は「とりあえず生きているページに戻す」便利技ですが、検索エンジン的には推奨されません。
Googleは「存在しないページで404/410以外を返す、またはホームにリダイレクトする」ケースを「ソフト404」と呼び、ユーザーにも検索エンジンにも混乱を招き得ると明確に述べています。マーケ系の公開サイトでは、404ページを適切に返しつつ、検索やサイトマップ誘導を行う設計が安全です。
引用:404(ページが見つかりません)エラー - Search Console ヘルプ
存在しないページで404/410以外を返したりホームにリダイレクトしたりするのは「ソフト404」に該当し得ると解説されています
一方、ログイン必須の「アプリ型」体験や、社内向けツールなどクロール前提でない領域では、404→ホームの即時リダイレクトはUX優先の合理的な選択肢になり得ます。つまり「公開サイトのインデックス対象」か「非公開のアプリ」かで方針を分けることが重要です
実運用でハマりやすい点と対処のヒント
以下の注意点は、実際の運用でよく問題化します。チェックの前に結論を一文でまとめると、リダイレクトは「誰に対して」「どの環境で」発火するかを厳密にコントロールする必要があります。
要点 | 詳細 |
---|---|
クローラー向けには404/410を返すべきである | 「User-Agent」に「bot」を含む場合はリダイレクトを抑止するなどの分岐が有効である |
APIや非HTMLのフェッチには絶対に適用しない設計である |
|
HEAD・OPTIONSなどGET以外のメソッドは除外するべきである | GET以外に302を返すとクライアントの期待と齟齬が生じるためである |
リバースプロキシ配下では「X-Forwarded-Proto/Host」を考慮するべきである |
|
SSG/プリレンダリング時は404.htmlの発行戦略を別途設計すべき | ホスティングのルールと整合させないと意図せぬ挙動になり得るためである。 |
改善版のサンプル(ボット除外・HEAD除外・恒久移転の余地)
画像・API・ボットをより厳密に除外し、ヘッダーも少し丁寧に扱う改良例です。用途に応じてステータスコードは調整する必要があります。
// server/plugins/redirect-404-safer.ts
import { getRequestURL, sendRedirect } from 'h3'
import type { H3Event } from 'h3'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('error', async (error: any, ctx) => {
const event = (ctx as any)?.event as H3Event | undefined
if (!event) return
const status = error?.statusCode ?? error?.status ?? 0
if (status !== 404) return
const url = getRequestURL(event) // X-Forwarded-* を考慮
const req = event.node.req
const method = req.method || 'GET'
const accept = String(req.headers['accept'] || '')
const ua = String(req.headers['user-agent'] || '')
// 非対象の早期return
if (method !== 'GET') return
if (!accept.includes('text/html')) return
if (url.pathname.startsWith('/api')) return
if (url.pathname === '/') return
// ボットは正統な404を返す(ソフト404回避)
if (/\b(bot|google|bing|baidu|yandex|duckduck|slurp)\b/i.test(ua)) return
// 一時的扱いなら302、恒久移転なら301/308を選択
return await sendRedirect(event, '/', 302)
})
})
代替アプローチとの比較(いつどれを使うか)
以下では、Nuxtでリダイレクトや404ハンドリングを行う主要手段を並べ、強みと弱みを整理します。
表 Nuxtにおけるリダイレクト/404ハンドリング手段の比較
手段 | どこで動くか | 強み | 注意点・弱み | 代表的な用途 |
---|---|---|---|---|
Nitroプラグイン+「error」フック | サーバー(Nitro) | SSRレベルで一括制御できる。APIや静的アセットを除外しやすい | 設計を誤るとソフト404やAPI破壊のリスク。ボット除外が必須 | 非公開アプリ的サイトの404→ホーム誘導 |
ルートミドルウェア+ | クライアント/SSR両方 | 画面遷移の文脈で柔軟に分岐。ログインガードと相性が良い | 既に404になった後の介入ではない。サーバーコードではないためSEO上の恒久リダイレクトには不向き | 認証リダイレクト、UI都合の分岐 |
| ビルド時に各プラットフォームへ展開 | 宣言的に301/308等のルールを定義でき、SEO的に正しい恒久移転を表現しやすい | ワイルドカードや動的置換の制約がある。未知パス一括対応は不得手 | 旧URL→新URLの恒久移転(301/308) |
| Nuxtアプリ | 404を正しく返しつつサイト内検索や導線を提供できる | リダイレクトはしないためURLは変わらない | 公開サイトのUXとSEOの両立(正統404) |
CDN/ホスティングのリダイレクトルール | エッジ | アプリに到達前に高速にさばける。恒久移転の一元管理がしやすい | プラットフォーム依存。未知パスの扱いは各社仕様に依存 | 大規模サイトの恒久リダイレクトやwww統合、HTTP→HTTPS強制 |
※ 表は各手段の一般的な特徴を比較し、本記事の文脈での使いどころを示したものです
Nuxt・H3の仕様に基づく補足
NuxtのエラーハンドリングはAccept
によりJSON応答とエラーページを切り替えます。このため「HTMLだけにリダイレクト」はフレームワーク方針との整合性が取れています 。sendRedirect
はデフォルト302でLocationを付与し、メタリフレッシュのフォールバックも埋め込みます。クライアントの互換性リスクを下げられます。
尚、Accept
ヘッダーの意味はHTTP標準の「コンテンツネゴシエーション」で、ブラウザは取得対象に応じて値を変えます。ここを根拠に「text/html」かどうかを判断すると安全です。
個人的見解
公開サイトでの404は「正しく404/410を返し、ユーザーには役立つエラーページを出す」のが原則です。今回のような「404→ホームの即時リダイレクト」は、社内向けツールやログイン必須のアプリで「URL直打ち時にもアプリ入口へ戻す」UXを優先したいときに限定的に採用するのが良いと考えます。
もし公開サイトで「旧URL→新URL」の移転を扱うなら、routeRules
やCDNの恒久リダイレクトを選び、404は404として返しておくのが安全です。
まとめ
今回のような「SSRレベルの即時リダイレクト」は強力なツールです。公開サイトでは「404は404で返す」を基本線に、アプリ体験では限定的に使う——この線引きをチームの設計基準として明文化しておくと、後々のトラブルを減らせます。