Build content navigation
Generate a locale-aware tree from content files and use the same order for adjacent-page links.
Call navigation() with a collection handle, cache the result by locale, and render the returned tree.
<script setup lang="ts">
import { navigation } from '@lupinum/ginko-content/client'
import { docs } from '~~/content.config'
const { locale } = useI18n()
const { data: items } = await useAsyncData(
() => `docs-navigation:${locale.value}`,
() => navigation(docs, {
locale: locale.value,
select: ['description', 'icon', 'badge', 'sidebar']
}),
{ watch: [locale] }
)
</script>
<template>
<nav aria-label="Documentation">
<DocsNavigationBranch :items="items ?? []" />
</nav>
</template>Render each branch recursively and check path before creating a link:
<script setup lang="ts">
import type { ContentNavigationItem } from '@lupinum/ginko-content/client'
defineOptions({ name: 'DocsNavigationBranch' })
defineProps<{ items: ContentNavigationItem[] }>()
</script>
<template>
<ul>
<li v-for="(item, index) in items" :key="item.path ?? `group:${index}`">
<NuxtLink v-if="item.path" :to="item.path">{{ item.title }}</NuxtLink>
<span v-else>{{ item.title }}</span>
<DocsNavigationBranch v-if="item.children?.length" :items="item.children" />
</li>
</ul>
</template>Page items carry title and path. Pass select to include extra fields that the navigation component renders.
Structural groups have children but nopath. Render a<NuxtLink>only whenitem.pathexists. The example above handles both page and group items.
Shorten a page label
Keep the page title descriptive and give navigation a shorter label with nested frontmatter:
---
title: Install Ginko in an existing Nuxt project
navigation:
title: Installation
---The nested shape matters. navigation.title written as a dotted frontmatter key stays a literal field and does not change the sidebar label.
Name folders
Add .navigation.yml inside a folder to describe that folder in the tree.
content/docs/
1.get-started/
.navigation.yml
1.index.md
2.installation.md
2.reference/
1.index.mdtitle: Get started
icon: i-lucide-rocket
sidebar: sectionNumeric prefixes set sibling order without appearing in labels or URLs.
sidebar: section and sidebar: group are structural markers that a theme can use for major areas and local groups. .navigation.yml remains folder metadata; use the page's nested navigation object for a page-specific label or badge.
Redirect to the first page
For a docs-root redirect or a "Get Started" button, resolve the first routable page of the tree.
import { navigation } from '@lupinum/ginko-content/client'
import { findFirstNavigationPage } from '@lupinum/ginko-content/navigation'
const items = await navigation(docs, { locale })
const docsEntry = findFirstNavigationPage(items)
if (docsEntry) await navigateTo(docsEntry.path)To resolve one section's own page first, pass it as a one-item tree: findFirstNavigationPage([section]). To skip the section and search only inside it, pass its children: findFirstNavigationPage(section?.children).
Add previous and next links
On a route page, set surround on useContentPage to get the neighbors in collection order.
<script setup lang="ts">
import { createError } from '#imports'
import { docs } from '~~/content.config'
definePageMeta({ key: route => route.path })
const { page, previous, next } = await useContentPage(docs, {
surround: {
select: ['description']
}
})
if (!page.value) {
throw createError({ statusCode: 404, statusMessage: 'Document not found', fatal: true })
}
</script>
<template>
<article v-if="page">
<ContentRenderer :value="page" />
<nav aria-label="Adjacent pages" class="grid sm:grid-cols-2">
<NuxtLink v-if="previous" :to="previous.path">
<span>Previous</span>
<strong>{{ previous.title }}</strong>
<small>{{ previous.description }}</small>
</NuxtLink>
<NuxtLink v-if="next" :to="next.path">
<span>Next</span>
<strong>{{ next.title }}</strong>
<small>{{ next.description }}</small>
</NuxtLink>
</nav>
</article>
</template>For a document that is not the current route, call surround() directly and target it by reference.
import { surround } from '@lupinum/ginko-content/client'
import { docs } from '~~/content.config'
const neighbors = await surround(docs, {
by: { ref: 'guide:installation' },
select: ['description']
})previous and next are null at the ends of the collection, so render each link only when it exists. Numeric file and folder prefixes determine their order.
Use the generated surroundings instead of maintaining a second sidebar array. A separate list can drift from the files and does not apply locale resolution.
What stays hidden
The tree respects the same visibility rules as everything else:
draft: truehides a page in production.navigation: falsekeeps a page routable but drops it from the tree.- A filename starting with
_is a partial with no route. - The collection handle and
localeoption scope the whole tree.
During development and prerender, Ginko warns once when select names a field that neither the collection schema nor the built-in navigation fields declare. It also warns when a .navigation.yml file matches no folder in that locale. Runtime production requests do not emit these diagnostics.