Creating your own blog with modern web technologies has never been easier. In this tutorial, we'll walk through setting up a blog using Nuxt 3, Nuxt Content module for markdown support, and Tailwind CSS for styling. This powerful combination gives you a fast, SEO-friendly blog with an excellent developer experience.
GitHub Repo
Prerequisites
Before starting, make sure you have:
- Node.js (version 18 or newer)
- npm or yarn
- Basic knowledge of Vue.js
Step 1: Create a New Nuxt Project
First, let's create a new Nuxt project:
npx nuxi@latest init your-blog
cd your-blog
This command creates a new Nuxt 3 project in the your-blog
directory. After the initialization completes, navigate into the project folder.
Step 2: Install Dependencies
Next, we need to install the Nuxt Content module for managing markdown content and Tailwind CSS:
npm install @nuxt/content
npm install tailwindcss @tailwindcss/vite
Update your nuxt.config.ts
file to include the necessary modules and configurations:
import tailwindcss from "@tailwindcss/vite";
export default defineNuxtConfig({
compatibilityDate: "2024-11-01",
devtools: { enabled: true },
modules: ['@nuxt/content'],
vite: {
plugins: [
tailwindcss(),
],
},
});
Step 4: Configure Content Module
Create a content.config.ts
file in the root of your project:
import { defineCollection, defineContentConfig } from '@nuxt/content'
export default defineContentConfig({
collections: {
content: defineCollection({
type: 'page',
source: '**/*.md'
}),
blog: defineCollection({
source: 'blog/*.md',
type: 'page',
})
}
})
This configures the Nuxt Content module to recognize all markdown files as content pages.
Step 5: Create App Layout
First, let's create the main app layout. Create an app.vue
file in the root directory:
// filepath: app.vue
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
This simple template uses Nuxt's layout system and renders the current page.
Step 6: Create Home Page
Create an index.vue
file in the pages
directory:
// filepath: pages/index.vue
<script setup lang="ts">
const { data: home } = await useAsyncData(() => queryCollection('content').path('/').first())
useSeoMeta({
title: home.value?.title,
description: home.value?.description
})
</script>
<template>
<ContentRenderer v-if="home" :value="home" />
<div v-else>Home not found</div>
</template>
This page fetches and renders content from a markdown file at the root of the content directory and uses the metadata for SEO.
Step 7: Set Up Default Layout
Create a default layout file:
// filepath: layouts/default.vue
<template>
<div>
<header class="bg-gray-900 text-white p-4">
<div class="container mx-auto">
<h1 class="text-xl font-bold">My Nuxt Blog</h1>
<nav class="mt-2">
<NuxtLink to="/" class="mr-4 hover:underline">Home</NuxtLink>
<NuxtLink to="/blog" class="hover:underline">Blog</NuxtLink>
</nav>
</div>
</header>
<main class="container mx-auto p-4">
<slot />
</main>
<footer class="bg-gray-200 p-4 mt-8">
<div class="container mx-auto text-center text-gray-700">
© {{ new Date().getFullYear() }} My Blog
</div>
</footer>
</div>
</template>
Create a CSS file to import Tailwind:
// filepath: assets/css/main.css
@import "tailwindcss";
Then, create a tailwind.config.js
file:
module.exports = {
content: [
'./components/**/*.{vue,js}',
'./layouts/**/*.vue',
'./pages/**/*.vue',
'./plugins/**/*.{js,ts}',
'./app.vue',
],
theme: {
extend: {},
},
plugins: [],
}
Finally, update nuxt.config.ts
to include your CSS:
import tailwindcss from "@tailwindcss/vite";
export default defineNuxtConfig({
compatibilityDate: "2024-11-01",
devtools: { enabled: true },
modules: ['@nuxt/content'],
css: ['~/assets/css/main.css'],
vite: {
plugins: [
tailwindcss(),
],
},
});
Step 9: Create Blog Content
Now it's time to add some content. Create a content
directory in the root of your project and add markdown files:
// filepath: content/index.md
---
title: Welcome to My Blog
description: This is my personal blog built with Nuxt 3 and Content
---
# Welcome to My Blog
This is the homepage of my blog built with Nuxt 3 and the Content module.
## Recent Posts
Check out my latest articles in the blog section.
// filepath: content/blog/first-post.md
---
title: My First Blog Post
description: This is my first blog post using Nuxt Content
date: 2023-01-15
---
# My First Blog Post
Welcome to my first blog post! This post is written in Markdown and rendered using Nuxt Content.
## Features of Markdown
- **Bold text**
- *Italic text*
- [Links](https://nuxt.com)
- Lists like this one
// filepath: content/blog/second-post.md
---
title: My Second Blog Post
description: This is my second blog post using Nuxt Content
date: 2023-01-15
---
# My Second Blog Post
Welcome to my second blog post! This post is written in Markdown and rendered using Nuxt Content.
## Features of Markdown
- **Bold text**
- *Italic text*
- [Links](https://nuxt.com)
- Lists like this one
Step 10: Create a Blog Listing Page
Create a page to list all blog posts:
// filepath: pages/blog/index.vue
<script setup lang="ts">
const { data: posts } = await useAsyncData('blog', () => queryCollection('blog').all())
useSeoMeta({
title: 'Blog',
description: 'All blog posts'
})
</script>
<template>
<div>
<h1 class="text-3xl font-bold mb-6">Blog Posts</h1>
<div v-if="posts && posts.length">
<div v-for="post in posts" :key="post.path" class="mb-6 p-4 border border-gray-200 rounded-lg">
<h2 class="text-xl font-semibold">
<NuxtLink :to="post.path" class="text-blue-600 hover:underline">
{{ post.title }}
</NuxtLink>
</h2>
<div class="text-gray-500 text-sm mt-1" v-if="post.date">
{{ new Date(post.date).toLocaleDateString() }}
</div>
<p class="mt-2">{{ post.description }}</p>
<NuxtLink :to="post.path" class="text-blue-600 hover:underline mt-2 inline-block">
Read more
</NuxtLink>
</div>
</div>
<p v-else>No blog posts found.</p>
</div>
</template>
Step 11: Create a Blog Post Dynamic Page
Create a dynamic route to handle individual blog posts:
// filepath: pages/blog/[...slug].vue
<script setup lang="ts">
const route = useRoute()
const { data: post } = await useAsyncData(route.path, () => {
return queryCollection('blog').path(route.path).first()
})
if (!post.value) {
throw createError({ statusCode: 404, message: 'Post not found' })
}
useSeoMeta({
title: post.value.title,
description: post.value.description
})
</script>
<template>
<article>
<ContentRenderer v-if="post" :value="post">
<template #empty>
<p>No content found.</p>
</template>
</ContentRenderer>
<div class="mt-8">
<NuxtLink to="/blog" class="text-blue-600 hover:underline">
← Back to all posts
</NuxtLink>
</div>
</article>
</template>
Step 12: Running Your Blog
Start the development server:
Your blog should now be running at http://localhost:3000
.
Conclusion
You now have a functional blog built with Nuxt 3, Content, and Tailwind CSS! This setup provides:
- Fast performance with Nuxt 3's server-side rendering
- Easy content management with markdown files
- Beautiful styling with Tailwind CSS
- Great SEO with automatic metadata handling
With this foundation in place, you can easily extend your blog with additional features to make it uniquely yours.
For more information, check out the official documentation:
Nuxt 3,
Nuxt Content,
Query Collection,
Tailwind CSS