Type safety

Generate types from your content's own schema, no manual typing required.

Every source can carry a JSON Schema for its frontmatter, inferred automatically or declared explicitly. comark-content turns that schema into real TypeScript types with one interface per source, plus a per-path map.

How it works

  • ContentRegistry maps each source name to its { data, row } shape.
  • ContentPaths maps every known document path to its data type, so content.get('/posts/hello') narrows automatically without a type parameter.

Both interfaces are empty by default — the module augmentation written to comark-content.d.ts is what fills them in.

Generating types

Type generation is the same operation, writeSourceTypes(), run three different ways depending on your setup.

CLI

Run comark-content prepare as a standalone script or CI step:

Terminal
npx comark-content prepare

Use this when you're not using Vite, or want types regenerated on demand (e.g. after editing content schemas) rather than tied to a dev server or build.

Vite plugin

If you're using the Vite plugin, types are generated for you — no extra step:

vite.config.ts
import { defineConfig } from 'vite'
import comark from 'comark-content/vite'
import { content } from './content'

export default defineConfig({
  plugins: [comark({ content })],
})

It runs on dev-server start and at build (the types option, on by default). Pass types: false to disable it, or types: { outDir } to change where it writes.

A failure to generate types only warns — it never fails your build or dev server.

writeSourceTypes()

Call it directly from any script for full control over when and how types are written:

scripts/generate-types.ts
import { writeSourceTypes } from 'comark-content'
import { content } from '../content'

await writeSourceTypes(content, {
  outFile: 'types/comark-content.d.ts',
  moduleName: 'comark-content',
  paths: true,
})

Parameters:

  • content: the ComarkContent instance to read schemas from.
  • options?: an object with outDir/fileName/outFile (default ./comark-content.d.ts), moduleName (default comark-content), and paths (default true).

Returns: Promise<string>, the generated file contents (also written to disk).

Under the hood it calls generateSourceTypes(content, options) — the same function, without the file write — if you'd rather handle the output yourself (e.g. print it, or write it somewhere non-filesystem).

Wiring it into your project

Whichever method you use, make sure your tsconfig.json picks up the generated file:

tsconfig.json
{
  "include": ["comark-content.d.ts", "**/*.ts"]
}
Generated types are derived, not source of truth — add comark-content.d.ts to .gitignore and regenerate it in CI/dev instead of committing it.

Using generated types

Once generated, get(), list(), and query() all pick up the types automatically:

usage.ts
// Known path -> data auto-narrowed, no type parameter needed
const page = await content.get('/posts/hello')
page?.data.title // typed from the `posts` source's schema

// Unknown path -> pass a type parameter explicitly
const other = await content.get<PostData>('/dynamic/path')

// Known source name -> each item's data narrowed the same way
const posts = await content.list(['posts'])
posts[0].data.title // typed from the `posts` source's schema

See Registry and query types for every generated/derived type, and the sqlQuery plugin for how query() uses the same schemas.

Plugin metadata

Metadata declared by Comark plugins is inferred directly from the Content instance configuration; it does not need generated declarations. For example, the headings plugin makes meta.title and meta.description available as string | undefined on get(), list(), and stat() results:

content.ts
import { comarkContent } from 'comark-content'
import fs from 'comark-content/sources/fs'
import headings from 'comark/plugins/headings'

export const content = comarkContent({
  source: fs('./content'),
  markdown: {
    plugins: [headings()],
  },
})

The same inference works when configuring the markdown plugin explicitly:

content.ts
import { comarkContent } from 'comark-content'
import markdown from 'comark-content/plugins/markdown'
import headings from 'comark/plugins/headings'

export const content = comarkContent({
  plugins: [
    markdown({
      comark: {
        plugins: [headings()],
      },
    }),
  ],
})

Semantic field types

Source schemas support vendor extensions on JsonSchema via x-content. The markdown-fields plugin parses string fields marked { type: 'markdown' } into MarkdownDocument at runtime and adjusts generated data types when registered. Import markdownField() from comark-content/plugins/markdown-fields, not the main package entry.