Files
2026-09-27 11:51:31 +03:00

3.7 KiB

Docs Layout

Build documentation sites with sidebar navigation, table of contents, and surround links.

When to use

  • Technical documentation sites
  • Knowledge bases, help centers
  • Any content-heavy site with hierarchical navigation

Requires @nuxt/content — see conventions for setup (module order + @source).

Component tree

UApp
├── UHeader
├── UMain
│   └── NuxtLayout (docs)
│       └── UPage
│           ├── #left → UPageAside → UContentNavigation
│           └── NuxtPage
│               ├── UPageHeader
│               ├── UPageBody → ContentRenderer + UContentSurround
│               └── #right → UContentToc
└── UFooter

App shell

<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'

const route = useRoute()

const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('docs'))

provide('navigation', navigation)

const items = computed<NavigationMenuItem[]>(() => [{
  label: 'Docs',
  to: '/docs/getting-started',
  active: route.path.startsWith('/docs')
}])
</script>

<template>
  <UApp>
    <UHeader>
      <template #title>
        <Logo class="h-6 w-auto" />
      </template>

      <UNavigationMenu :items="items" />

      <template #right>
        <UContentSearchButton />
        <UColorModeButton />
      </template>
    </UHeader>

    <UMain>
      <NuxtLayout>
        <NuxtPage />
      </NuxtLayout>
    </UMain>

    <UFooter />

    <UContentSearch :navigation="navigation" />
  </UApp>
</template>

Layout

<script setup lang="ts">
import type { ContentNavigationItem } from '@nuxt/content'

const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
</script>

<template>
  <UPage>
    <template #left>
      <UPageAside>
        <UContentNavigation :navigation="navigation" />
      </UPageAside>
    </template>

    <slot />
  </UPage>
</template>

Page

<script setup lang="ts">
const route = useRoute()

definePageMeta({ layout: 'docs' })

const { data: page } = await useAsyncData(route.path, () => {
  return queryCollection('docs').path(route.path).first()
})

const { data: surround } = await useAsyncData(`${route.path}-surround`, () => {
  return queryCollectionItemSurroundings('docs', route.path)
})
</script>

<template>
  <UPage>
    <UPageHeader :title="page.title" :description="page.description" />

    <UPageBody>
      <ContentRenderer :value="page" />

      <USeparator />

      <UContentSurround :surround="surround" />
    </UPageBody>

    <template #right>
      <UContentToc :links="page.body.toc.links" />
    </template>
  </UPage>
</template>

How nesting works

The outer UPage in the layout handles the left sidebar. The inner UPage in the page handles the right sidebar. They nest correctly — this is intentional.

Common mistakes

  • Not providing navigation via provide/inject — the layout needs it from the app shell.
  • Forgetting UContentSearch in app.vue — search won't work without it.
  • Using UContentSearchButton without UContentSearch — the button opens search, but the search component must exist.

Key components

  • UPage — multi-column grid with #left, #default, #right slots
  • UPageAside — sticky sidebar wrapper (visible from lg breakpoint)
  • UContentNavigation — sidebar navigation tree from Nuxt Content
  • UContentToc — table of contents from page headings
  • UContentSurround — prev/next links
  • UContentSearch / UContentSearchButton — search command palette
  • UPageAnchors — simpler alternative to full TOC