Building a Blog with Nuxt File-Based Routing: Conventions That Save Configuration
Nuxt's file-based routing turns a directory of Vue components into a working blog without manual route config. Pair it with @nuxt/content for Markdown-driven posts and static generation for fast, SEO-friendly pages.
30 Dec 2025, 03:30 UTC

The Problem: Manual Route Configuration Doesn't Scale
Content-heavy sites like blogs accumulate routes quickly. Defining each route in a central router file works for a handful of pages but becomes brittle when authors add new posts daily. Nuxt's file-based routing eliminates this maintenance burden by deriving routes directly from the pages/ directory structure.
How the Convention Maps Files to URLs
Nuxt 3 treats every .vue file under pages/ as a route. The mapping follows predictable rules:
pages/index.vue→/pages/about.vue→/aboutpages/blog/index.vue→/blogpages/blog/[slug].vue→/blog/:slug(dynamic segment)pages/blog/[...catchAll].vue→/blog/*(catch-all)
For a blog, the dynamic segment [slug].vue is the workhorse. Create pages/blog/[slug].vue and Nuxt generates a route for every slug you provide at build time or request time.
Fetching Content Per Route with useAsyncData
Each blog post needs its own data. The useAsyncData composable runs on the server during SSR and again on the client during hydration, returning a reactive result you can use directly in the template.
<script setup lang="ts">
const route = useRoute()
const { data: post } = await useAsyncData(`post-${route.params.slug}`, async () => {
const { $content } = useNuxtApp()
return await $content('blog', route.params.slug as string).fetch()
})
</script>
<template>
<article v-if="post">
<h1>{{ post.title }}</h1>
<time :datetime="post.date">{{ post.date }}</time>
<ContentRenderer :value="post" />
</article>
</template>
The key `post-${route.params.slug}` ensures each slug gets its own cache entry. Without a unique key, Nuxt would deduplicate the fetch across different slugs—a common pitfall.
Static Generation for SEO and Speed
Run npx nuxt generate (or nuxt generate with nitro: { preset: 'static' } in nuxt.config.ts) to pre-render every route into static HTML. The output lands in .output/public/ (Nuxt 3) or dist/ (Nuxt 2). For a blog with 200 posts, you get 200 HTML files plus a payload JSON for client-side navigation.
Configure the content source in nuxt.config.ts:
export default defineNuxtConfig({
modules: ['@nuxt/content'],
content: {
sources: {
blog: {
driver: 'fs',
base: 'content/blog'
}
}
},
nitro: {
preset: 'static'
}
})
Place Markdown files at content/blog/my-first-post.md with front-matter:
---
title: "My First Post"
date: "2024-01-15"
tags: ["nuxt", "vue"]
---
## Introduction
Nuxt file-based routing makes this easy...
The @nuxt/content module parses front-matter, transforms Markdown to a JSON AST, and exposes it via $content—no extra API layer needed.
Trade-off: Time to First Byte on Dynamic Routes
Static generation solves TTFB for known slugs. But if you allow arbitrary slugs (e.g., user-generated content) or skip pre-rendering, useAsyncData runs on every request. The server must fetch and render before sending HTML, adding latency. Mitigations:
- Enable Nitro route rules with
swr: truefor stale-while-revalidate caching. - Use a CDN edge cache with
Cache-Control: s-maxage=60, stale-while-revalidate=300. - Pre-render high-traffic slugs; fall back to SSR for the long tail.
Verify the behavior: open DevTools Network tab, request a blog post, and check the x-nitro-cache header or response timing. A cache hit should show hit and sub-100ms TTFB.
Verify Your Setup
- Run
npx nuxt generate. - Inspect
.output/public/blog/my-first-post/index.html—it should contain the rendered post HTML, not a loading skeleton. - Check
.output/public/_payload.jsonfor the JSON payload used during client navigation. - Deploy the
.output/publicfolder to any static host (Netlify, Cloudflare Pages, S3+CloudFront).
If the generated HTML matches your template and includes the post content, the pipeline works. If you see <div id="__nuxt"></div> only, SSR didn't run—check ssr: true in nuxt.config.ts and ensure useAsyncData isn't returning a client-only promise.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.