Nuxt 4
Routing và Rendering modes
File-based routing, layouts, middleware, SSR / SSG / SPA / Hybrid rendering — chọn đúng cho app.
1. File-based routing
Trong Nuxt, mỗi .vue trong app/pages/ = 1 route. Không cần config router.
Mapping cơ bản
app/pages/
├── index.vue → /
├── about.vue → /about
├── contact.vue → /contact
├── users/
│ ├── index.vue → /users
│ └── [id].vue → /users/123 (dynamic param)
├── blog/
│ ├── [...slug].vue → /blog/a/b/c (catch-all)
│ └── index.vue → /blog
└── admin/
└── [[tab]].vue → /admin or /admin/users (optional)
Dynamic param
<!-- app/pages/users/[id].vue -->
<template>
<div>User ID: {{ route.params.id }}</div>
</template>
<script setup lang="ts">
const route = useRoute();
// route.params.id — string | string[]
const { data: user } = await useFetch(`/api/users/${route.params.id}`);
</script>
Catch-all [...slug]
<!-- /blog/a/b/c → params.slug = ["a", "b", "c"] -->
<script setup lang="ts">
const route = useRoute();
const path = (route.params.slug as string[]).join("/");
</script>
Nested route + layout
app/pages/
├── parent.vue ← parent page
└── parent/ ← nested
├── child-a.vue → /parent/child-a
└── child-b.vue → /parent/child-b
<!-- app/pages/parent.vue -->
<template>
<div>
<h1>Parent</h1>
<NuxtPage /> <!-- render child route ở đây -->
</div>
</template>
2. Navigation
<!-- Declarative — <NuxtLink> -->
<template>
<NuxtLink to="/users/123">View user</NuxtLink>
<NuxtLink :to="{ name: 'users-id', params: { id: 42 } }">By name</NuxtLink>
<NuxtLink to="/login" external>External link</NuxtLink>
</template>
<!-- Programmatic -->
<script setup lang="ts">
const router = useRouter();
function goHome() { router.push("/"); }
// Hoặc dùng navigateTo (cả server + client)
async function login() {
await $fetch("/api/login", { method: "POST" });
return navigateTo("/dashboard");
}
</script>
3. Layouts
<!-- app/layouts/default.vue -->
<template>
<div class="layout">
<Header />
<main>
<slot /> <!-- page nội dung -->
</main>
<Footer />
</div>
</template>
<!-- app/layouts/admin.vue -->
<template>
<div class="admin-layout">
<AdminSidebar />
<main><slot /></main>
</div>
</template>
<!-- app/pages/admin/users.vue — chọn layout admin -->
<script setup lang="ts">
definePageMeta({ layout: "admin" });
</script>
Hoặc dynamic:
<NuxtLayout :name="layoutName">
<NuxtPage />
</NuxtLayout>
4. Route middleware
// app/middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const user = useState("user");
if (!user.value && to.path !== "/login") {
return navigateTo("/login");
}
});
Áp dụng
<!-- Per-page -->
<script setup lang="ts">
definePageMeta({ middleware: "auth" });
</script>
<!-- Hoặc global — auto chạy cho mọi route -->
<!-- app/middleware/log.global.ts -->
// app/middleware/log.global.ts — auto chạy mọi route
export default defineNuxtRouteMiddleware((to) => {
console.log("Navigating to:", to.path);
});
⭐ Câu PV:"Middleware Nuxt khác Vue Router beforeEach chỗ nào?"
"Vue Router
beforeEachchỉ chạy ở client. Nuxt middleware chạy cả server (SSR) lẫn client navigation — đảm bảo logic auth consistent ở mọi render mode. File*.global.tsauto-run mọi route, không global thì phải register quadefinePageMeta."
5. definePageMeta — config page
<script setup lang="ts">
definePageMeta({
layout: "admin",
middleware: ["auth", "admin-only"],
validate: async (route) => {
return /^\d+$/.test(route.params.id); // chỉ accept số
},
scrollToTop: true,
pageTransition: { name: "slide", mode: "out-in" },
});
</script>
6. Rendering modes — 4 lựa chọn
Nuxt mạnh ở chỗ mỗi route có thể chọn rendering mode khác nhau.
SSR (Server-Side Rendering) — default
// nuxt.config.ts
export default defineNuxtConfig({
ssr: true // default
});
Mỗi request → server render HTML → trả về browser → hydrate JS. SEO tốt, first paint nhanh.
SSG / Static (Prerender)
# Build ra static HTML cho mọi page
npx nuxt generate
# Deploy thư mục .output/public — dùng Netlify, Cloudflare Pages, GitHub Pages
// Prerender per-route
export default defineNuxtConfig({
routeRules: {
"/": { prerender: true },
"/about": { prerender: true },
"/blog/**": { prerender: true },
}
});
SPA (Client-only)
export default defineNuxtConfig({
ssr: false // toàn app là SPA
});
Hoặc per-route:
export default defineNuxtConfig({
routeRules: {
"/dashboard/**": { ssr: false } // dashboard là SPA, public route SSR
}
});
Hybrid rendering — routeRules
⭐ Hybrid rendering là sức mạnh lớn nhất Nuxt 4 — mỗi route mode riêng:
export default defineNuxtConfig({
routeRules: {
// Static — prerender ở build, serve CDN
"/": { prerender: true },
"/about": { prerender: true },
// ISR — regenerate sau N giây (như Next.js ISR)
"/blog/**": { isr: 600 }, // cache 10 phút
// SWR — stale-while-revalidate
"/news/**": { swr: 60 },
// SPA only — không SSR
"/admin/**": { ssr: false },
// Server only — không bundle JS client
"/api/**": { cors: true },
// Custom headers
"/legal/*": {
headers: { "Cache-Control": "max-age=31536000" }
},
// Redirect
"/old-page": { redirect: "/new-page" },
}
});
So sánh modes
| Mode | Server render | Client hydrate | Khi nào |
|---|---|---|---|
| SSR (default) | Mỗi request | ✅ | App có data động, SEO quan trọng |
| SSG / Prerender | Build time | ✅ | Marketing site, docs, blog ổn định |
| ISR | Build time + revalidate | ✅ | Content đổi định kỳ (blog, product) |
| SWR | Cache request đầu | ✅ | Trang dynamic không cần realtime |
| SPA | ❌ | Full client | Dashboard auth-gated, không cần SEO |
7. Error pages
<!-- app/error.vue -->
<template>
<div class="error">
<h1>{{ error.statusCode }}</h1>
<p>{{ error.statusMessage }}</p>
<button @click="handleError">Go home</button>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{ error: { statusCode: number; statusMessage: string } }>();
function handleError() {
clearError({ redirect: "/" });
}
</script>
Throw error trong page/middleware:
// Server
throw createError({ statusCode: 404, statusMessage: "Not Found" });
// Client + Server
throw createError({ statusCode: 401, statusMessage: "Unauthorized", fatal: true });
8. Layout transition + page transition
<script setup lang="ts">
definePageMeta({
pageTransition: { name: "slide", mode: "out-in" },
layoutTransition: { name: "fade" },
});
</script>
<style>
.slide-enter-active, .slide-leave-active { transition: all 0.3s; }
.slide-enter-from { transform: translateX(50px); opacity: 0; }
.slide-leave-to { transform: translateX(-50px); opacity: 0; }
</style>
9. NuxtLink prefetch
<NuxtLink to="/blog/post-1">Read post</NuxtLink>
Mặc định Nuxt prefetch link visible trong viewport — click instant. Tắt:
<NuxtLink to="/heavy" :prefetch="false">Heavy page</NuxtLink>
10. SEO meta
<script setup lang="ts">
useSeoMeta({
title: "Blog post — My App",
description: "Tóm tắt nội dung bài viết",
ogTitle: "Blog post",
ogDescription: "Tóm tắt",
ogImage: "https://example.com/og.png",
ogUrl: "https://example.com/blog/post-1",
twitterCard: "summary_large_image",
});
// Hoặc useHead — generic
useHead({
link: [{ rel: "canonical", href: "https://example.com/blog/post-1" }],
});
</script>
Tiếp theo: Data Fetching & Server Routes