File-based routing
Turn files in app/pages into routes, read dynamic and catch-all segments with useRoute(), nest pages with NuxtPage, and send a real 404 when content is missing.
What you'll learn
When you finish, you can do these.- 1Create a page and its URL with one file in app/pages
- 2Read a post's short name from its URL with useRoute()
- 3Send a real 404 when a post or a doc is missing
- 4Keep a post list on screen while the post changes
The Corner Blog, a small blog about Nuxt, needs a home page, an about page and one page per post, each at its own URL, such as /posts/hello-nuxt. A URL and the page it shows make a route. In a plain Vue app you usually write every route by hand in a router file, and keep that list in step with your pages.
In Nuxt the folder app/pages/ is that list: add a file and its URL exists, rename the file and the URL changes with it. You need one idea from the lesson A Nuxt project tour: Nuxt finds your files by the folder they sit in, so a helper in app/utils/ works without an import line. The Code tab holds the blog's pages and its sample content; by the end you can say which URL each page answers.
Files become URLs
Where a file sits in app/pages/ decides its URL, so each page of the blog is one file. app/pages/index.vue answers /, the home page. A folder adds a segment, one part of the URL between slashes, so app/pages/posts/index.vue answers /posts.
Square brackets make a dynamic segment, a part of the URL that changes. app/pages/posts/[slug].vue matches /posts/hello-nuxt, /posts/file-routing and any other single segment after /posts/. That segment is the post's slug, its short name in the URL, and the page reads it from useRoute().params.slug. useRoute(), a function Nuxt imports for you, returns the current route.
- app/pages/posts/[slug].vue
- /posts/:slug
- /posts/hello-nuxt
- params.slughello-nuxt
The post page in the Code tab reads the slug and looks up the post with it:
<script setup lang="ts">
const route = useRoute()
const post = findPost(String(route.params.slug))
if (!post) {
throw createError({ status: 404, statusText: 'Post not found' })
}
</script>
<template>
<article v-if="post">
<h2>{{ post.title }}</h2>
<p>{{ post.body }}</p>
</article>
</template>
findPost() comes from app/utils/content.ts and searches the sample posts in the same file. At /posts/hello-nuxt the page shows the heading Hello Nuxt and the line Our first post.
When no post has that slug, the page throws an error made with Nuxt's createError(), with status 404, the number a server sends for a missing page.
The home page links to the first post with <NuxtLink to="/posts/hello-nuxt">. A <NuxtLink> renders a normal <a>, but a click swaps the page without a full reload.
Catch-all pages and real 404s
One page can answer URLs with any number of segments, as long as it sends a real 404 for the ones it cannot fill. The blog also has docs, short guides filed in sections, such as an install guide at /docs/getting-started/install. app/utils/content.ts keeps each doc's text in docs, under its path.
Three dots inside the brackets make a catch-all segment. app/pages/docs/[...slug].vue matches that URL and any other under /docs, however deep, and params.slug is an array of the segments: ['getting-started', 'install']. The docs page joins them back into a path and looks up the text:
<script setup lang="ts">
const route = useRoute()
const parts = route.params.slug as string[]
const text = docs[parts.join('/')]
if (!text) {
throw createError({ status: 404, statusText: 'Doc not found' })
}
</script>
At /docs/getting-started/install its template, in the Code tab, shows the heading getting-started / install, and under it the text Run npm create nuxt@latest.
When no page matches a URL, Nuxt shows its error page with a 404 status, as a visit to /nope shows. A catch-all changes that, because it matches every URL below it, /docs/missing included. If the doc is missing and the page just says "Not found", the server still answers 200 OK, the status of a working page. That is a soft 404: search engines index the empty page as if it were real.
Throw createError({ status: 404 }) when the content is missing, as the docs page does, and the server answers with Nuxt's error page and a 404 status. A catch-all at the root, app/pages/[...slug].vue, needs the same throw: it receives every URL no other page matches, typos included.
Nested pages, groups and named views
A page can sit inside another page, so the post list stays on screen while the post changes, like a mail app with its inbox beside the open message. A file and a folder with the same name make a parent and its children: app/pages/posts.vue next to the folder app/pages/posts/ wraps both /posts and /posts/hello-nuxt.
The child renders where the parent places <NuxtPage />. In app/app.vue, the component around every page, that tag shows the current page. In a parent page it is the outlet, the spot where the child appears. posts.vue lists the sample posts from app/utils/content.ts and places the outlet under the list:
<template>
<section class="posts">
<h1>Posts</h1>
<ul>
<li v-for="post in posts" :key="post.slug">
<NuxtLink :to="`/posts/${post.slug}`">{{ post.title }}</NuxtLink>
</li>
</ul>
<NuxtPage />
</section>
</template>
At /posts/hello-nuxt the post appears under the list, and a click on the second post, Routing with files, changes only the post. At /posts the child is posts/index.vue, which asks the reader to pick a post.
A folder in parentheses groups files without adding to the URL, which keeps a big folder tidy. The Code tab keeps the about page in a (marketing) folder, and app/pages/(marketing)/about.vue answers /about, as app/pages/about.vue would.
What goes wrong
Two mistakes leave part of the blog blank, and in development the console names each one.
The first is a parent page without an outlet, which hides its children:
<template>
<section class="posts">
<h1>Posts</h1>
<ul>
<li v-for="post in posts" :key="post.slug">
<NuxtLink :to="`/posts/${post.slug}`">{{ post.title }}</NuxtLink>
</li>
</ul>
</section>
</template>
/posts/hello-nuxt shows the list and nothing else. The URL matched the child, but the parent never renders it. The console warns with NUXT_E4016: the parent page "does not render <NuxtPage />, so the nested page cannot be displayed". Add the outlet back:
<template>
<section class="posts">
<h1>Posts</h1>
<ul>...</ul>
<NuxtPage />
</section>
</template>
The post appears under the list again.
The second is a page with two root elements. A page needs exactly one root element, one element that wraps everything in its template, because Nuxt runs page transitions, the animations between pages, on it. An HTML comment above that element counts as a second root. Here the about page has lost the <main> that wrapped it:
<template>
<h1>About</h1>
<p>A blog about Nuxt.</p>
</template>
Without page transitions it shows correctly. Turn them on with app: { pageTransition: { name: 'page', mode: 'out-in' } } in nuxt.config.ts, the settings file, and the page still opens, but the console warns with NUXT_E4004 that the file "does not have a single root node and will cause errors when navigating between routes". Leave the page with Home in the menu at the top, and the page area stays empty. Wrap the content:
<template>
<main>
<h1>About</h1>
<p>A blog about Nuxt.</p>
</main>
</template>
With one root element, the warning is gone and the Home link opens the home page.
What to remember
- Every file in
app/pages/is a route, and each folder adds a segment to its URL. A folder in parentheses groups pages without changing their URLs. [slug]makes a dynamic segment you read fromuseRoute().params, and[...slug]catches the rest as an array.- A catch-all answers 200 for every URL it matches. Throw
createError({ status: 404 })when the content is missing. - A parent page such as
posts.vueshows its children only where it places<NuxtPage />. - A page needs one root element, or page transitions break when a link leaves it.
In your project
Do these in a real project. Each one says how to confirm it worked.- Add
app/pages/(marketing)/pricing.vuewith a single<main>element, and a<NuxtLink to="/pricing">inapp/app.vue. The link opens/pricing, with nomarketingin the URL, and the Pages tab of Nuxt DevTools (Shift + Alt + D) lists the new route. - Delete
<NuxtPage />fromapp/pages/posts.vueand open/posts/hello-nuxt. Only the list shows, and the console printsNUXT_E4016. Put the line back and the post appears under the list. - Type
/nopeand then/docs/missinginto the address bar with the browser's Network tab open: the page request has status 404 both times. Comment out thethrowinapp/pages/docs/[...slug].vueand reload/docs/missing: the status is now 200, a soft 404.