content.config.ts
Collections, schemas, providers, CMS metadata, and agent-readable content.
content.config.ts is the source of truth for the content model. Import its runtime helpers from @lupinum/ginko-content/config; put parsing and runtime behavior in the content module options.
import {
defineCollection,
defineContentConfig,
fields
} from '@lupinum/ginko-content/config'
export const articles = defineCollection({
type: 'page',
source: 'articles/**/*.md',
schema: fields.object({
title: fields.text().required(),
publishedAt: fields.datetime()
})
})
export default defineContentConfig({
collections: { articles }
})Root config
defineContentConfig(config: ContentConfig): ContentConfigdefineContentConfig() preserves type inference and assigns each collection map key as the collection handle's name.
| Option | Type | Purpose |
|---|---|---|
collections | Record<string, ContentCollectionConfig> | Named collections used by queries |
provider | string | Active provider name; defaults to 'filesystem' |
providers | Record<string, string> | Provider names mapped to module specifiers |
agent | ContentAgentConfig | Site-wide agent metadata, sections, policies, and app-owned pages |
Register a custom provider by name:
export default defineContentConfig({
provider: 'custom',
providers: {
custom: '@acme/content-provider'
},
collections: { articles }
})The provider module must implement the provider contract.
Collections
defineCollection(config: DefineCollectionObject): ContentCollectionHandle| Option | Type | Default | Purpose |
|---|---|---|---|
type | 'page' | 'data' | required | Route-backed pages or data-only records |
source | string | string[] | { include, exclude? } | none | Filesystem source globs; optional for provider-backed collections |
schema | ZodType | none | Parsed document schema |
strict | boolean | true | Treat schema validation failures as fatal |
i18n | boolean | { defaultLocale, locales } | false | Inherit global locales, opt out, or declare a collection policy |
route | string | Record<string, string> | none | Public mount, optionally per locale |
sitemap | boolean | pages: true; data: false | Include the collection in sitemap output |
cms | ContentCmsCollectionConfig | none | Editor metadata that cannot be inferred safely |
agent | ContentAgentCollectionConfig | none | Agent section and Markdown inclusion policy |
The object form of source moves exclude onto the normalized collection:
const docs = defineCollection({
type: 'page',
source: {
include: ['docs/**/*.md', 'shared/**/*.md'],
exclude: 'docs/private/**'
}
})Use locale-specific route mounts when translated URLs differ:
const docs = defineCollection({
type: 'page',
source: 'docs/**/*.md',
i18n: true,
route: { en: '/docs', de: '/dokumentation' }
})i18n: true inherits the module locale policy. i18n: false opts a collection out. The object form owns defaultLocale and locales for that collection.
With the default strict: true, a document that fails the collection schema is rejected. Set strict: false only when schema failures should warn and pass through the original document; it does not change the Zod schema's own unknown-key policy.
CMS options
cms supplements the schema with editor-specific information. It does not replace the collection or schema as the content contract.
| Option | Type | Purpose |
|---|---|---|
label | string | Record<string, string> | Editor label |
type | 'flat' | 'tree' | Collection layout |
icon | string | Editor icon |
route | object | Route editing policy |
fields | Record<string, ContentCmsFieldConfig> | Explicit field metadata or overrides |
settings | unknown | Provider-owned collection settings |
cms.route accepts mode, pathPrefix, slugMode, rootSlug, singleton, and allowMultipleRoots. slugMode is one of 'shared', 'localized', 'stable', or 'localizedStable'.
A cms.fields entry may set type, label, description, required, localized, searchable, sortable, defaultValue, validation, options, relation, nested fields, min, max, step, slugFrom, language, and an opaque editor object. Prefer fields.* metadata when it can express the same rule once in the schema.
Collection agent options
const docs = defineCollection({
type: 'page',
agent: {
section: 'documentation',
markdown: {
includeInIndex: true,
includeInFull: true,
metadata: ['title', 'description', 'url']
}
}
})agent.section assigns pages to a root agent section. agent.markdown is a boolean or an object with includeInIndex, includeInFull, and metadata.
Schema fields
fields produces Zod schemas with CMS metadata. Fields are optional unless .required() is applied; fields.object() itself is required.
| Helper | Options or argument | Output |
|---|---|---|
fields.text() | — | String |
fields.textarea() | — | Multiline string |
fields.richtext() | — | Rich-text string |
fields.slug() | { from?: string } | Slug string |
fields.email() | — | Valid email string |
fields.url() | — | Valid URL string |
fields.number() | — | Number |
fields.boolean() | — | Boolean |
fields.date() | — | Calendar date normalized to YYYY-MM-DD |
fields.datetime() | — | Date-like input normalized to a UTC ISO 8601 string |
fields.select() | Non-empty readonly string tuple | Enum value; the tuple becomes CMS options |
fields.json() | — | Unknown JSON-shaped value |
fields.icon() | — | Icon identifier string |
fields.object() | Zod shape | Nested object |
fields.array() | Item schema | Array; a relation item becomes a multi-relation field |
fields.image() | { aspectRatio?, accept? } | Managed image reference string |
fields.relation() | Target collection name | One authored reference |
fields.relations() | Target collection name | Authored reference array |
Managed assets currently support PNG, JPEG, GIF, and WebP up to 25 MiB. When
accept is omitted, all four formats are allowed. Supplying any other MIME type
fails contract generation instead of silently weakening the field policy.
General files such as PDFs are not managed or portable yet. Ginko Content does
not expose a generic file field until that lifecycle is supported end to end;
use a canonical external HTTPS reference when the content model needs one.
Every field schema supports these metadata modifiers:
fields.text()
.label('Title')
.help('Shown in cards and SEO metadata')
.localized()
.required()
fields.number().shared()| Modifier | Effect |
|---|---|
.required() | Removes the optional wrapper and marks the field required |
.label(value) | Sets a string, localized-label map, or null |
.help(value) | Sets the editor description or clears it with null |
.localized(value = true) | Marks the field as localized or shared |
.shared() | Equivalent to .localized(false) |
Plain Zod schemas remain supported. A schema that outputs a JavaScript Date is rejected at ingest; use fields.date() or fields.datetime() so stored values remain portable strings.
References
reference(collection?: string): ContentReferenceSchemareference() marks an authored reference without CMS field metadata. Pass a collection name to constrain the target, or omit it for a cross-collection reference.
import { reference } from '@lupinum/ginko-content/config'
import { z } from 'zod'
schema: z.object({
author: reference('authors'),
related: z.array(reference('articles')).default([])
})Use fields.relation() and fields.relations() when the same schema should also describe the CMS editor.
Root agent config
The root agent object owns site-wide output policy. The collection-level agent object only chooses exposure and section membership.
| Option | Fields |
|---|---|
site | Required title, description, and whenToUse; optional whenNotToUse and contentSignals.search, aiInput, aiTrain |
markdown | metadata, as a boolean, metadata field list, or { enabled?, defaultFields? } |
sections | { id, title, order? }[] |
pages | App-owned agent pages |
Localized site values and page routes accept either a string or a locale map. An app page requires id, route, section, title, description, and render(ctx); its title and description may also be sync or async functions of ctx. It may set updated, includeInIndex, includeInFull, and metadata.
Configure the canonical deployment origin as Nuxt site.url. Agent output does
not accept a separate URL because two origin settings can produce broken
canonical links and indexes. Production agent routes require this value even
when prerendering is disabled.
Use the identity helpers to preserve literal inference:
import {
defineAgentAppPage,
defineAgentMarkdownPolicy,
defineAgentMetadataFields,
defineAgentSection
} from '@lupinum/ginko-content/config'
const metadata = defineAgentMetadataFields(['title', 'description', 'url'])
export default defineContentConfig({
agent: {
markdown: defineAgentMarkdownPolicy({
metadata: { enabled: true, defaultFields: metadata }
}),
sections: [
defineAgentSection({ id: 'documentation', title: 'Documentation', order: 10 })
],
pages: [
defineAgentAppPage({
id: 'support',
route: '/support',
section: 'documentation',
title: 'Support',
description: 'How to get help.',
render: () => '# Support\n\nContact the maintainers.'
})
]
},
collections: { articles }
})agentMetadataFields contains the allowed built-ins: title, description, url, route, locale, section, collection, source, and updated.
Metadata and slug utilities
getContentFieldMetadata(schema): ContentFieldMetadata | null
isContentFieldSchema(schema): boolean
slugifyUrlSegment(value, options?: { lower?: boolean }): stringgetContentFieldMetadata() reads the metadata attached by fields.*; isContentFieldSchema() is its type guard. slugifyUrlSegment() transliterates and normalizes one URL segment, lowercasing by default. Pass { lower: false } to preserve ASCII letter case.
defineContentConfig() throws when an authored collection name disagrees with its map key. defineCollection() rejects the removed (name, config) signature; put the desired name in the collections map instead.