Enable and customize top-level header navigation derived from content directories.

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

LayoutNav reads pageContext.data.headerNav and derives currentPath automatically — just drop it into your layout:

<script setup>
import { LayoutNav } from 'vike-vue-content/components/layout-nav'
</script>

<template>
  <header>
    <LayoutNav />
  </header>
</template>

Both items and current-path props are optional. Pass them explicitly only when you need to override the defaults (e.g. a filtered nav or a custom path-matching strategy).

Using the data in a custom header

If you don't want to use LayoutNav directly, you can consume the same data yourself. The docs data loader exposes the header tree as pageContext.data.headerNav:

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