Learn how to migrate your Nuxt blog from Nuxt Content v2 to v3 in this guide. Discover the key changes, updated APIs, and improved features like SQL-backed queries, collection-based content organization, and enhanced rendering components.
Get notified when we release new tutorials, lessons, and other expert Nuxt content.
Welcome back to our series on Building a blog with Nuxt Content! In the previous parts, we covered setting up a Nuxt application with Nuxt Content v2, creating blog posts using markdown files, and implementing pagination and navigation between articles.
In this third installment, we'll explore the process of migrating our blog from Nuxt Content v2 to v3, which has been rebuilt from the ground up with enhanced capabilities and a new architecture.
While we work on the migration, we will also be exploring some of the new features of the latest version
Nuxt Content v3 offers several advantages:
Let's dive into the migration process and see what changes we need to make to our existing blog.
First, update your dependencies to the latest version:
npm install @nuxt/content@latest
# or
yarn add @nuxt/content@latest
Create a content.config.ts
file at the root of your project to define your collections:
export default defineCollection({
content: {
source: {
include: '**/*.md',
exclude: ['**/.(!(navigation.yml))'] // Exclude dot files except navigation.yml
}
}
})
The biggest change is replacing queryContent()
with the new collection-based queryCollection()
:
const { data: blog } = await useAsyncData("blog", () =>
queryContent("/blog")
.sort({ datePublished: -1 })
.skip((currentPage.value - 1) * limit.value)
.limit(limit.value)
.find()
)
const { data: blog, refresh } = await useAsyncData("blog", () =>
queryCollection("content")
.where('path', 'LIKE', '/blog%')
.skip((currentPage.value - 1) * limit.value)
.limit(limit.value)
.all())
Replace the content rendering approach with the new <ContentRenderer>
component:
<ContentDoc :path="route.path" />
<ContentRenderer v-if="page" :value="page" />
Replace findSurround
with the new dedicated API:
const [prev, next] = await queryContent('blog')
.only(['_path'])
.findSurround(route.path)
const [prev, next] = await queryCollectionItemSurroundings(
'content',
route.path,
{
fields: ['path']
}
)
Rename any _dir.yml
files to .navigation.yml
to match the new naming convention.
Let's update our pagination implementation to work with Nuxt Content v3:
<script setup>
const route = useRoute()
const currentPage = computed(() => parseInt(route.query.page) || 1);
const limit = ref(3);
const { data: blog, refresh } = await useAsyncData("blog",
() => queryCollection("content")
.where('path', 'LIKE', '/blog%')
.skip((currentPage.value - 1) * limit.value)
.limit(limit.value)
.all())
// Re-fetch when page changes
watch(() => route.query, () => refresh())
// Get total for pagination
const allArticles = await queryCollection('content')
.where('path', 'LIKE', '/blog%')
.count()
const totalPages = computed(() => Math.ceil(allArticles / limit.value));
</script>
For navigating between articles, we'll update our implementation:
<script setup>
const route = useRoute();
const [prev, next] = await queryCollectionItemSurroundings(
'content',
route.path, {
fields: ['path', 'title']
})
</script>
<template>
<div class="w-full pb-10">
<div class="flex justify-center mt-10">
<div class="flex items-center gap-4">
<UTooltip text="prev.title">
<UButton
icon="i-heroicons-arrow-small-left-20-solid"
label="Previous Pokemon"
:ui="{ rounded: 'rounded-full' }"
:disabled="!prev"
class="rtl:[&_span:first-child]:rotate-180 me-2 bg-yellow-500 hover:bg-yellow-600 disabled:invisible"
@click="navigateTo(prev.path)"
/>
</UTooltip>
<UTooltip text="next.title">
<UButton
icon="i-heroicons-arrow-small-right-20-solid"
label="Next Pokemon"
:disabled="!next"
:ui="{ rounded: 'rounded-full' }"
class="rtl:[&_span:last-child]:rotate-180 ms-2 flex-row-reverse bg-yellow-500 hover:bg-yellow-600 disabled:invisible"
@click="navigateTo(next.path)"
/>
</UTooltip>
</div>
</div></div
></template>
One notable change is that document-driven mode (where markdown files automatically convert to pages) is no longer built-in. We can implement this functionality ourselves with a catch-all route:
<!-- pages/blog/[id].vue -->
<script setup>
const route = useRoute();
const { data: page } = await useAsyncData(route.path, () => {
return queryCollection('content')
.path(route.path)
.first()
})
</script>
<template>
<div class="w-full pb-10">
<div class="w-full pb-40 pt-20 bg-gray-100 px-4 flex flex-col items-center">
<NuxtLink to="/"
><img src="/images/pok.webp" class="h-auto w-80 md:shrink-0"
/></NuxtLink>
<h1 class="text-5xl font-bold text-center -mt-7">{{ page.title }}</h1>
</div>
<div
class="max-w-7xl bg-white rounded-3xl flex flex-col items-center shadow-md -mt-20 mx-auto p-10 gap-10"
>
<img
src="/images/blog-pok.webp"
class="max-w-3xl rounded-2xl"
:alt="`image for ${page.title} article`"
/>
<ContentRenderer v-if="page" :value="page" />
</div>
</template>
If you were using Nuxt Studio, the integration has been simplified. Remove the @nuxthq/studio
package and update your configuration:
// nuxt.config.ts
export default defineNuxtConfig({
content: {
preview: {
api: 'https://api.nuxt.studio'
}
},
})
Full-text search has also been updated with the new queryCollectionSearchSections
API:
<script setup>
const searchQuery = ref('')
const { data: searchResults } = await useAsyncData(
'search',
async () => {
if (!searchQuery.value) return []
return queryCollectionSearchSections('content', searchQuery.value)
},
{ watch: [searchQuery] }
)
</script>
<template>
<div>
<input v-model="searchQuery" placeholder="Search..." />
<div v-if="searchResults?.length">
<div v-for="result in searchResults" :key="result.path">
<NuxtLink :to="result.path">
{{ result.title }}
</NuxtLink>
<p v-if="result.excerpt">{{ result.excerpt }}</p>
</div>
</div>
</div>
</template>
Migrating to Nuxt Content v3 brings significant improvements to our blog in terms of performance, organization, and developer experience. The collection-based approach makes content management more flexible and the SQL-backed query API offers more powerful querying capabilities.
While the migration requires several changes to our code, the benefits are well worth the effort. The new API is more intuitive and the separation between content collections and queries provides a cleaner architecture for our blog.
In this post, we covered the key changes needed to migrate our Nuxt Content v2 blog to v3, including:
With these changes implemented, our blog is now running on the latest version of Nuxt and Nuxt Content, taking advantage of all the improvements and new features it offers.
Stay tuned for future posts where we'll explore more advanced features and optimizations for our Nuxt Content blog!