How to upgrade your Nuxt documentation website to Content x UI v3
2025 kicks off with the power of 3!
This start of year is marked by major updates to our favorite tools. The UI team is about to launch version 3 of the UI / UI Pro libraries (currently in alpha), while the Content team has already released Nuxt Content v3.
These updates mean that all our starter templates combining Content and UI will need to be updated to align with the latest versions. To help you make the transition, this guide walks through migrating the Nuxt UI Pro Docs Starter to the new Content v3 and Nuxt UI v3 packages.
Content migration (v2 → v3)
1. Update package to v3
pnpm add @nuxt/content@^3
yarn add @nuxt/content@^3
npm install @nuxt/content@^3
bun add @nuxt/content@^3
content.config.ts
file 2. Create
This configuration file defines your data structure. A collection represents a set of related items. In the case of the docs starter, there are two different collections, the landing
collection representing the home page and another docs
collection for the documentation pages.
import { defineContentConfig, defineCollection, z } from '@nuxt/content'
export default defineContentConfig({
collections: {
landing: defineCollection({
type: 'page',
source: 'index.yml'
}),
docs: defineCollection({
type: 'page',
source: {
include: '**',
exclude: ['index.yml']
},
schema: z.object({
links: z.array(z.object({
label: z.string(),
icon: z.string(),
to: z.string(),
target: z.string().optional()
})).optional()
})
})
}
})
On top of the built-in fields provided by the page
type, we added the extra field links
to the docs
collection so we can optionally display them in the docs page header.
type: page
means there is a 1-to-1 relationship between the content file and a page on your site.app.vue
3. Migrate
Navigation fetch can be updated by moving from fetchContentNavigation
to queryCollectionNavigation
method
const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('docs'))
const { data: navigation } = await useAsyncData('navigation', () => fetchContentNavigation())
Content search command palette data can use the new queryCollectionSearchSections
method
const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSections('docs'), {
server: false,
})
const { data: files } = useLazyFetch<ParsedContent[]>('/api/search.json', {
default: () => [],
server: false
})
4. Migrate landing page
Home page data fetching can be updated by moving from queryContent
to queryCollection
method
const { data: page } = await useAsyncData('index', () => queryCollection('landing').path('/').first())
const { data: page } = await useAsyncData('index', () => queryContent('/').findOne())
useSeoMeta
can be populated using the seo
field provided by the page type
useSeoMeta({
title: page.value.seo.title,
ogTitle: page.value.seo.title,
description: page.value.seo.description,
ogDescription: page.value.seo.description
})
seo
field is automatically overridden by the root title
and description
if not set.5. Migrate catch-all docs page
Docs page data and surround fetching can be updated and mutualised by moving from queryContent
to queryCollection
and queryCollectionItemSurroundings
methods
const { data } = await useAsyncData(route.path, () => Promise.all([
queryCollection('docs').path(route.path).first(),
queryCollectionItemSurroundings('docs', route.path, {
fields: ['title', 'description'],
}),
]), {
transform: ([page, surround]) => ({ page, surround }),
})
const page = computed(() => data.value?.page)
const surround = computed(() => data.value?.surround)
const { data: page } = await useAsyncData(route.path, () => queryContent(route.path).findOne())
const { data: surround } = await useAsyncData(`${route.path}-surround`, () => queryContent()
.where({ _extension: 'md', navigation: { $ne: false } })
.only(['title', 'description', '_path'])
.findSurround(withoutTrailingSlash(route.path))
)
Populate useSeoMeta
with the seo
field provided by the page type
useSeoMeta({
title: page.value.seo.title,
ogTitle: `${page.value.seo.title} - ${seo?.siteName}`,
description: page.value.seo.description,
ogDescription: page.value.seo.description
})
seo
field is automatically overridden by the root title
and description
if not set.6. Update types
Types have been significantly enhanced in Content v3, eliminating the need for most manual typings, as they are now directly provided by the Nuxt Content APIs.
Concerning the documentation starter, the only typing needed concerns the navigation items where NavItem
can be replaced by ContentNavigationItem
.
import type { ContentNavigationItem } from '@nuxt/content'
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
7. Replace folder metadata files
All _dir.yml
files become .navigation.yml
8. Migrate Studio activation
Since the studio module has been deprecated and a new generic Preview API
has been implemented directly into Nuxt Content, we can remove the @nuxthq/studio
package from our dependencies and from the nuxt.config.ts
modules.
Instead we just need to enable the preview mode in the Nuxt configuration file by binding the Studio API.
export default defineNuxtConfig({
content: {
preview: {
api: 'https://api.nuxt.studio'
}
},
})
Finally, in order to keep the app config file updatable from Studio, we just need to update the helper import of the nuxt.schema.ts
file from @nuxthq/studio/theme
to @nuxt/content/preview
.
Nuxt UI Pro Migration (v1 → v3)
1. Setup package to v3
Install the Nuxt UI v3 alpha package
pnpm add @nuxt/ui-pro@next
yarn add @nuxt/ui-pro@next
npm install @nuxt/ui-pro@next
bun add @nuxt/ui-pro@next
Add the module in the Nuxt configuration file
It's no longer required to add @nuxt/ui
in modules as it is automatically imported by @nuxt/ui-pro
.
export default defineNuxtConfig({
modules: ['@nuxt/ui-pro']
})
export default defineNuxtConfig({
extends: ['@nuxt/ui-pro'],
modules: ['@nuxt/ui']
})
Import Tailwind CSS and Nuxt UI Pro in your CSS
@import "tailwindcss";
@import "@nuxt/ui-pro";
export default defineNuxtConfig({
modules: ['@nuxt/ui-pro'],
css: ['~/assets/css/main.css']
})
Remove tailwind config file and use CSS-first theming
Nuxt UI v3 uses Tailwind CSS v4 that follows a CSS-first configuration approach. You can now customize your theme with CSS variables inside a @theme
directive.
- Delete the
tailwind.config.ts
file - Use the
@theme
directive to apply your theme inmain.css
file - Use the
@source
directive in order for Tailwind to detect classes inmarkdown
files.
@import "tailwindcss";
@import "@nuxt/ui-pro";
@source "../content/**/*";
@theme {
--font-sans: 'DM Sans', sans-serif;
--color-green-50: #EFFDF5;
--color-green-100: #D9FBE8;
--color-green-200: #B3F5D1;
--color-green-300: #75EDAE;
--color-green-400: #00DC82;
--color-green-500: #00C16A;
--color-green-600: #00A155;
--color-green-700: #007F45;
--color-green-800: #016538;
--color-green-900: #0A5331;
--color-green-950: #052E16;
}
ui
overloads in app.config.ts
2. Update
ui
props in a component or the ui
key in the app.config.ts
are obsolete and need to be checked in the UI / UI Pro documentation.export default defineAppConfig({
ui: {
colors: {
primary: 'green',
neutral: 'slate'
}
},
uiPro: {
footer: {
slots: {
root: 'border-t border-gray-200 dark:border-gray-800',
left: 'text-sm text-gray-500 dark:text-gray-400'
}
}
},
}
export default defineAppConfig({
ui: {
primary: 'green',
gray: 'slate',
footer: {
bottom: {
left: 'text-sm text-gray-500 dark:text-gray-400',
wrapper: 'border-t border-gray-200 dark:border-gray-800'
}
}
},
})
error.vue
page 3. Migrate
New UError
component can be used as full page structure.
<template>
<div>
<AppHeader />
<UError :error="error" />
<AppFooter />
<ClientOnly>
<LazyUContentSearch
:files="files"
:navigation="navigation"
/>
</ClientOnly>
</div>
</template>
<template>
<div>
<AppHeader />
<UMain>
<UContainer>
<UPage>
<UPageError :error="error" />
</UPage>
</UContainer>
</UMain>
<AppFooter />
<ClientOnly>
<LazyUContentSearch
:files="files"
:navigation="navigation"
/>
</ClientOnly>
<UNotifications />
</div>
</template>
app.vue
page 4. Migrate
Main
,Footer
andLazyUContentSearch
components do not need any updates in our case.Notification
component can be removed sinceToast
components are directly handled by theApp
component.- Instead of the
NavigationTree
component you can use theNavigationMenu
component or theContentNavigation
component to display content navigation.
<script>
// Content navigation provided by queryCollectionNavigation('docs')
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
</script>
<template>
<UHeader>
<template #content>
<UContentNavigation
highlight
:navigation="navigation"
/>
</template>
</UHeader>
</template>
<script>
// Content navigation provided by fetchContentNavigation()
const navigation = inject<Ref<NavItem[]>>('navigation')
</script>
<template>
<UHeader>
<template #panel>
<UNavigationTree :links="mapContentNavigation(navigation)" />
</template>
</UHeader>
</template>
5. Update landing page
We've decided to move the landing content from YML
to Markdown
.
components/content
folder). Content v3 handles it under the hood.Update content configuration
export default defineContentConfig({
collections: {
landing: defineCollection({
type: 'page',
source: 'index.md'
}),
docs: defineCollection({
type: 'page',
source: {
include: '**',
exclude: ['index.md']
},
...
})
}
})
Use ContentRenderer
to render Markdown
prose
property must be set to false
in ContentRendered
as we don't want Mardown
to be applied with prose styling in the case of a landing page integrating non prose Vue components.<template>
<UContainer>
<ContentRenderer
v-if="page"
:value="page"
:prose="false"
/>
</UContainer>
</template>
<template>
<div>
<ULandingHero
v-if="page.hero"
v-bind="page.hero"
>
<template #headline>
<UBadge
v-if="page.hero.headline"
variant="subtle"
size="lg"
class="relative rounded-full font-semibold"
>
<NuxtLink
:to="page.hero.headline.to"
target="_blank"
class="focus:outline-none"
tabindex="-1"
>
<span
class="absolute inset-0"
aria-hidden="true"
/>
</NuxtLink>
{{ page.hero.headline.label }}
<UIcon
v-if="page.hero.headline.icon"
:name="page.hero.headline.icon"
class="ml-1 w-4 h-4 pointer-events-none"
/>
</UBadge>
</template>
<template #title>
<MDC :value="page.hero.title" />
</template>
<MDC
:value="page.hero.code"
class="prose prose-primary dark:prose-invert mx-auto"
/>
</ULandingHero>
<ULandingSection
:title="page.features.title"
:links="page.features.links"
>
<UPageGrid>
<ULandingCard
v-for="(item, index) of page.features.items"
:key="index"
v-bind="item"
/>
</UPageGrid>
</ULandingSection>
</div>
</template>
Migrate Vue components to MDC
Move all components in index.md
following the MDC syntax.
Landing components have been reorganised and standardised as generic Page
components.
LandingHero
=>PageHero
LandingSection
=>PageSection
LandingCard
=>PageCard
(we'll use thePageFeature
instead)
6. Migrate docs page
Layout
Aside
component has been renamed toPageAside
.ContentNavigation
component can be used (instead ofNavigationTree
) to display the content navigation returned byqueryCollectionNavigation
.<template> <UContainer> <UPage> <template #left> <UPageAside> <UContentNavigation highlight :navigation="navigation" /> </UPageAside> </template> <slot /> </UPage> </UContainer> </template>
<template> <UContainer> <UPage> <template #left> <UAside> <UNavigationTree :links="mapContentNavigation(navigation)" /> </UAside> </template> <slot /> </UPage> </UContainer> </template>
Catch-all pages
Divider
has been renamed toSeparator
findPageHeadline
must be imported from#ui-pro/utils/content
prose
property does not exist no more onPageBody
component.
Edit on Studio
If you're using Nuxt Studio to edit your documentation you also need to migrate the related code.
The Studio module has been deprecated and a new generic Preview API
has been implemented directly into Nuxt Content, you can remove the @nuxthq/studio
package from your dependencies and from thenuxt.config.ts
modules. Instead you just need to enable the preview mode in the Nuxt configuration file by binding the Studio API.
export default defineNuxtConfig({
content: {
preview: {
api: 'https://api.nuxt.studio'
}
},
})
In order to keep the app config file updatable from Studio you need to update the helper import of the nuxt.schema.ts
file from @nuxthq/studio/theme
to @nuxt/content/preview
.