Skip to main content

Model data collections and references

Query records that have no public page and connect documents with stable authored references.

Use a data collection for records such as authors, team members, and products that do not need their own pages.

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

export const team = defineCollection({
  type: 'data',
  source: 'team/*.yml',
  schema: z.object({
    name: z.string(),
    role: z.string(),
    order: z.number(),
    avatar: z.string().optional()
  })
})

export default defineContentConfig({ collections: { team } })
content/team/jane.yml
ref: team:jane
name: Jane Doe
role: Documentation engineer
order: 1
avatar: /images/team/jane.jpg

Every record is queryable. A type: 'data' collection has no public routes and stays out of the sitemap.

Render records from a page

Query the collection with many, then render the results from an application page:

app/pages/team.vue
<script setup lang="ts">
import { many } from '@lupinum/ginko-content/client'
import { team } from '~~/content.config'

const { data: members } = await useAsyncData('team', () =>
  many(team, {
    sort: { order: 'asc' },
    select: ['name', 'role', 'order', 'avatar']
  })
)
</script>

<template>
  <ul>
    <li v-for="member in members" :key="member.canonicalKey">
      <img v-if="member.avatar" :src="member.avatar" alt="">
      <strong>{{ member.name }}</strong>
      <span>{{ member.role }}</span>
    </li>
  </ul>
</template>

Choose a source format

  • YAML for compact, hand-authored records.
  • JSON for machine-generated data.
  • CSV for flat tables with stable columns.
  • Markdown when a record needs a body.

Every format passes through the collection schema before a query can return it.

Give each target a ref, then declare the relation with reference(). The ref is the stable, authored value stored by other documents.

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

export const authors = defineCollection({
  type: 'data',
  source: 'authors/*.yml',
  schema: z.object({
    name: z.string(),
    bio: z.string().optional()
  })
})

export const posts = defineCollection({
  type: 'page',
  source: 'blog/*.md',
  route: '/blog',
  schema: z.object({
    title: z.string(),
    author: reference('authors'),
    related: z.array(reference('posts')).default([])
  })
})

export default defineContentConfig({ collections: { authors, posts } })
content/blog/first-post.md
---
title: First post
ref: post:first
author: author:jane
related:
  - post:second
---
Store refs, not canonical keys. Ginko derives canonicalKey from content identity, so renaming or moving files can change it. An authored ref stays stable until you choose to change it.

Populate a relation

Pass the target collection in populate to load the linked author with the post:

ts
const post = await one(posts, {
  by: { ref: 'post:first' },
  populate: {
    author: authors
  }
})

Populate only the relations the view renders.

Find documents that point back

Use backlinks to reverse the relation and load every post that names an author:

ts
const authoredPosts = await backlinks(authors, {
  by: { ref: 'author:jane' },
  from: posts,
  sort: { title: 'asc' }
})