Configure directory-level navigation with .config.yml, and set page/directory redirects.

Directory Config & Redirects

Beyond per-file frontmatter, you can place a .config.yml file in any content directory to configure the whole directory.

Directory-level navigation

# content/docs/guide/.config.yml
navigation:
  label: Guide
  icon: book
  description: A collection of how-to guides
FieldTypeDescription
navigation.titlestringThe directory's group title in the sidebar.
navigation.labelstringAn alternative label.
navigation.descriptionstringGroup description.
navigation.iconstringIcon identifier (interpreted by your nav component).
navigation.hiddenbooleanHide the whole directory group, including its children.
navigation.flattenbooleanPromote children to the parent level, removing this grouping layer.

Header Navigation

If your layout maps content directories into the top header, enable it explicitly in .config.yml:

# content/docs/guide/.config.yml
navigation:
  label: Guide
header:
  enable: true

A common pattern is enabling header.enable only on top-level directories so the header shows first-level sections. If you also enable it on nested directories, they remain in the header tree as nested menu items.

The built-in schema currently supports:

FieldTypeDescription
header.enablebooleanWhen true, include the directory in the header navigation tree.
header.labelstringOverride the title shown in the header without affecting navigation.label.

Render the header with built-in LayoutNav

The docs data loader automatically exposes this header tree as pageContext.data.headerNav.

If you want to use the built-in component directly, wire it in your layout like this:

<script setup lang="ts">
import { computed } from 'vue'
import { usePageContext } from 'vike-vue/usePageContext'
import { LayoutNav } from 'vike-vue-content/components/layout-nav'
import type { DocsHeaderNavItem } from 'vike-vue-content/shared/types'

const pageContext = usePageContext()

const headerNav = computed<DocsHeaderNavItem[]>(() => {
  const data = pageContext.data as { headerNav?: DocsHeaderNavItem[] } | undefined
  return data?.headerNav ?? []
})

const currentPath = computed(() => {
  const pathname = pageContext.urlPathname
  const base = (pageContext as { _baseServer?: string })._baseServer ?? '/'

  if (base === '/' || !pathname.startsWith(base)) {
    return pathname
  }

  const trimmed = pathname.slice(base.length)
  return trimmed.startsWith('/') ? trimmed : `/${trimmed}`
})
</script>

<template>
  <header>
    <LayoutNav :items="headerNav" :current-path="currentPath" />
  </header>
</template>

Prefer passing currentPath without the base prefix so the active state matches headerNav.path.

Using the data in a custom header

If you don't want to use LayoutNav directly, you can consume the same data yourself. The headerNav shape is:

type DocsHeaderNavItem = {
  title: string
  path: string
  matchPath: string
  children?: DocsHeaderNavItem[]
}

Here is a minimal runnable custom header example. It reads from pageContext.data directly and renders its own top navigation:

Docs

This example reads pageContext.data.headerNav directly instead of using the built-in LayoutNav.

index.vue
<template>
  <div class="demo-header-shell">
    <header class="demo-header">
      <div class="demo-header-brand">
        Docs
      </div>

      <nav v-if="headerNav.length" class="demo-header-nav" aria-label="Custom header demo">
        <Link
          v-for="item in headerNav"
          :key="item.path"
          :href="item.path"
          class="demo-header-link"
          :class="{ 'is-active': isActive(item) }"
          data-vike="false"
        >
          {{ item.title }}
        </Link>
      </nav>
    </header>

    <p class="demo-header-note">
      This example reads <code>pageContext.data.headerNav</code> directly instead of using the built-in <code>LayoutNav</code>.
    </p>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { usePageContext } from 'vike-vue/usePageContext'
import type { DocsHeaderNavItem } from 'vike-vue-content/shared/types'
import { Link } from 'vike-vue-content/components/link'

const pageContext = usePageContext()

const headerNav = computed<DocsHeaderNavItem[]>(() => {
  const data = pageContext.data as { headerNav?: DocsHeaderNavItem[] } | undefined
  return data?.headerNav ?? []
})

const currentPath = computed(() => {
  const pathname = pageContext.urlPathname
  const base = (pageContext as { _baseServer?: string })._baseServer ?? '/'

  if (base === '/' || !pathname.startsWith(base)) {
    return pathname
  }

  const trimmed = pathname.slice(base.length)
  return trimmed.startsWith('/') ? trimmed : `/${trimmed}`
})

function isActive(item: DocsHeaderNavItem) {
  return (
    currentPath.value === item.matchPath
    || currentPath.value.startsWith(`${item.matchPath}/`)
  )
}
</script>

<style scoped>
.demo-header-shell {
  background:
    radial-gradient(circle at top left, color-mix(in srgb, var(--color-primary) 12%, transparent), transparent 42%),
    linear-gradient(180deg, color-mix(in srgb, var(--color-surface) 88%, white), var(--color-bg));
  border: 1px solid var(--color-border);
  border-radius: calc(var(--radius) + 0.5rem);
  padding: 1rem;
}

.demo-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}

.demo-header-brand {
  color: var(--color-text);
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.demo-header-nav {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.demo-header-link {
  color: var(--color-text-muted);
  text-decoration: none;
  padding: 0.55rem 0.85rem;
  border-radius: 999px;
  transition: background-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
}

.demo-header-link:hover {
  color: var(--color-text);
  background-color: var(--color-surface-elevated);
}

.demo-header-link.is-active {
  color: var(--color-primary);
  background-color: color-mix(in srgb, var(--color-primary) 10%, var(--color-bg));
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 22%, transparent);
  font-weight: 600;
}

.demo-header-note {
  margin: 0.875rem 0 0;
  color: var(--color-text-muted);
  font-size: 0.925rem;
  line-height: 1.6;
}

.demo-header-note code {
  background-color: var(--color-surface-elevated);
  border-radius: 0.35rem;
  padding: 0.1rem 0.35rem;
}
</style>

Notes:

  • headerNav only includes directory nodes that explicitly enabled header.enable
  • if nested directories also enable header.enable, they show up under children, which is useful for second-level menus
  • the sidebar still comes from the navigation tree; header only derives an extra top navigation tree
  • you can move this demo implementation into your own +Layout.vue and evolve it into a dropdown, tabs, or drawer

hidden: hide a group

navigation:
  hidden: true

The directory disappears from the sidebar, but pages inside it remain accessible by URL.

flatten: collapse a level

navigation:
  flatten: true

Child pages are promoted one level up, removing the "group → children" two-level structure — handy when you don't want an extra layer of nesting.

Redirects

redirect can be set in .config.yml (directory-level) or in Markdown frontmatter (page-level).

Directory redirect

Commonly used to redirect a collection's root path to a specific doc:

# content/en-US/.config.yml
navigation:
  hidden: true
redirect: /introduction

Visiting /en-US automatically jumps to /en-US/introduction.

Page redirect

---
redirect: /en-US/guide/routing
---

How redirect targets resolve

  • Absolute protocol links (e.g. https://..., mailto:...) are kept as-is.
  • Paths starting with / are resolved from the current collection base. For example, writing redirect: /introduction in a collection whose base is /en-US resolves to /en-US/introduction.
  • Paths without a leading / are resolved relative to the current directory. For example, in content/en-US/guide/.config.yml, writing redirect: test resolves to /en-US/guide/test.
  • If the target already includes the base prefix, it isn't added twice.
The framework collects redirect rules across the whole workspace at build time. If the same source path has conflicting targets, it throws an error so you catch the misconfiguration early.