Skip to main content

Composables

Route-aware page loading and headless content search.

Ginko exposes useContentPage(), useContentLocalePath(), and useContentSearch(). Import them from @lupinum/ginko-content/client. The Nuxt module also auto-imports the page and locale-link functions and the collision-safe search name useGinkoContentSearch.

useContentPage()

Resolve the current Nuxt route against one collection.

ts
useContentPage(handle, options?): Promise<UseContentPageResult>
app/pages/docs/[...slug].vue
<script setup lang="ts">
import { createError } from '#imports'
import { useContentPage } from '@lupinum/ginko-content/client'
import { docs } from '~~/content.config'

definePageMeta({ key: route => route.path })

const { page, status, error } = await useContentPage(docs, {
  fallback: true
})

if (status.value === 'not-found') {
  throw createError({ statusCode: 404, statusMessage: 'Document not found', fatal: true })
}
</script>

<template>
  <ContentRenderer v-if="page" :value="page" />
</template>

The page key reruns setup when the path changes, without remounting for query or hash changes. The composable integrates with the Nuxt SSR payload and hides a previous route's document while the next route is resolving. fatal: true makes the same route-owned 404 policy reach Nuxt's error page during client navigation.

Options

Options may contain refs or getters.

OptionTypeDefaultPurpose
localestringCurrent route localeOverride locale inference
fallbackLocaleFallbackexactResolve a configured or explicit locale fallback
selectstring[]all fieldsProject authored fields
populatePopulateSpecnoneResolve reference fields
surroundboolean | { select? }falseLoad previous and next navigation items

Setting surround performs one additional request. Pass true for the default projection or an object to select fields:

ts
const { page, previous, next } = await useContentPage(docs, {
  surround: { select: ['description', 'icon'] }
})

Return value

FieldTypePurpose
pageComputedRef<LocalizedContentDocument | null>Current matching document, or null when it is not available
previousComputedRef<NavigationItem | null>Previous item when surround is enabled
nextComputedRef<NavigationItem | null>Next item when surround is enabled
statusComputedRef<'pending' | 'success' | 'not-found' | 'error'>Explicit route state
errorComputedRef<unknown>Page or surround request error
refresh()() => Promise<void>Refresh the page and optional surround request

Returned documents use the document envelope. useContentPage() does not throw a 404, update <head>, or redirect from a fallback URL; the route component owns those policies.

Check status, not only page, when route behavior matters. page is also null while a navigation is pending and after an error; the status keeps those states distinct from a settled missing document.

useContentLocalePath()

Create language-switch links from an existing page result. The function makes no content request and stores no global page state.

app/pages/docs/[...slug].vue
const contentPage = await useContentPage(docs, { fallback: true })
const switchLocalePath = useContentLocalePath(contentPage)
const englishPath = computed(() => switchLocalePath('en'))

The result is a path string or undefined. A missing alternate, pending page, missing document, failed query, or stale document returns undefined. Disable or omit that language link. Ginko uses proven alternates and prefers a real translation over a supplied fallback alternate. It does not invent a translated slug.

Interactive links preserve the current query and hash. Ginko does not translate heading anchors. Build canonical and hreflang entries from document route facts and your indexing policy, not from this interactive link function.

Load the page before rendering its layout. Passing the result to the layout makes the header correct during server rendering. A child page cannot update HTML that an outer header has already rendered.

app/pages/docs/[...slug].vue
<script setup lang="ts">
import { useContentPage } from '@lupinum/ginko-content/client'
import { docs } from '~~/content.config'

definePageMeta({ layout: false, key: route => route.path })
const contentPage = await useContentPage(docs, { fallback: true })
const { page, status, error } = contentPage
if (status.value === 'error') throw error.value
if (status.value === 'not-found') {
  throw createError({ statusCode: 404, statusMessage: 'Document not found', fatal: true })
}
</script>

<template>
  <NuxtLayout name="default" :content-page="contentPage">
    <ContentRenderer v-if="page" :value="page" />
  </NuxtLayout>
</template>
app/layouts/default.vue
<script setup lang="ts">
import { useContentLocalePath, type ContentLocalePage } from '@lupinum/ginko-content/client'

const props = defineProps<{ contentPage?: ContentLocalePage }>()
const switchLocalePath = useContentLocalePath(() => props.contentPage, {
  fallback: useSwitchLocalePath()
})
const englishPath = computed(() => switchLocalePath('en'))
</script>

<template>
  <header>
    <NuxtLink v-if="englishPath" :to="englishPath">English</NuxtLink>
    <span v-else aria-disabled="true">English</span>
  </header>
  <main><slot /></main>
</template>

An application-only page can use the same layout normally. With no content page result, Ginko calls the optional fallback function only when the requested locale is in the global content locale configuration (runtime.locales). Otherwise, it returns undefined without calling the function. Ginko returns the callback's value unchanged. Content pages use their document's alternates, including collection-specific locales. A content page with no translation never calls application fallback. Callback errors propagate.

The source accepts a result, ref, or getter. Read the returned link function in a computed value or template so it updates. ContentLocalePage selects page and status from the existing page result; it introduces no new document shape.

This pattern remounts the page-owned layout on path changes. Keep persistent application controls outside it if needed. A persistent outer header must receive a result loaded by an ancestor before it renders; do not publish child page facts into a global variable as an SSR workaround.

useContentSearch()

Create headless search state for a combobox, modal, or command palette.

ts
useContentSearch(options?): Promise<UseContentSearchResult>
ts
import { useContentSearch } from '@lupinum/ginko-content/client'

const search = await useContentSearch({
  initialQuery: '',
  limit: 8,
  locale: computed(() => 'en')
})

search.setQuery('routing')
const selected = search.select(0)

The configured content.search.engine supplies results. Passing a collection additionally loads that collection's search sections and search-shaped navigation:

ts
import { docs } from '~~/content.config'

const { files, searchNavigation, results } = await useContentSearch({
  collection: docs,
  locale: computed(() => 'en')
})

Keep this call outside <ClientOnly> when server-rendered search UI needs files or searchNavigation in its payload.

Options

OptionTypeDefaultPurpose
initialQueryMaybeRefOrGetter<string>''Initial writable query value
limitMaybeRefOrGetter<number>engine result countLimit exposed results
localeMaybeRefOrGetter<string>noneFilter results and collection data
collectionHandle or dynamic namenoneAlso load search sections and search navigation

Return value

FieldPurpose
queryWritable search term ref
resultsNormalized, limited search results
pending / errorBackend loading state
activeIndex / activeResultCurrent highlighted result
hasQuery / hasResults / isEmptyDerived rendering state
setQuery(value) / reset()Query controls
next() / previous() / setActiveIndex(index)Highlight controls
select(index?)Return a result or null without navigating
filesCollection search sections; empty without collection
searchNavigationSearch-shaped collection navigation; empty without collection

Search errors are exposed through error, including disabled search, provider failures, and Pagefind loading failures. Each result contains collection and path, so routing does not need to infer collection identity from URL prefixes.

Other client exports

These are ordinary functions, not composables. getCollectionPath(), findFirstNavigationPage(), and extractContentToc() are framework-free derivations. querySiteData() is a one-shot async Nuxt query function:

ExportSignaturePurpose
getCollectionPath(collection, options?) => stringProject a collection route from locale, slug, or path; localePrefix: false omits the application locale prefix
findFirstNavigationPage(items?) => item | nullFind the first route-bearing page depth-first, skipping structural nodes
querySiteData(key, options?) => Promise<{ key, locale?, data, updatedAt? }>Read provider site data; options are locale and an optional custom fetcher
extractContentToc(content, options?) => TocDerive an H2–H4 table of contents from a Markdown string

getCollectionPath() options are locale, slug, path, and localePrefix; path takes precedence over slug. Locale mounts come from the collection handle and cannot be overridden per call. A handle using inherited i18n: true does not contain that inherited policy, so this pure helper requires the explicit collection-local i18n: { locales, defaultLocale } form instead of guessing. extractContentToc() accepts depth, title, and searchDepth, with a maximum heading depth of 4 by default. Link ids match the ids that the Markdown parser gives plain-text headings, including parent prefixes such as intro-details and suffixes such as intro-1 for repeated headings.

For lists, pagination, navigation, backlinks, and exact document reads, compose the one-shot async Nuxt query functions with useAsyncData(). That composition gives Nuxt ownership of SSR payload transfer, cache keys, reactive state, pending state, and errors. Pre-v3 query composable names were removed rather than deprecated; the Ginko version migration maps those names to their replacements.