Vue 3 and Nuxt · Building Apps with Nuxt
Nuxt Pages, Layouts, Routing and SEO
Build pages with Nuxt's file-based router, share layouts, read route params, set SEO metadata, and return real 404 pages.
What you'll build
A small Nuxt 4 shop: a layout with navigation, a home page linking to products, a dynamic route /products/[slug] that sets its title and description with useSeoMeta, and a custom error page that Nuxt renders with a real 404 status for unknown products and unknown URLs. Every Nuxt example in this course is built with nuxi build, started from .output, and called over HTTP.
The idea
- Files in
app/pages/become routes;[slug].vueis a dynamic segment read withuseRoute().params. app/layouts/default.vuewraps pages through<NuxtLayout>;<NuxtLink>navigates client-side after the first load.useSeoMetasets<title>and meta tags during server rendering, so crawlers and link previews see them.throw createError({ statusCode: 404, ... })in a page rendersapp/error.vuewith that status code (Nuxt renders the whole page before sending, so the status is correct).
The code
// file: nuxt.config.ts
export default defineNuxtConfig({ compatibilityDate: "2026-10-01", devtools: { enabled: false } });
// file: app/app.vue
<template>
<NuxtLayout><NuxtPage /></NuxtLayout>
</template>
// file: app/layouts/default.vue
<template>
<header><NuxtLink to="/">Kitchen shop</NuxtLink></header>
<main><slot /></main>
</template>
// file: app/utils/catalog.ts
export const catalog: Record<string, { name: string; price: number; summary: string }> = {
mug: { name: "Mug", price: 8.5, summary: "Stoneware mug, 350 ml" },
teapot: { name: "Teapot", price: 24, summary: "Cast iron teapot, 1 litre" },
};
// file: app/pages/index.vue
<script setup lang="ts">
useSeoMeta({ title: "Kitchen shop", description: "Mugs and teapots" });
</script>
<template>
<h1>Products</h1>
<ul>
<li v-for="(product, slug) in catalog" :key="slug">
<NuxtLink :to="`/products/${slug}`">{{ product.name }}</NuxtLink>
</li>
</ul>
</template>
// file: app/pages/products/[slug].vue
<script setup lang="ts">
const route = useRoute();
const product = catalog[route.params.slug as string];
if (!product) throw createError({ statusCode: 404, statusMessage: "Product not found" });
useSeoMeta({ title: `${product.name} | Kitchen shop`, description: product.summary });
</script>
<template>
<h1>{{ product.name }}</h1>
<p>{{ product.summary }}, ${{ product.price.toFixed(2) }}</p>
</template>
// file: app/error.vue
<script setup lang="ts">
import type { NuxtError } from "#app";
defineProps<{ error: NuxtError }>();
</script>
<template>
<main>
<h1>{{ error.statusCode === 404 ? "Page not found" : "Something went wrong" }}</h1>
<p>{{ error.statusMessage }}</p>
<NuxtLink to="/">Back to the shop</NuxtLink>
</main>
</template>
// request: GET /
// request: GET /products/teapot find:"<title>Teapot | Kitchen shop</title>"
// request: GET /products/lamp
// request: GET /no-such-pageOutput
GET / -> 200 Products Mug Teapot GET /products/teapot -> 200 Teapot Cast iron teapot, 1 litre, $24.00 response contains "<title>Teapot | Kitchen shop</title>": yes GET /products/lamp -> 404 Page not found Product not found Back to the shop GET /no-such-page -> 404 Page not found Page not found: /no-such-page Back to the shop
Run this on your computer — see “Get Started” for setup.
Common mistakes
- Rendering "not found" content with status 200; search engines index it as a real page. Throw
createErrorwith 404. - Using plain
<a>tags for internal links, causing full page reloads; use<NuxtLink>. - Setting titles in
onMounted, which crawlers don't see; useuseSeoMetain setup. - Putting page-specific markup in
app.vueinstead of layouts and pages.
Key points
- File-based routes, dynamic segments with useRoute().params.
- Layouts wrap pages; NuxtLink for client navigation.
- useSeoMeta for server-rendered metadata; createError for real 404s.
Further reading: Nuxt routing · SEO and meta · Error handling
Practice questions
How do you make a dynamic route in Nuxt?
Answer
Create a page file with a bracketed segment, such as app/pages/products/[slug].vue, and read the value with useRoute().params.slug.
Why use useSeoMeta instead of setting document.title?
Answer
useSeoMeta runs during server rendering, so the title and meta tags are in the HTML that crawlers and link previews read.
How do you return a 404 status for a missing item?
Answer
Throw createError({ statusCode: 404 }) during setup; Nuxt renders app/error.vue with that status code.
Open in the app to mark lessons complete, take notes and track your progress.