Skip to main content

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.

content.config.ts
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

ts
defineContentConfig(config: ContentConfig): ContentConfig

defineContentConfig() preserves type inference and assigns each collection map key as the collection handle's name.

OptionTypePurpose
collectionsRecord<string, ContentCollectionConfig>Named collections used by queries
providerstringActive provider name; defaults to 'filesystem'
providersRecord<string, string>Provider names mapped to module specifiers
agentContentAgentConfigSite-wide agent metadata, sections, policies, and app-owned pages

Register a custom provider by name:

content.config.ts
export default defineContentConfig({
  provider: 'custom',
  providers: {
    custom: '@acme/content-provider'
  },
  collections: { articles }
})

The provider module must implement the provider contract.

Collections

ts
defineCollection(config: DefineCollectionObject): ContentCollectionHandle
OptionTypeDefaultPurpose
type'page' | 'data'requiredRoute-backed pages or data-only records
sourcestring | string[] | { include, exclude? }noneFilesystem source globs; optional for provider-backed collections
schemaZodTypenoneParsed document schema
strictbooleantrueTreat schema validation failures as fatal
i18nboolean | { defaultLocale, locales }falseInherit global locales, opt out, or declare a collection policy
routestring | Record<string, string>nonePublic mount, optionally per locale
sitemapbooleanpages: true; data: falseInclude the collection in sitemap output
cmsContentCmsCollectionConfignoneEditor metadata that cannot be inferred safely
agentContentAgentCollectionConfignoneAgent section and Markdown inclusion policy

The object form of source moves exclude onto the normalized collection:

ts
const docs = defineCollection({
  type: 'page',
  source: {
    include: ['docs/**/*.md', 'shared/**/*.md'],
    exclude: 'docs/private/**'
  }
})

Use locale-specific route mounts when translated URLs differ:

ts
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.

OptionTypePurpose
labelstring | Record<string, string>Editor label
type'flat' | 'tree'Collection layout
iconstringEditor icon
routeobjectRoute editing policy
fieldsRecord<string, ContentCmsFieldConfig>Explicit field metadata or overrides
settingsunknownProvider-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

ts
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.

HelperOptions or argumentOutput
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 tupleEnum value; the tuple becomes CMS options
fields.json()Unknown JSON-shaped value
fields.icon()Icon identifier string
fields.object()Zod shapeNested object
fields.array()Item schemaArray; 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 nameOne authored reference
fields.relations()Target collection nameAuthored reference array

Every field schema supports these metadata modifiers:

ts
fields.text()
  .label('Title')
  .help('Shown in cards and SEO metadata')
  .localized()
  .required()

fields.number().shared()
ModifierEffect
.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

ts
reference(collection?: string): ContentReferenceSchema

reference() marks an authored reference without CMS field metadata. Pass a collection name to constrain the target, or omit it for a cross-collection reference.

ts
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.

OptionFields
siteRequired title and description; optional url, profile, and contentSignals.search, aiInput, aiTrain
markdownmetadata, as a boolean, metadata field list, or { enabled?, defaultFields? }
sections{ id, title, order? }[]
pagesApp-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:

content.config.ts
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

ts
getContentFieldMetadata(schema): ContentFieldMetadata | null
isContentFieldSchema(schema): boolean
slugifyUrlSegment(value, options?: { lower?: boolean }): string

getContentFieldMetadata() 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.