Add typed frontmatter
Define Markdown fields once, infer their query types, and reject invalid content before release.
Put structured data above the Markdown body, then describe the same fields in the collection schema.
---
title: Hello
description: A first post.
publishedAt: 2026-07-14
tags:
- authoring
featured: false
---
# Hello
Use headings for structure and frontmatter for data.import { defineCollection, defineContentConfig, fields } from '@lupinum/ginko-content/config'
import { z } from 'zod'
export const posts = defineCollection({
type: 'page',
source: 'blog/**/*.md',
route: '/blog',
schema: z.object({
title: fields.text().required(),
description: fields.text().required(),
publishedAt: fields.date().required(),
tags: fields.array(fields.text()),
featured: fields.boolean()
})
})
export default defineContentConfig({ collections: { posts } })The posts handle carries the inferred schema type through one, many, useContentPage, and the server query API. Import the handle wherever you query the collection so TypeScript can infer the result.
Choose fields or Zod
Use fields.* when an editor or CMS needs labels, help text, localization, or field types. These helpers return Zod schemas, so validation and editor metadata share one definition.
Use plain Zod when you only need validation:
publishedAt: z.iso.datetime()Every field helper supports .label(), .help(), .localized(), and .required().
Keep dates as strings.fields.date()returnsYYYY-MM-DD, whilefields.datetime()returns a UTC ISO 8601 string. Both remain sortable with$gtand$lt. Schemas that outputDateinstances, includingz.date()andz.coerce.date(), fail ingestion because content documents must stay JSON-safe.
Set a default once
Put defaults in the schema instead of repeating fallbacks in every component:
tags: fields.array(fields.text()).default([]) // empty array when omittedLeave the field optional when omission has a different meaning from an explicit empty value.
Reject invalid documents
Collection schemas are strict by default. Ginko rejects a document that fails its schema, so no module option is needed for this behavior.
Set strict: false only as a temporary migration aid:
import { defineCollection, defineContentConfig } from '@lupinum/ginko-content/config'
import { z } from 'zod'
export const legacyPosts = defineCollection({
type: 'page',
source: 'legacy-posts/*.md',
strict: false,
schema: z.object({
title: z.string()
})
})
export default defineContentConfig({ collections: { legacyPosts } })In non-strict mode, Ginko warns and passes through the original document. Query results may contain values that did not satisfy the schema.
Control table-of-contents depth
Ginko builds heading IDs while parsing Markdown. Configure the toc plugin when a page needs table-of-contents data; no TOC plugin is installed implicitly:
export default defineNuxtConfig({
content: {
markdown: {
plugins: [
['toc', { depth: 3, searchDepth: 3 }],
'summary'
]
}
}
})