こんにちは、かつコーチです。
前回はlayouts/を使った共通レイアウトの管理方法を解説しました。
今回は一歩進んで「/posts/1のようにIDが変わるページ」を作る動的ルートを扱います。
「ブログ記事の詳細ページってどう作るの」という悩みを、この記事で解決します。
動的ルートとは?
用語の定義
動的ルートとは、URLの一部が固定文字列ではなく、可変の値になっているルートのことです。
たとえば/posts/1や/posts/2のように、記事IDだけが変わるページが該当します。
Nuxtでは、ファイル名を角括弧[ ]で囲むことで、この動的な部分を表現します。
なぜ必要なのか
もし動的ルートがなければ、記事の数だけpages/posts/1.vue、pages/posts/2.vue…とファイルを作る必要があります。
これは記事が増えるたびにファイルを追加する、非現実的な運用です。
動的ルートを使えば、1つのファイルで無限のパターンのURLに対応できます。
基本の書き方
手順1:[id].vueで動的セグメントを作る
pages/posts/[id].vueというファイルを作成します。
<!-- pages/posts/[id].vue -->
<script setup lang="ts">
const route = useRoute()
const postId: string = route.params.id as string
</script>
<template>
<div>
<h2>記事ID: {{ postId }}</h2>
</div>
</template>
/posts/1にアクセスするとroute.params.idに"1"が入り、/posts/99なら"99"が入ります。
手順2:useRouteでパラメータを取得する
パラメータの取得にはuseRoute()というコンポーザブル(再利用可能な処理をまとめた関数)を使います。
<script setup lang="ts">
interface Post {
id: number
title: string
}
const route = useRoute()
const postId = Number(route.params.id)
const { data: post } = await useFetch<Post>(`/api/posts/${postId}`)
</script>
<template>
<div v-if="post">
<h2>{{ post.title }}</h2>
</div>
</template>
取得したIDをAPIのパスに埋め込むことで、記事ごとの詳細データを取得できます。
つまずきやすい設定・注意点:paramsは常に文字列
私が実際にハマったのは、route.params.idの型です。
URLパラメータは常にstring型で渡ってくるため、数値として比較しようとすると失敗します。
const route = useRoute()
// ❌ これは動かない("1" !== 1 のため常にfalse)
if (route.params.id === 1) {
console.log("1番目の記事です")
}
数値として扱いたい場合はNumber()で明示的に変換する必要があります。
const route = useRoute()
const id = Number(route.params.id)
if (id === 1) {
console.log("1番目の記事です")
}
このミスに気づかず「if文の中に絶対入るはずなのに入らない」と、しばらく原因を探した経験があります。
よくあるつまずきポイント・エラー対処
複数の動的セグメントを使うとき
記事のコメント詳細のように、複数のIDが必要なページもあります。
❌ Before:1つのファイルに無理やり詰め込む
pages/posts/[id]-comment-[commentId].vue
Nuxtのファイルベースルーティングでは、このような書き方はエラーになります。
実行すると[nuxt] Invalid routeのようなビルドエラーが表示されました。
✅ After:ネストしたディレクトリ構成にする
pages/
posts/
[id]/
comments/
[commentId].vue
<!-- pages/posts/[id]/comments/[commentId].vue -->
<script setup lang="ts">
const route = useRoute()
const postId = route.params.id as string
const commentId = route.params.commentId as string
</script>
<template>
<p>記事{{ postId }}のコメント{{ commentId }}</p>
</template>
ディレクトリを分けることで、URLは/posts/1/comments/5のように対応します。
複数の動的な値を扱うときは、1ファイルに詰め込まず、ディレクトリ階層で表現しましょう。
応用・一歩先の使い方
ネストされたルートと<NuxtPage/>
一覧ページの中に詳細ページを表示したい場合は、ネストされたルートを使います。
親ページと同名のディレクトリを作り、その中に子ページを置く構成です。
pages/
posts.vue
posts/
[id].vue
<!-- pages/posts.vue(親) -->
<template>
<div>
<h1>記事一覧</h1>
<NuxtPage />
</div>
</template>
親のposts.vue内に<NuxtPage />を置くことで、/posts/1にアクセスしたときに[id].vueの中身がここへ差し込まれます。
一覧のサイドバーを固定したまま、右側だけ詳細内容を切り替える、といったUIが実現できます。
catch-allルート([...slug].vue)
「それ以外の全てのパス」を1つのページで受け取りたい場合は、[...slug].vueという書き方も使えます。
<!-- pages/[...slug].vue -->
<script setup lang="ts">
const route = useRoute()
const slug = route.params.slug as string[]
</script>
<template>
<p>404: {{ slug.join('/') }} は見つかりませんでした</p>
</template>
存在しないURLの受け皿(404ページ)や、階層が可変なドキュメントページなどで活躍します。
まとめ
この記事のポイント
[id].vueのようにファイル名を角括弧で囲むと動的ルートになるroute.paramsで取得できる値は常にstring型なので、数値比較にはNumber()変換が必要- 動的な値が複数あるときは、1ファイルに詰め込まずディレクトリ階層で表現する
- 親ページに
<NuxtPage />を置くとネストされたルートが構成できる [...slug].vueで任意階層のパスをまとめて受け取れる
次に読むべき記事
動的ルートが扱えるようになったら、次はページ遷移を制御するミドルウェアを学んでいきましょう。
→ 次の記事:ルートミドルウェアでページ遷移を制御する
タグ: Nuxt.js, 中級者向け, ルーティング