Upgrading from Nuxt Content v2 to v3

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.

Charles Allotey
Nuxt 3

The Mastering Nuxt FullStack Unleashed Course is here!

Get notified when we release new tutorials, lessons, and other expert Nuxt content.

Click here to view course

Building a Blog with Nuxt Content (Part 3): Migrating to Nuxt Content v3

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.

  1. Building a blog with Nuxt content (Part 1) - Setting up the foundations
  2. Building a blog with Nuxt content (Part 2) - Pagination and navigation
  3. Building a blog with Nuxt content (Part 3) - Migrating to Nuxt Content v3 👈🏻 We are here

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

Why Migrate to Nuxt Content v3?

Nuxt Content v3 offers several advantages:

  • SQL-backed query API for better performance
  • Collection-based content organization
  • Improved rendering components
  • Better separation of concerns
  • Enhanced type safety

Let's dive into the migration process and see what changes we need to make to our existing blog.

Key Migration Steps

1. Update Dependencies

First, update your dependencies to the latest version:

npm install @nuxt/content@latest
# or
yarn add @nuxt/content@latest

2. Configure Collections

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
    }
  }
})

3. Update Content Queries

The biggest change is replacing queryContent() with the new collection-based queryCollection():

Before (v2):

const { data: blog } = await useAsyncData("blog", () =>
  queryContent("/blog")
    .sort({ datePublished: -1 })
    .skip((currentPage.value - 1) * limit.value)
    .limit(limit.value)
    .find()
)

After (v3):

const { data: blog, refresh } = await useAsyncData("blog", () =>
  queryCollection("content")
      .where('path', 'LIKE', '/blog%')
      .skip((currentPage.value - 1) * limit.value)
      .limit(limit.value)
      .all())

4. Update Rendering Components

Replace the content rendering approach with the new <ContentRenderer> component:

Before (v2):

<ContentDoc :path="route.path" />

After (v3):

<ContentRenderer v-if="page" :value="page" />

5. Update Navigation Functionality

Replace findSurround with the new dedicated API:

Before (v2):

const [prev, next] = await queryContent('blog')
  .only(['_path'])
  .findSurround(route.path)

After (v3):

const [prev, next] = await queryCollectionItemSurroundings(
  'content',
  route.path,
  {
    fields: ['path']
  }
)

6. Update File Naming

Rename any _dir.yml files to .navigation.yml to match the new naming convention.

Updated Pagination Implementation

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>

Updated Article Navigation

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>

Implementing Document-Driven Mode

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>

Nuxt Studio Integration

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>

Conclusion

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:

  • Setting up collections
  • Updating content queries
  • Implementing the new rendering components
  • Adapting pagination and navigation
  • Setting up document-driven mode manually

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!

Useful Resources

Charles Allotey
Charles is a Frontend Developer at Vueschool. Has a passion for building great experiences and products using Vue.js and Nuxt.

Follow MasteringNuxt on