Hello! It's my first post here, and I'm going to describe what I've encountered while creating a blog using Nuxt and Nuxt Content: what worked, what didn't, and what's the verdict on that stack.
Let's briefly introduce what we'll be using to create our blog. I'm quoting from the official docs.
Nuxt is a free and open-source framework with an intuitive and extendable way to create type-safe, performant and production-grade full-stack web applications and websites with Vue.js.
Nuxt Content is a module for Nuxt that provides a simple way to manage content for your application. It allows developers to write their content in Markdown, YAML, CSV, or JSON files and then query and display it in their application.
We'll be combining these two to create a simple yet functional blog using Markdown files.
To get started, first, we need to create a new Nuxt project and either select @nuxt/content
to be installed now or we can add it manually later. For a package manager, I'm using pnpm
, but you can use whichever one you prefer.
For detailed installation guide check out Nuxt's and Nuxt Content's docs.
pnpm create nuxt my-cool-blog
> Would you like to install any of the official modules?
[•] @nuxt/content – The file-based CMS with support for Markdown, YAML, JSON
✨ Nuxt project has been created with the v3 template. Next steps:
› cd my-cool-blog
› Start development server with pnpm run dev
Cool! After we have it all in place, we can proceed to actual creation of our blog, but with AI, of course.
When I was planning this, I decided to take the newly released Claude Sonnet 4 for a spin. It should be simple enough, right? I create a naive prompt in Cursor without overthinking it:
Create a simple Markdown based blog in this Nuxt application using Nuxt Content.
At first glance, everything looked fine, and I was impressed that it was able to one-shot this. We got a new file content.config.ts
with a newly defined collection:
import { defineContentConfig, defineCollection } from "@nuxt/content";
export default defineContentConfig({
collections: {
blog: defineCollection({
type: "page",
source: "**/*.md",
}),
},
});
The generated Vue component looked exactly like an example from the documentation:
<script setup>
const { data } = await useAsyncData('blog', () => {
return queryContent('/').findOne()
})
</script>
<template>
<pre>
{{ data }}
</pre>
</template>
Then I tried to add a new page into the newly created /content
directory that should serve our Markdown pages, but there were no data found.
After couple more tries of changing directories, filenames, and quite a lot of console.log
statement I've realized what was the problem; queryContent
was replaced by queryCollection
in Nuxt Content version 3.
So yeah, we have an explanation for why it didn't work.
I also tried asking Claude a few times to fix the problem, but it just hallucinated and actually made everything worse, so beware (more on this later).
The actual Vue code using Nuxt Content version 3 should look like this:
<script setup>
const { data } = await useAsyncData("/blog/post", () => {
return queryCollection("blog").path("/blog/post").first();
});
</script>
<template>
<pre>
{{ data }}
</pre>
</template>
This code queries the blog
collection (which was defined in content.config.ts
) looks for a file on a path /blog/post
and renders the post that you're reading at the moment :)
Before discussing a valuable lesson learned from using Claude Sonnet 4 while developing this blog, let's talk about Nuxt Content's limitations.
It's not hot because:
queryCollection
returns undefined
. That's definitely something I want to investigate and figure out what's causing this.It's hot because:
ContentRenderer
is great, easily customizable and does the job of rendering Markdown well.In summary, it's nice, but for me, having to rebuild the whole application to add a new Markdown file is an overkill. Also it's a bit of a pain to have to reload the page after each Vue component change (that's most likely some config bug). I'd say it's really good for serving a lot of existing files that don't change very often.
I'm satisfied with what I've build, but there were few important lessons learned along the way:
I hope this was an interesting read, and I can say that there will be more content coming. Thanks for your time :)