Markdown Fields Plugin

Parse Markdown strings in document data — frontmatter, JSON, YAML — into Comark trees via x-content schema metadata.

Document data — frontmatter on .md files, records in .json / .yaml, and any other parsed shape — can include string fields that hold Markdown. The markdown-fields plugin lets you mark those fields in a source schema with x-content: { type: 'markdown' } so they are parsed by the same Comark engine as document bodies.

The plugin is opt-in — register it explicitly on your Content. Without it, marked fields stay raw strings in file.data, including frontmatter on Markdown documents.

Usage

content.ts
import { comarkContent, type JsonSchema } from 'comark-content'
import fs from 'comark-content/sources/fs'
import json from 'comark-content/plugins/json'
import yaml from 'comark-content/plugins/yaml'
import markdownFields, { markdownField } from 'comark-content/plugins/markdown-fields'

const faqSchema: JsonSchema = {
  type: 'object',
  properties: {
    items: {
      type: 'array',
      items: {
        type: 'object',
        properties: {
          question: { type: 'string' },
          answer: markdownField(),
        },
        required: ['question', 'answer'],
      },
    },
  },
  required: ['items'],
}

const content = comarkContent({
  source: fs('./content', { schema: faqSchema }),
  plugins: [yaml(), json(), markdownFields()],
})

Given content/faq/faq.yaml:

faq.yaml
items:
  - question: How do I return a product?
    answer: |
      You have **14 days** to request a return.

      > Refunds are processed within 5 business days.
query.ts
const faq = await content.get('/faq/faq')
faq?.data.items[0].answer
//    ^? MarkdownDocument — parsed nodes, frontmatter, meta

Render with <MarkdownDocument> (Angular/React/Svelte/Vue) or render to HTML directly.

The same schema annotation works on Markdown frontmatter, only the document body is parsed automatically; frontmatter fields need this plugin when they contain Markdown:

post.md
---
title: Release notes
summary: |
  We shipped **v2** with a new API.

  > See the migration guide for details.
---
# Release notes

With summary: markdownField() in the source schema and markdownFields() registered, content.get('/post') returns a parsed MarkdownDocument at data.summary while file.nodes still holds the body.


API

markdownFields(options?)

Returns a ContentPlugin that:

  1. Hooks file:parsed to replace marked string fields in file.data with MarkdownDocument objects after the file parser runs.
  2. Hooks typegen:field so generated types emit MarkdownDocument for marked fields on data interfaces (query row types keep string).

Parameters:

Returns: ContentPlugin

Requires a source schema and a registered .md (or .markdown) file parser — typically the built-in markdown parser that comarkContent installs by default.


Options

OptionTypeDefaultDescription
onError'throw' | 'warn' | 'ignore'Content onErrorHow to react when a marked field fails to parse.

onError

How the plugin reacts when a marked Markdown field can't be parsed: 'warn' logs and leaves the original string in place, 'ignore' leaves it silently, 'throw' aborts. Defaults to the Content-level onError (which defaults to 'warn').

markdownFields({ onError: 'throw' })

Default: the Content instance onError option


Schema helpers

Import helpers from the plugin entry, not the main package:

import markdownFields, {
  markdownField,
  schemaHasMarkdownFields,
  isMarkdownDocument,
} from 'comark-content/plugins/markdown-fields'
ExportDescription
markdownField()Returns { type: 'string', 'x-content': { type: 'markdown' } } for use in a source schema.
schemaHasMarkdownFields(schema)Returns true when a schema (recursively) declares markdown fields.
isMarkdownDocument(value)Runtime type guard for a parsed field value.

How parsing works

  • Runs on every parsed document — Markdown frontmatter, JSON, YAML, and any other format whose parser fills file.data.
  • Field parsing reuses the registered .md parser via a synthetic ParserContext — no duplicate parse pipeline.
  • Nested objects and array items are walked recursively; any field matching the schema annotation is transformed.
  • Parse failures are handled per onError — by default a warning is logged and the original string is left in place.

Type generation

When markdown-fields is registered, writeSourceTypes() emits MarkdownDocument for marked fields on <Source>Data interfaces and adds import type { MarkdownDocument } from 'comark' when needed.

Query row types (<Source>Row) always use string for markdown columns at the type level. At runtime, content.query() indexes the parsed MarkdownDocument values written during load.

See the Vite + Vue markdown fields example to compare list(), get(), and query() side by side.

Without the plugin, marked fields type as string even if the schema carries x-content metadata.


  • content.query() indexes the flattened parsed data.* values from the manifest.
  • content.search() only indexes document nodes, not inline field markdown — use full get() and render field trees in the UI, or add a custom index if you need field-level search.
This plugin parses Markdown inside file.data fields (frontmatter, JSON, YAML, etc.). The document body of a .md file is always parsed into file.nodes by the built-in Markdown parser — that path does not use x-content annotations.

Trim listing payloads

Combine with json({ listingFields }) or yaml({ listingFields }) when FAQ-style records are large but listings only need titles:

content.ts
plugins: [
  yaml({ listingFields: ['title'] }),
  json(),
  markdownFields(),
]