この記事で学ぶこと
Part 5 では、lint・型・テスト・アクセシビリティ(#25)と、作ったものの品質を守る話を続けてきました。ここまで私たちはずっと「素の Vue + Vite」で手を動かしてきました。今回からの Part 6 では、実務で出会う現場の多くが乗っている Nuxt というフレームワークに踏み込みます。まずは、Vue というフレームワークがあるのに、なぜその上にもう1つフレームワークを重ねるのか、から始めます。
- なぜ素の Vue ではなく Nuxt を使う現場が多いのか、理由を2つ説明できる
-
pages/にファイルを置くとページになる仕組み(ファイルベースルーティング)を #13 の手書きルートと比べて説明できる - 全ページ共通のヘッダーやフッターを、レイアウトという1枚にまとめられる
迷ったら🔴だけ確実に(🔴 覚える、🟡 理屈、⚪ 流す)。
この記事のコードと実行結果は、すべて Node.js 20 + Nuxt 4.6.2(内部で Vue 3.5.44 / Vite 8 / Nitro 2.13)で実際にプロジェクトを作り、開発サーバーを起動して curl で応答を確かめたものです。ルートの対応やレイアウトの適用、SSR の有無による応答の違いも、その実測です。
🔴 なぜNuxtか。フレームワークの上のフレームワーク
Vue だけでもアプリは作れます。実際ここまで作ってきました。それなのに Nuxt を重ねる理由は、料理に例えると分かりやすいです。
素の Vue + Vite は、食材と調理器具がひととおり揃った状態です。何をどう組み合わせ、どの棚に何を置き、どんな手順で作るかは、毎回自分で決めます。自由ですが、プロジェクトごとに「ルーターはどう設定する」「ファイルはどこに置く」「サーバーはどう用意する」を考え直すことになります。
Nuxt は、献立とキッチンの配置まで決めた定食屋の厨房です。決まったルール(規約)どおりに材料(ファイル)を所定の棚に置くだけで、料理(ページ)が出てきます。考えることが減り、どのプロジェクトを開いても同じ場所に同じものがある、という状態になります。これが Nuxt を使う1つ目の理由、規約で決まっているから、毎回ゼロから設計しなくていいことです。
2つ目の理由は、サーバー側での下ごしらえ(SSR)が最初から付いてくることです。こちらは記事の後半で実際に効果を測ります。まずは、規約のいちばん分かりやすい例から手を動かします。
🔴 ファイルを置くとページになる
#13 で Vue Router を学んだとき、「どの URL にどのコンポーネントを対応させるか」を routes という配列に自分で書きました。
// #13 でやった、手書きのルート定義
const routes = [
{ path: "/", component: Home },
{ path: "/about", component: About },
{ path: "/users/:id", component: UserDetail },
];
Nuxt では、この対応表を書くファイルがそもそも要りません。app/pages/ というフォルダにファイルを置くと、その置き場所がそのまま URL になるからです。これがファイルベースルーティングです。
図のとおり、app/pages/index.vue を置くと /、app/pages/about.vue を置くと /about になります。実際に3つのページを置いて開発サーバーを起動し、curl でそれぞれの URL を叩いた結果がこれです。
<!-- app/pages/index.vue -->
<template>
<h1>ホーム画面です</h1>
</template>
<!-- app/pages/about.vue -->
<template>
<h1>このアプリについて</h1>
</template>
$ curl -s http://localhost:3000/ → <h1>ホーム画面です</h1>
$ curl -s http://localhost:3000/about → <h1>このアプリについて</h1>
$ curl -s http://localhost:3000/zzz → 404
ファイルを2つ置いただけで、/ と /about の2ページができました。存在しない URL は、こちらが何もしなくても 404 を返します。routes 配列も createRouter も書いていません。#13 で学んだルーターは消えたのではなく、Nuxt がファイルの置き場所を読んで、裏で同じ routes を自動生成しています。
Nuxt 4 では、アプリのソースは app/ フォルダの中に置きます(app/pages/、app/layouts/、app/app.vue)。以前の記事や古い Nuxt 3 のサンプルでは app/ が付かず pages/ が直接ルートにありますが、仕組みは同じです。
🟡 動的ルートは [id].vue
#13 の /users/:id のように、URL の一部を値として受け取るページも、ファイル名で表せます。角かっこで囲んだ [id].vue というファイルがそれです。
<!-- app/pages/users/[id].vue -->
<script setup lang="ts">
const route = useRoute();
</script>
<template>
<h1>ユーザー {{ route.params.id }} の詳細</h1>
</template>
app/pages/users/[id].vue を置くと /users/:id にマッチし、:id の中身は #13 とまったく同じ useRoute().params で取り出せます。実際に叩くとこうなりました。
$ curl -s http://localhost:3000/users/42 → <h1>ユーザー 42 の詳細</h1>
$ curl -s http://localhost:3000/users/7 → <h1>ユーザー 7 の詳細</h1>
[id] の id はただの名前で、[slug].vue にすれば route.params.slug になります。#13 で注意した「同じルートのまま id だけ変わるとコンポーネントは使い回される」「URL の値は常に文字列」という性質も、そっくりそのまま効きます。Nuxt は Vue Router を隠しているだけで、別物を使っているわけではないからです。
ここで useRoute を import せずにいきなり使っている点に気づいたでしょうか。これも Nuxt の規約で、useRoute のような Nuxt / Vue の関数は書いた瞬間に自動で読み込まれます(自動インポート)。import 文の行が減るぶん、ファイルの中身がすっきりします。
🔴 レイアウト。全ページ共通の枠を1枚にまとめる
ページが増えると、どのページにも同じヘッダーとフッターを置きたくなります。各ページにヘッダーをコピペして回ると、ロゴを1つ直すのに全ページを触ることになります。
Nuxt はこれをレイアウトで解決します。app/layouts/default.vue という1枚に共通の枠を書き、各ページが入る場所に <slot /> を開けておきます。
<!-- app/layouts/default.vue -->
<template>
<div>
<header>買い物アプリ(共通ヘッダー)</header>
<main><slot /></main>
<footer>共通フッター</footer>
</div>
</template>
この <slot /> は #9 で学んだスロットと同じものです。#9 では「親が子に中身を差し込む穴」でしたが、ここでは「レイアウトという枠に、ページが差し込まれる穴」になっています。枠は1枚、中身(ページ)は差し替わる、という関係です。
枠とページをつなぐのが、ルートの app/app.vue に書く2つのタグです。
<!-- app/app.vue -->
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
<NuxtPage /> が「今の URL に対応するページ」の差し込み口で、<NuxtLayout> がそれを default.vue の枠で包みます。NuxtLayout も NuxtPage も自動インポートなので import は要りません。この状態でホーム画面を curl で取ると、ページ本体だけでなく共通の枠まで一緒に返ってきました。
$ curl -s http://localhost:3000/
買い物アプリ(共通ヘッダー)
<h1>ホーム画面です</h1>
共通フッター
index.vue には <h1>ホーム画面です</h1> しか書いていないのに、ヘッダーとフッターが付いています。レイアウトの枠にページが差し込まれた証拠です。/about でも同じ枠が付き、変わるのは真ん中の中身だけでした。ロゴを直したいときは default.vue を1回直すだけで全ページに反映されます。
🟡 サーバーがHTMLを作って返す。これがSSR
さきほどの curl の結果を、もう一度よく見てください。curl は JavaScript を実行しません。ただサーバーに「このページをください」と頼んで、返ってきた文字をそのまま表示しただけです。それなのに <h1>ホーム画面です</h1> という完成した中身が返ってきました。
これは、サーバー側で Vue をいったん実行して HTML を組み立ててから送っているからです。これが Nuxt を使う2つ目の理由、SSR(サーバーサイドレンダリング) です。
効果を確かめるために、同じプロジェクトで SSR を切って(nuxt.config.ts に ssr: false)、素の SPA と同じ「ブラウザで組み立てる」動きにしてから、同じ curl を叩いてみました。
# SSRあり(Nuxtの初期設定)
$ curl -s http://localhost:3000/ → 「ホーム画面です」も「共通ヘッダー」も含まれている
# SSRなし(ssr: false にした場合)
$ curl -s http://localhost:3000/ → 「ホーム画面です」も「共通ヘッダー」も含まれていない(空の入れ物だけ)
SSR を切ると、サーバーが返すのは中身の入っていない入れ物だけになり、ページの文字はブラウザが JavaScript を実行して初めて現れます。この違いが効くのは、JavaScript を実行しない相手に見られたときです。検索エンジンのクローラーや、SNS にリンクを貼ったときのプレビュー生成は、まさに curl のように中身を取りに来ます。SSR があれば、その相手にもページの中身が届きます。初回表示が速くなる効果もあります。
SSR / SPA / SSG という描き分けの選択は、それぞれに向き不向きがあり、#28 のレンダリング戦略でまとめて扱います。今回は「Nuxt は初期設定でサーバーが HTML を作って返している」ことだけ、curl で自分の目で確かめられれば十分です。
⚪ nuxt.config.ts という1つの設定場所
Nuxt のもう1つの規約が、設定を nuxt.config.ts という1ファイルに集めることです。さきほど SSR を切るのに触ったのがこれです。
// nuxt.config.ts
export default defineNuxtConfig({
compatibilityDate: "2025-07-15",
devtools: { enabled: true },
});
ルーターの設定、ビルドの設定、使うモジュールの追加などが、すべてこの1ファイルに集まります。素の Vite なら vite.config.ts やルーターの初期化コードなど複数の場所に散っていた設定が、Nuxt では置き場所が決まっています。今は中身を覚える必要はなく、「設定はここを見ればいい」という場所があることだけ知っておけば十分です。
【手を動かす】3ページ + 共通レイアウトのNuxtアプリ
Docker は不要です。Node.js さえ入っていれば動きます。まず新しいフォルダでプロジェクトを作ります。
npm create nuxt@latest shopping
cd shopping
npm run dev
対話で聞かれる項目はそのまま Enter で進めて構いません。できあがった app/app.vue を本文の <NuxtLayout><NuxtPage /></NuxtLayout> に書き換え、次の4ファイルを作ります。
-
app/layouts/default.vue… 本文のヘッダー・<slot />・フッターの枠 -
app/pages/index.vue…<h1>ホーム画面です</h1> -
app/pages/about.vue…<h1>このアプリについて</h1> -
app/pages/users/[id].vue…route.params.idを表示するページ
npm run dev のまま http://localhost:3000/、/about、/users/42 をブラウザで開いてください。ファイルを置いただけで3つの URL が開くこと、どのページにも同じヘッダーとフッターが付くこと、/users/42 と /users/7 で表示が変わることを確かめます。慣れたら app/pages/ に contact.vue を1つ足して、/contact が設定なしで増えることも試してみてください。
確認テスト
知識問題
Q1. Vue というフレームワークがあるのに、その上に Nuxt というフレームワークを重ねて使う現場が多いのはなぜですか。理由を2つ挙げてください。
解答を見る
1つ目は、規約で決まっているぶん、プロジェクトごとにゼロから設計しなくて済むことです。ルーティング、レイアウト、フォルダ構成、ビルド設定などが「こう置けばこう動く」と決まっているので、どのプロジェクトを開いても同じ場所に同じものがあります。定食屋の厨房のように、材料を所定の棚に置くだけで料理が出てくる状態です。
2つ目は、SSR(サーバーサイドレンダリング)が初期設定で付いてくることです。サーバー側で HTML を組み立ててから返すので、JavaScript を実行しない相手(検索エンジンのクローラーや SNS のプレビュー)にもページの中身が届き、初回表示も速くなります。素の Vue + Vite でも同じことは実現できますが、自分で用意する必要があります。
Q2. app/pages/about.vue というファイルを1つ置きました。routes 配列も createRouter も一切書いていません。それでも /about が開けるのはなぜですか。
解答を見る
Nuxt のファイルベースルーティングが、ファイルの置き場所から URL の対応表を自動で作っているからです。app/pages/about.vue という場所が /about という URL を意味する、という規約になっています。
ルーターが消えたわけではありません。#13 で手書きした routes 配列を、Nuxt が app/pages/ の中身を読んで裏で生成しています。だから中身は Vue Router そのもので、useRoute など #13 で学んだものがそのまま使えます。
Q3. curl(JavaScript を実行しないただの HTTP リクエスト)で Nuxt アプリのトップページを取ったら、<h1> の中身まで含まれた完成した HTML が返ってきました。これは何が起きているからですか。また、同じことを素の SPA でやると結果はどう変わりますか。
解答を見る
サーバー側で Vue を実行して HTML を組み立ててから返している(SSR)からです。だから JavaScript を実行しない curl にも、完成した中身が届きます。
素の SPA(またはNuxtでSSRを切った場合)では、サーバーが返すのは中身の入っていない空の入れ物だけになります。ページの文字はブラウザが JavaScript を実行して初めて現れるので、curl の応答にはページの中身が含まれません(本文で ssr: false にして実測したところ、「ホーム画面です」も「共通ヘッダー」も応答に含まれませんでした)。この違いが、検索エンジンや SNS プレビューのように中身だけ取りに来る相手への見え方を分けます。
実技問題
Q4. 全ページの先頭に共通のナビゲーションバー(「ホーム」「このアプリについて」へのリンク)を出したいです。各ページにコピペせず、1か所にまとめる書き方を示してください。リンクは #13 の RouterLink の Nuxt 版である <NuxtLink> を使います。
解答を見る
レイアウト(app/layouts/default.vue)の枠に、<slot /> の外側としてナビを書きます。ページ側には一切書きません。
<!-- app/layouts/default.vue -->
<template>
<div>
<nav>
<NuxtLink to="/">ホーム</NuxtLink>
<NuxtLink to="/about">このアプリについて</NuxtLink>
</nav>
<main><slot /></main>
</div>
</template>
ポイントは3つです。
- 共通で出したいものは
<slot />の外に置く。<slot />の中身はページごとに差し替わるが、外は全ページ共通になる - 各ページ(
index.vueなど)にはナビを書かない。リンクを1本足したいときも、直すのはこの1ファイルだけ - ページ内リンクは
<a>ではなく<NuxtLink>を使う。#13 で学んだとおり、<a>だとページ全体がリロードされてしまう
Q5. 商品一覧ページ /products と、商品ごとの詳細ページ /products/123 の2つを作りたいです。それぞれどのファイルを置けばよいか、パスを答えてください。また、詳細ページで URL の 123 を受け取る1行も書いてください。
解答を見る
一覧は app/pages/products/index.vue、詳細は app/pages/products/[id].vue を置きます。
products/ フォルダの中の index.vue が /products に対応し、同じフォルダの [id].vue が /products/:id に対応します。角かっこのファイル名が動的ルートの目印です。
詳細ページで URL の値を受け取るのは、#13 と同じ useRoute です(Nuxt では import 不要)。
<script setup lang="ts">
const route = useRoute();
// route.params.id に "123" が入る(文字列。数値で使うなら Number() で変換)
</script>
実測では /products/123 を開くと route.params.id は文字列の "123" でした。#4・#13 で触れたとおり URL の一部は常に文字列で届くので、数値として計算に使うときは Number() で変換します。
まとめ
- Nuxt は Vue の上に乗るフレームワーク。規約でフォルダ構成や設定が決まっているので毎回ゼロから設計せずに済み、SSR も初期設定で付いてくる
-
app/pages/にファイルを置くと、その置き場所がそのまま URLになる(ファイルベースルーティング)。routes配列は書かない。裏では #13 の Vue Router が自動生成されている - URL の一部を受け取るページは
[id].vueのように角かっこで表し、値は #13 と同じuseRoute().paramsで取る - 全ページ共通の枠は
app/layouts/default.vueに1枚でまとめ、ページが入る場所に<slot />(#9)を開ける。app/app.vueの<NuxtLayout><NuxtPage /></NuxtLayout>で枠とページがつながる - Nuxt の初期設定ではサーバーが HTML を作って返す(SSR)。
curlで中身まで返ってくることで確かめられる。詳しい描き分けは #28
次回は #27 Nuxt②(useFetch・サーバーAPI・ミドルウェア) です。今回は画面を表示するところまででしたが、実務の画面はサーバーからデータを取ってきて表示します。Nuxt でデータを取る useFetch、Nuxt 自身の中に API を作る仕組み、そして #13 の beforeEach に当たる Nuxt のミドルウェアを手を動かして確かめます。
![Nuxtのファイルベースルーティングとレイアウトを示した図。上段①ファイルベースルーティング。左のapp/pages/フォルダにindex.vue・about.vue・users/[id].vueを置くと、右のブラウザのURLが自動でできる。index.vueは/に、about.vueは/aboutに、users/[id].vueは/users/:idに対応する。ファイルの場所がそのままURLになり、:idはroute.params.idで受け取る。素のVueではcreateRouterにroutes配列を手で書いた(#13)が、Nuxtはこの対応表をファイル名から作るので、ルート定義のファイルがそもそも無い。下段②レイアウト。app/layouts/default.vueが共通の枠で、上に共通ヘッダー(ロゴ・ナビ)、中央に点線のslot(ここに各ページが入る、#9のslot)、下に共通フッターがある。ホーム画面・このアプリについて・ユーザー詳細などどのページも、同じ枠のslotに差し込まれる](https://qiita-user-contents.imgix.net/https%3A%2F%2Fraw.githubusercontent.com%2Fkanata-iketani%2Farticle-assets%2Fmain%2Ff35%2F26-nuxt-routing.png?ixlib=rb-4.1.1&auto=format&gif-q=60&q=75&s=15e6ccafe073690cd36fde8b22ce58a9)