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| Field | Type | Description |
|---|---|---|
navigation.title | string | The directory's group title in the sidebar. |
navigation.label | string | An alternative label. |
navigation.description | string | Group description. |
navigation.icon | string | Icon identifier (interpreted by your nav component). |
navigation.hidden | boolean | Hide the whole directory group, including its children. |
navigation.flatten | boolean | Promote 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: trueA 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:
| Field | Type | Description |
|---|---|---|
header.enable | boolean | When true, include the directory in the header navigation tree. |
header.label | string | Override 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:
This example reads pageContext.data.headerNav directly instead of using the built-in LayoutNav.
<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:
headerNavonly includes directory nodes that explicitly enabledheader.enable- if nested directories also enable
header.enable, they show up underchildren, which is useful for second-level menus - the sidebar still comes from the
navigationtree;headeronly derives an extra top navigation tree - you can move this demo implementation into your own
+Layout.vueand evolve it into a dropdown, tabs, or drawer
hidden: hide a group
navigation:
hidden: trueThe directory disappears from the sidebar, but pages inside it remain accessible by URL.
flatten: collapse a level
navigation:
flatten: trueChild 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: /introductionVisiting /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, writingredirect: /introductionin a collection whose base is/en-USresolves to/en-US/introduction. - Paths without a leading
/are resolved relative to the current directory. For example, incontent/en-US/guide/.config.yml, writingredirect: testresolves 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.