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.

Create a free account to save this page, take notes and track your progress. Sign in

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].vue is a dynamic segment read with useRoute().params.
  • app/layouts/default.vue wraps pages through <NuxtLayout>; <NuxtLink> navigates client-side after the first load.
  • useSeoMeta sets <title> and meta tags during server rendering, so crawlers and link previews see them.
  • throw createError({ statusCode: 404, ... }) in a page renders app/error.vue with that status code (Nuxt renders the whole page before sending, so the status is correct).

The code

Layouts, dynamic routes, SEO metadata and 404s
// 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-page

Output

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 createError with 404.
  • Using plain <a> tags for internal links, causing full page reloads; use <NuxtLink>.
  • Setting titles in onMounted, which crawlers don't see; use useSeoMeta in setup.
  • Putting page-specific markup in app.vue instead 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.

Was this page helpful?