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.
pnpm add @nuxt/content@^3
yarn add @nuxt/content@^3
npm install @nuxt/content@^3
bun add @nuxt/content@^3
content.config.ts fileThis 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.vuefetchContentNavigation to queryCollectionNavigation methodconst { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('docs'))
const { data: navigation } = await useAsyncData('navigation', () => fetchContentNavigation())
queryCollectionSearchSections methodconst { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSections('docs'), {
server: false,
})
const { data: files } = useLazyFetch<ParsedContent[]>('/api/search.json', {
default: () => [],
server: false
})
queryContent to queryCollection methodconst { 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 typeuseSeoMeta({
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.queryContent to queryCollection and queryCollectionItemSurroundings methodsconst { 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))
)
useSeoMeta with the seo field provided by the page typeuseSeoMeta({
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.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')
All _dir.yml files become .navigation.yml
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.
pnpm add @nuxt/ui-pro@next
yarn add @nuxt/ui-pro@next
npm install @nuxt/ui-pro@next
bun add @nuxt/ui-pro@next
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 "tailwindcss" theme(static);
@import "@nuxt/ui-pro";
export default defineNuxtConfig({
modules: ['@nuxt/ui-pro'],
css: ['~/assets/css/main.css']
})
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.
tailwind.config.ts file@theme directive to apply your theme in main.css file@source directive in order for Tailwind to detect classes in markdown files.@import "tailwindcss" theme(static);
@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.tsui 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 pageNew 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 pageMain, Footer and LazyUContentSearch components do not need any updates in our case.Notification component can be removed since Toast components are directly handled by the App component.NavigationTree component you can use the NavigationMenu component or the ContentNavigation 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>
We've decided to move the landing content from YML to Markdown .
components/content folder). Content v3 handles it under the hood.export default defineContentConfig({
collections: {
landing: defineCollection({
type: 'page',
source: 'index.md'
}),
docs: defineCollection({
type: 'page',
source: {
include: '**',
exclude: ['index.md']
},
...
})
}
})
ContentRenderer to render Markdownprose 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 cache-key="head-title" :value="page.hero.title" />
</template>
<MDC
:value="page.hero.code"
cache-key="head-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>
Move all components in index.md following the MDC syntax.
Landing components have been reorganised and standardised as generic Page components.
LandingHero => PageHeroLandingSection => PageSectionLandingCard => PageCard (we'll use the PageFeature instead)Aside component has been renamed to PageAside .ContentNavigation component can be used (instead of NavigationTree) to display the content navigation returned by queryCollectionNavigation.<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>
Divider has been renamed to SeparatorfindPageHeadline must be imported from #ui-pro/utils/contentprose property does not exist no more on PageBody component.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.