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? } | Image reference string |
fields.asset() | { accept? } | Asset reference string |
fields.file() | { accept? } | File reference string |
fields.relation() | Target collection name | One authored reference |
fields.relations() | Target collection name | Authored reference array |
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 and description; optional url, profile, 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.
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.