Skip to main content

How Ginko thinks

Ginko normalizes files or provider records into one document model for typed queries and derived site output.

Ginko has one content flow: files or provider records become normalized documents, and every feature reads from that model.

content files or provider


      collection

   parse → validate → normalize

          ├── resolve page routes and translations
          ├── run typed queries
          ├── build navigation and surroundings
          ├── produce search records
          └── produce sitemap and prerender routes

Collections define document sets

A collection defines which documents belong together, their type, source, and schema. A page collection can also mount those documents at a public route. A data collection remains queryable but does not create public Nuxt pages.

content.config.ts
import { defineCollection, defineContentConfig } from '@lupinum/ginko-content/config'
import { z } from 'zod'

export const blog = defineCollection({
  type: 'page',
  source: 'blog/**/*.md',
  route: '/blog',
  schema: z.object({
    title: z.string(),
    description: z.string().optional(),
    date: z.string()
  })
})

export default defineContentConfig({
  collections: { blog }
})

defineCollection returns a typed handle—here it is blog. Import it and pass it to queries so their results stay typed against the collection schema.

One document shape

Every query returns the same document envelope, whether the source is Markdown, structured data, or an external provider. Page documents expose their public route and locale resolution inside that envelope. Data documents use the same query vocabulary, but they do not register public pages.

Routes and locales for page documents

Three fields answer the common routing questions for a page document:

  • What URL do I link to? route.resolvedPath. Never the file path.
  • Where is this page in other languages? route.alternates. A language switcher is a loop over it.
  • Did the reader get a fallback? resolution.usedFallback. A fallback is content shown from another language when the requested one has no translation.

canonicalKey is the opaque shared ID that ties a page to its translations. Use it as a list key, never as a URL.

One query vocabulary

For route-backed page collections, useContentPage(handle) resolves the current route into a page document:

app/pages/blog/[...slug].vue
definePageMeta({ key: route => route.path })

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

if (!page.value) {
  throw createError({ statusCode: 404, statusMessage: 'Post not found', fatal: true })
}

Use one, many, and paginate for record queries. Page collections also support navigation and surround. Wrap these functions in useAsyncData when a component needs them:

app/pages/blog/index.vue
const { data: posts } = await useAsyncData('blog:published', () =>
  many(blog, {
    where: { publishedAt: { $exists: true } },
    sort: { publishedAt: 'desc' }
  })
)

The same verbs run on the server. Pass the request event as the first argument so provider and locale context stay explicit.

Site output is derived

For route-backed page collections, navigation, public search records, sitemap entries, and prerender routes derive from the normalized documents. Ginko can rebuild these outputs from the canonical filesystem or provider instead of asking you to maintain a second copy.