7 min read

Building a Blog with Nuxt 3

Nuxt 3 is the latest major release of the popular Vue.js framework Nuxt.js. It comes with several improvements and new features that make it easier than ever to build fast, SEO-friendly web applications. In this comprehensive tutorial, we will walk through how to build a blog from scratch using Nuxt 3.

Overview of Nuxt 3

Nuxt 3 is a complete rewrite of Nuxt.js using Vue 3 and Vite under the hood. Some of the key improvements and new features in Nuxt 3 include:

  • Built on top of Vite for faster development and HMR
  • Uses Vue 3 with Composition API as the default
  • File-based routing system
  • Modules are renamed to App Services for better semantics
  • New Schema-based page fetching with AsyncData 2.0
  • Auto importing of components
  • Improved developer experience with IntelliSense, autocompletion etc.

Overall, Nuxt 3 provides a more modular and flexible architecture while retaining the core features that made Nuxt popular - server-side rendering, powerful routing, and static site generation.

Project Setup

Let's start by creating a new Nuxt 3 project:

npm create nuxt-app nuxt-blog

Choose the following options during project setup:

  • Package manager: Npm
  • UI framework: None
  • Nuxt modules: Content
  • Version control system: Git

This will generate a basic Nuxt 3 project structure with the content module installed. The content module allows managing Markdown/JSON/YAML/CSV content seamlessly in Nuxt apps. We'll use it later for our blog posts.

Next, install the dependencies:

cd nuxt-blog
npm install

and run the dev server:

npm run dev

Our basic Nuxt blog skeleton is now ready!

Routes and Pages

For any blog or content site, routes and pages form the core. Nuxt offers a file-system based routing system that makes it easy to organize the app pages.

First, let's setup the main routes needed for our blog:

  • / - The homepage
  • /posts - List all blog posts
  • /posts/:slug - Individual blog post page
  • /about - Simple about us page

To define these routes, we can create corresponding .vue pages inside the pages/ directory:

pages/
  |- index.vue
  |- posts/
    |- index.vue
  |- posts/_slug.vue
  |- about.vue

Nuxt will automatically map these component files to routes as per the file structure.

Let's add some basic content to these pages for now:

index.vue

<template>
  <div>
    <h1>Homepage</h1>
  </div>
</template>

posts/index.vue

<template>
  <div>
    <h1>Blog Posts</h1>
  </div>
</template>

posts/_slug.vue

<template>
  <div>
    <h1>Post Page</h1>
  </div>
</template>

about.vue

<template>
  <div>
    <h1>About Us</h1>
  </div>
</template>

Now if you navigate to http://localhost:3000 and click around, you'll see the basic routes in action!

Layouts

Every site needs a consistent layout across pages. In Nuxt, layouts can be defined as components and wrapped around page components.

Let's create a layout in layouts/default.vue:

<template>
  <div>
    <header>
      <NuxtLink to="/">Home</NuxtLink>
      <NuxtLink to="/about">About</NuxtLink>
    </header>
    <slot />
  </div>
</template>

This adds a header with navigation links and uses the <slot/> component to display the page components.

To apply this layout, open nuxt.config.js and update the layouts section:

export default {
  layouts: {
    default: "layouts/default",
  },
};

Now our layout will be applied across all routes!

Dynamic Routes and Params

We defined the posts/_slug.vue route using an underscore prefix to denote that it's a dynamic route. This allows capturing the post slug from the url.

For example, /posts/my-first-post will be mapped to posts/_slug.vue and the slug param will be passed in the context object.

To access it, first we need to export the route definition from pages/posts/_slug.vue:

export default {
  // bind slug param to props
  props: ["slug"],
};

Then access it in the template:

<template>
  <div>
    <h1>Post: {{ slug }}</h1>
  </div>
</template>

Now when you navigate to /posts/test, you can see the slug param being passed properly!

Global Styling and Assets

For any blog or website, consistent styling across pages is important. Nuxt provides different ways to add styles - CSS files, pre-processors like Sass, CSS-in-JS etc.

For this tutorial, let's keep it simple and add a global CSS file. Create a assets/main.css file:

body {
  font-family: "Segoe UI", sans-serif;
  padding: 20px;
  max-width: 600px;
  margin: 0 auto;
}
a {
  text-decoration: none;
  color: teal;
}

To include this CSS file globally, open nuxt.config.js and update the css section:

export default {
  css: ["~/assets/main.css"],
};

This will automatically import it across all pages.

We can also add static image assets that can be imported into components later. For example, create an assets/images folder with a placeholder logo logo.png.

Blog Post Content with Markdown

It's time to create our blog content! For a real app, content will come from a headless CMS. But for this demo, we'll manage posts as Markdown files manually.

The content module we installed earlier allows us to work easily with Markdown and other content formats. To manage Markdown content for our blog posts, first create a content/posts folder.

Then add a sample Markdown file first-post.md:

---
title: My First Blog Post
description: Learning how to use @nuxt/content to create a blog
img: first-post.jpg
alt: my first post
---
# My First Blog Post
Welcome to my first blog post using content module

Some key things to note:

  • The top contains YAML front matter for metadata
  • The body has actual Markdown content

Let's add one more sample post second-post.md:

---
title: My Second Blog Post
description: Creating a simple blog with Nuxt
img: second-post.jpg
alt: my second post
---
# My Second Blog Post
This is the second blog post. I'm learning Nuxt Content :)

Now we have some dummy content for our blog. Later we'll learn how to query and display these posts dynamically.

Homepage and Blog Listing

Let's build out the rest of the site now that routes, layouts, assets and content are ready.

First, update the homepage at pages/index.vue to show a list of recent blog posts:

<template>
  <div>
    <h1>My Nuxt Blog</h1>
    <div v-for="post in posts">
      <NuxtLink :to="`/posts/${post.slug}`"> {{ post.title }} </NuxtLink>
    </div>
  </div>
  <!-- fetch posts -->
</template>
<script>
  export default {
    async asyncData() {
      const posts = await queryContent("/posts")
        .only(["title", "slug"])
        .sortBy("createdAt", "desc")
        .limit(5)
        .find();
      return {
        posts,
      };
    },
  };
</script>

We are using asyncData to query only needed fields from Markdown content, sort by latest, and limit to 5 posts.

This will list post titles linked to each post route dynamically!

For the main posts list page, update posts/index.vue similarly:

<template>
  <div>
    <h1>All Blog Posts</h1>
    <div v-for="post in posts">
      <NuxtLink :to="`/posts/${post.slug}`"> {{ post.title }} </NuxtLink>
    </div>
  </div>
  <!-- fetch posts -->
</template>
<script>
  export default {
    async asyncData() {
      const posts = await queryContent("/posts")
        .sortBy("createdAt", "desc")
        .find();
      return {
        posts,
      };
    },
  };
</script>

This will display a full list of blog posts sorted by latest.

Post Page

Now let's display the full post content on post pages at posts/_slug.vue.

To get the post data based on the slug, we use:

const post = await queryContent("/posts")
  .where({ slug: context.params.slug })
  .findOne();

And render it:

<template>
  <div>
    <h1>{{ post.title }}</h1>
    <p>{{ post.description }}</p>
    <img :src="post.img" :alt="post.alt" />
    <div v-html="$md.render(post.body)"></div>
  </div>
  <!-- fetch post -->
</template>
<script>
  export default {
    async asyncData(context) {
      const post = await queryContent("/posts")
        .where({ slug: context.params.slug })
        .findOne();
      return {
        post,
      };
    },
  };
</script>

We're using the $md helper provided by content module to render the Markdown into HTML.

This will display the full post content and data on each post page!

About Page

Lastly, let's add some dummy content to the About page at pages/about.vue:

<template>
  <div>
    <h1>About My Blog</h1>
    <p>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras et lectus in
      quam convallis cursus a non lacus. Nam rhoncus purus id erat consectetur
      euismod.
    </p>
  </div>
</template>

And that completes all the core pages for our blog!

Recap

In this tutorial, we learned how to:

  • Create a Nuxt 3 project from scratch
  • Define routes and basic page structure
  • Use layouts for consistent header/footer
  • Manage assets like CSS and images
  • Leverage content module for Markdown content
  • Programmatically render posts on homepage and listing
  • Display full post content on post page
  • Build out other app pages like About

While this covers the fundamentals, here are some ideas to improve the blog further:

  • Install Tailwind CSS for styling
  • Add pagination to posts list
  • Implement search and filters
  • Build an admin UI to manage posts
  • Deploy to production using Vercel/Netlify

Nuxt 3 provides a fantastic developer experience and allows quickly building fast, scalable apps. I hope you enjoyed learning how to create a blog with Nuxt 3! Let me know if you have any other Nuxt topics you'd like me to cover.