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? }Managed image reference string
fields.relation()Target collection nameOne authored reference
fields.relations()Target collection nameAuthored 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:

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, description, and whenToUse; optional whenNotToUse 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.

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:

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.