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 beforeEach chỉ 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.ts auto-run mọi route, không global thì phải register qua definePageMeta."

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

ModeServer renderClient hydrateKhi nào
SSR (default)Mỗi requestApp có data động, SEO quan trọng
SSG / PrerenderBuild timeMarketing site, docs, blog ổn định
ISRBuild time + revalidateContent đổi định kỳ (blog, product)
SWRCache request đầuTrang dynamic không cần realtime
SPAFull clientDashboard 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>
<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>

© 2026 .NET Fresher Guide. All rights reserved.

Về Trang Web

Hướng dẫn toàn diện để chuẩn bị phỏng vấn vị trí .NET Fresher với nội dung từ lý thuyết đến thực hành.