---
title: "Migrating from MDC"
description: "Step-by-step guide for moving from @nuxtjs/mdc to Comark, covering package mapping, API changes, component registration, and renderer setup."
canonical_url: "https://comark.dev/kb/migration-from-mdc"
---
# Migrating from MDC

> Step-by-step guide for moving from @nuxtjs/mdc to Comark, covering package mapping, API changes, component registration, and renderer setup.

Comark is the successor to `@nuxtjs/mdc`. The markdown syntax is fully compatible: your `.md` files need no changes. What changes is the JavaScript API: package names, parse functions, the AST format, and how plugins work.

## Quick overview

- **Package**: `@nuxtjs/mdc` → `comark` (core) or `@comark/nuxt` (Nuxt module)
- **Parse**: `parseMarkdown()` → `parseMarkdown()` · factory: `createMarkdownParser()` (sync, no await)
- **AST**: object tree → compact tuples `['tag', props, ...children]`
- **Result**: `result.body` / `result.data` → `document.nodes` / `document.frontmatter`
- **Renderer**: `<MDCRenderer :body :data>` → `<MarkdownDocument :value>`
- **All-in-one**: `<MDC :value>` → `<Markdown :value>`
- **Slots**: `<MDCSlot />` → native `<slot />`
- **Plugins**: global `nuxt.config` → per-component `defineMarkdownComponent({ plugins })`
- **Binding**: `{{ path }}` via opt-in [binding plugin](https://comark.dev/plugins/built-in/binding) + render-time `data` prop (namespaced paths)
- **Markdown files**: component syntax unchanged; update binding paths if you used bare MDC names

## Agent skill

To give coding agents the MDC-to-Comark migration skill, install it from production:

```bash [Terminal]
npx skills add https://comark.dev/skills/migrate-mdc-to-comark
```

## Package changes

If you only use the programmatic API (parse functions, plugins, AST manipulation), install `comark` alone:

::code-group
```bash [npm]
npm uninstall @nuxtjs/mdc
npm install comark
```

```bash [pnpm]
pnpm remove @nuxtjs/mdc
pnpm add comark
```

```bash [yarn]
yarn remove @nuxtjs/mdc
yarn add comark
```

```bash [bun]
bun remove @nuxtjs/mdc
bun add comark
```
::

If you also use the Nuxt module (renderer components, auto-imports, prose components), install `@comark/nuxt` instead, as it includes `comark` as a dependency:

::code-group
```bash [npm]
npm uninstall @nuxtjs/mdc
npm install @comark/nuxt
```

```bash [pnpm]
pnpm remove @nuxtjs/mdc
pnpm add @comark/nuxt
```

```bash [yarn]
yarn remove @nuxtjs/mdc
yarn add @comark/nuxt
```

```bash [bun]
bun remove @nuxtjs/mdc
bun add @comark/nuxt
```
::

## Core package

The `comark` package is the foundation all other packages build on. The changes below apply to all programmatic usage, regardless of whether you use the Nuxt module.

### Parse functions

#### One-shot parsing

::code-group
```typescript [Before]
import { parseMarkdown } from '@nuxtjs/mdc/runtime'
const result = await parseMarkdown(markdown)
```

```typescript [After]
import { parseMarkdown } from 'comark'
const document = await parseMarkdown(markdown)
```
::

#### Reusable parser

::code-group
```typescript [Before]
import { createMarkdownParser } from '@nuxtjs/mdc/runtime'
const parser = await createMarkdownParser(options)
const result = await parser(markdown)
```

```typescript [After]
import { createMarkdownParser } from 'comark'
const parse = createMarkdownParser(options)   // synchronous, no await needed
const document = await parse(markdown)
```
::

Comark's `createMarkdownParser` is synchronous: the plugin system is eagerly initialized, so the factory returns immediately. MDC's factory was async because it had to initialize the unified processor pipeline.

::tip{to="https://comark.dev/reference/parse"}
See the Parse API for the full list of options and return types.
::

### Render functions

#### Serialize back to Markdown

::code-group
```typescript [Before]
import { stringifyMarkdown } from '@nuxtjs/mdc/runtime'
const markdown = stringifyMarkdown(result.body, result.data)
```

```typescript [After]
import { renderMarkdown } from 'comark/render'
const markdown = await renderMarkdown(document)
```
::

`renderMarkdown` includes frontmatter automatically: it reads `document.frontmatter` and serializes it as YAML front matter. See the [Render API](https://comark.dev/reference/render) for full options.

### Return type

The shape of the parsed result changed.

::code-group
```typescript [Before]
// MDCParserResult
{
  body: MDCRoot        // the AST
  data: {              // frontmatter
    title: string
    description: string
    [key: string]: any
  }
  toc: Toc | undefined
  excerpt: MDCRoot | undefined
}
```

```typescript [After]
// MarkdownDocument
{
  nodes: Node[]              // the AST
  frontmatter: Record<string, any> // frontmatter (all keys)
  meta: {
    toc?: Toc            // requires toc plugin
    summary?: Node[] // requires summary plugin
    [key: string]: any
  }
}
```
::

**Quick reference:**

| MDC                 | Comark                                              |
| ------------------- | --------------------------------------------------- |
| `result.body`       | `document.nodes`                                    |
| `result.data`       | `document.frontmatter`                              |
| `result.data.title` | `document.frontmatter.title`                        |
| `result.toc`        | `document.meta.toc` (requires `toc` plugin)         |
| `result.excerpt`    | `document.meta.summary` (requires `summary` plugin) |

### AST format

The node structure is fundamentally different. MDC uses objects; Comark uses compact tuples.

::code-group
```typescript [Before]
// MDC object tree
{
  type: 'element',
  tag: 'p',
  props: { class: 'intro' },
  children: [
    { type: 'text', value: 'Hello world' }
  ]
}
```

```typescript [After]
// Comark tuple array
['p', { class: 'intro' }, 'Hello world']
//  tag   attributes        children...
```
::

**Mapping:**

| MDC                       | Comark                     |
| ------------------------- | -------------------------- |
| `node.type === 'element'` | `Array.isArray(node)`      |
| `node.type === 'text'`    | `typeof node === 'string'` |
| `node.tag`                | `node[0]`                  |
| `node.props`              | `node[1]`                  |
| `node.children`           | `node.slice(2)`            |

If you traverse the AST manually, update your node-walking code accordingly.

::tip{to="https://comark.dev/getting-started/document-model"}
See the document model guide for the node types, parsed document structure, and traversal helpers.
::

### Plugins

MDC used the `unified`/`remark`/`rehype` pipeline. Comark uses its own lighter plugin interface.

#### Syntax highlighting

::code-group
```typescript [Before]
import { createMarkdownParser, rehypeHighlight, createShikiHighlighter } from '@nuxtjs/mdc/runtime'

const parser = await createMarkdownParser({
  rehype: {
    plugins: {
      highlight: {
        instance: rehypeHighlight,
        options: {
          theme: 'material-theme-palenight',
          highlighter: createShikiHighlighter({ /* ... */ }),
        },
      },
    },
  },
})
```

```typescript [After]
import { createMarkdownParser } from 'comark'
import shiki from 'comark/plugins/shiki'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'

const parse = createMarkdownParser({
  plugins: [
    shiki({
      themes: { light: githubLight, dark: githubDark }
    })
  ]
})
```
::

#### Table of contents

::code-group
```typescript [Before]
import { parseMarkdown } from '@nuxtjs/mdc/runtime'
const result = await parseMarkdown(md, { toc: { depth: 3 } })
const toc = result.toc
```

```typescript [After]
import { parseMarkdown } from 'comark'
import toc from 'comark/plugins/toc'

const document = await parseMarkdown(md, { plugins: [toc({ depth: 3 })] })
const tableOfContents = document.meta.toc
```
::

#### Excerpt / summary

::code-group
```typescript [Before]
const result = await parseMarkdown(md)
const excerpt = result.excerpt  // MDCRoot | undefined
```

```typescript [After]
import { parseMarkdown } from 'comark'
import summary from 'comark/plugins/summary'

const document = await parseMarkdown(md, { plugins: [summary()] })
const summaryNodes = document.meta.summary  // Node[] | undefined
```
::

#### Emoji

::code-group
```typescript [Before]
// enabled by default via remark-emoji
// mdc: { remarkPlugins: { 'remark-emoji': {} } }
```

```typescript [After]
import { parseMarkdown } from 'comark'
import emoji from 'comark/plugins/emoji'

const document = await parseMarkdown(md, { plugins: [emoji()] })
```
::

#### Binding (`{{ }}`)

MDC built-in bindings map to the opt-in [binding plugin](https://comark.dev/plugins/built-in/binding). See [Binding and data passing](#binding-and-data-passing) for the full before/after (namespaced paths, render-time `data`).

```typescript
import { parseMarkdown } from 'comark'
import binding from 'comark/plugins/binding'

const document = await parseMarkdown(md, { plugins: [binding()] })
// Render with Binding + data — do not pass data here
```

### Parse options

::code-group
```typescript [Before]
// MDCOptions
{
  remark: { plugins: { /* record */ } },
  rehype: { options: {...}, plugins: { /* record */ } },
  highlight: { theme: '...', langs: [...] } | false,
  toc: { depth: 3, searchDepth: 2 } | false,
  keepComments: false,
  keepPosition: false,
}
```

```typescript [After]
// ParserOptions
{
  plugins: ComarkPlugin[],   // ordered array, not a record
  autoUnwrap: true,          // removes <p> from single-paragraph containers
  autoClose: true,           // completes incomplete syntax (useful for streaming)
  // HTML, components, attributes, alerts, task-list, frontmatter are on by default
}
```
::

The `mdc.config.ts` unified pipeline hooks (`pre`, `remark`, `rehype`, `post`) have no equivalent because Comark doesn't use `unified`. Use the `ComarkPlugin` interface instead.

::tip{to="https://comark.dev/plugins"}
See the Plugins documentation for the full list of available plugins and how to write your own.
::

## Nuxt Module

### Configuration

::code-group
```typescript [Before]
export default defineNuxtConfig({
  modules: ['@nuxtjs/mdc'],
  mdc: { highlight: { ... }, remarkPlugins: { ... } },
})
```

```typescript [After]
export default defineNuxtConfig({
  modules: ['@comark/nuxt'],
  // No plugin config here, plugins are defined per-component (see below)
})
```
::

`@comark/nuxt` auto-imports `Markdown`, `MarkdownDocument`, `defineMarkdownComponent`, and `defineMarkdownDocumentComponent`. Plugin and component configuration moves out of `nuxt.config.ts` and into dedicated component definitions.

### Renderer component

::code-group
```vue [Before]
<MDCRenderer :body="result.body" :data="result.data" :components="components" />
```

```vue [After]
<MarkdownDocument :value="document" :components="components" />
```
::

Props that changed:

| MDC `<MDCRenderer>`  | Comark `<MarkdownDocument>`  | Notes                                                                                                                                                                            |
| -------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `body` (`MDCRoot`)   | `value` (`MarkdownDocument`) | Different shape, see AST section                                                                                                                                                 |
| `data` (frontmatter) | —                            | Frontmatter lives on `document.frontmatter`, not a separate prop                                                                                                                 |
| —                    | `data`                       | **New meaning**: runtime values for [data binding](https://comark.dev/syntax/components#data-binding) / [`{{ }}`](https://comark.dev/plugins/built-in/binding) — not frontmatter |
| `tag`                | —                            | Wrapper is always `<div class="comark-content">`                                                                                                                                 |
| `prose`              | —                            | `Prose*` component resolution is automatic                                                                                                                                       |
| `unwrap`             | `unwrap` prop / parse option | `<Markdown unwrap />` or `:options="{ unwrap: 'p' }"` — see [inline rendering](https://comark.dev/reference/parse#inline-rendering)                                              |
| `class`              | —                            | Style the wrapper via CSS                                                                                                                                                        |
| `components`         | `components`                 | Same purpose, same shape                                                                                                                                                         |
| —                    | `componentsManifest`         | New: dynamic async component resolver                                                                                                                                            |
| —                    | `streaming`                  | New: streaming mode                                                                                                                                                              |
| —                    | `caret`                      | New: animated caret for streaming                                                                                                                                                |

### All-in-one component

The `<MDC>` component maps to `<Markdown>`.

::code-group
```vue [Before]
<MDC :value="markdown" :parser-options="options" />
<MDC :value="result" />   <!-- pre-parsed MDCParserResult -->
```

```vue [After]
<Markdown :value="markdown" :options="options" />
<!-- or default slot -->
<Markdown>{{ markdown }}</Markdown>
```
::

For a pre-parsed document, use `<MarkdownDocument>` directly instead of passing it to the all-in-one component.

::tip{to="https://comark.dev/rendering/nuxt"}
See Render Comark in Nuxt for the full component API, streaming setup, and SSR examples.
::

### `defineMarkdownComponent`

In MDC, plugins were configured once in `nuxt.config.ts` and applied globally to every parse call. In Comark, you define one or more named components using `defineMarkdownComponent` and `defineMarkdownDocumentComponent`, each with its own plugins. This means different parts of your app can use different configurations without any workarounds.

A typical app needs at least two configurations: a full one for articles (highlight, TOC, math…) and a lighter one for areas like comment sections that don't need all that.

```typescript [composables/markdown.ts]
import { defineMarkdownComponent, defineMarkdownDocumentComponent } from '@comark/vue'
import shiki from '@comark/nuxt/plugins/shiki'
import toc from '@comark/nuxt/plugins/toc'
import emoji from '@comark/nuxt/plugins/emoji'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'
import CustomAlert from '~/components/Alert.vue'
import ProsePre from '~/components/ProsePre.vue'

// Full-featured component for articles
export const ArticleMarkdown = defineMarkdownComponent({
  name: 'ArticleMarkdown',
  plugins: [
    shiki({ themes: { light: githubLight, dark: githubDark } }),
    toc({ depth: 3 }),
    emoji(),
  ],
  components: {
    alert: CustomAlert,
    ProsePre,
  },
})

// Lightweight component for comments: no TOC, no highlight, no emoji
export const CommentMarkdown = defineMarkdownComponent({
  name: 'CommentMarkdown',
})
```

```vue [pages/article/[slug\\].vue]
<script setup lang="ts">
import { ArticleMarkdown, CommentMarkdown } from '~/composables/markdown'
</script>

<template>
  <!-- Full plugins for the article body -->
  <ArticleMarkdown>{{ article.content }}</ArticleMarkdown>

  <!-- Lightweight for each comment -->
  <CommentMarkdown v-for="c in comments" :key="c.id">{{ c.body }}</CommentMarkdown>
</template>
```

The same pattern applies to `defineMarkdownDocumentComponent` when you pre-parse on the server:

```typescript [composables/markdown.ts]
export const ArticleMarkdownDocument = defineMarkdownDocumentComponent({
  name: 'ArticleMarkdownDocument',
  components: {
    alert: CustomAlert,
    ProsePre,
  },
})
```

```vue [pages/article/[slug\\].vue]
<script setup lang="ts">
import { ArticleMarkdownDocument } from '~/composables/markdown'

const { data: document } = await useFetch(`/api/article/${slug}`)
</script>

<template>
  <ArticleMarkdownDocument :value="document" />
</template>
```

### Slots in custom components

MDC required a special `<MDCSlot>` component inside your custom Vue components to render children. Comark uses the native Vue `<slot>` element instead.

#### Default slot

::code-group
```vue [Before]
<template>
  <div class="alert">
    <MDCSlot />
  </div>
</template>
```

```vue [After]
<template>
  <div class="alert">
    <slot />
  </div>
</template>
```
::

#### Unwrapping children

MDC's `<MDCSlot unwrap="p">` and the older `<slot mdc-unwrap="p">` attribute both map to `<slot unwrap="p">`:

::code-group
```vue [Before]
<!-- MDCSlot -->
<MDCSlot unwrap="p" />

<!-- older mdc-unwrap attribute -->
<slot mdc-unwrap="p" />
```

```vue [After]
<slot unwrap="p" />
```
::

The `unwrap` attribute strips the specified wrapper tag(s) from the rendered children. Useful when you want to remove the auto-inserted `<p>` from single-paragraph slot content.

#### Named slots

Named slots work the same as in MDC: use `#slotName` in the markdown and `<slot name="slotName">` in the component:

```vue
<!-- components/Card.vue -->
<template>
  <div class="card">
    <div class="card-header">
      <slot name="header" />
    </div>
    <div class="card-body">
      <slot unwrap="p" />
    </div>
  </div>
</template>
```

```mdc
::card
#header
Card Title

Body content here.
::
```

### Summary

In Vue, render only the summary with the `summary` prop:

::code-group
```vue [Before]
<MDCRenderer :body="result.excerpt ?? result.body" :data="result.data" />
```

```vue [After]
<Markdown summary>{{ markdown }}</Markdown>
```
::

### Prose components

Both MDC and Comark support dropping custom Vue components into `components/prose/` to override how standard HTML elements render. The Nuxt module auto-registers them globally.

The resolution naming changed:

| Element  | MDC component name | Comark component name |
| -------- | ------------------ | --------------------- |
| `<p>`    | `ProseP`           | `ProseP`              |
| `<h1>`   | `ProseH1`          | `ProseH1`             |
| `<a>`    | `ProseA`           | `ProseA`              |
| `<pre>`  | `ProsePre`         | `ProsePre`            |
| `<code>` | `ProseCode`        | `ProseCode`           |

The names are the same. The internal resolution mechanism changed: MDC resolved `prose-p` (kebab-case) in the renderer, then looked up the registered `ProseP` component. Comark resolves directly by `ProseP` (PascalCase). If your prose components are in `components/prose/` and follow the `Prose*.vue` naming convention, they work without changes.

::note
Custom `ProsePre` components that relied on a `code` prop (or `meta.lines`) for copy buttons or collapse thresholds need a small update: after highlighting, Comark does not expose the raw source as a prop. Declare `__node` and reconstruct the text with `textContent()` from `comark/utils`. See [Custom Pre](https://comark.dev/kb/custom-code-block).
::

### Nuxt UI

If your project uses [Nuxt UI](https://ui.nuxt.com), `@comark/nuxt` registers [Nuxt UI prose components](https://ui.nuxt.com/docs/typography) automatically.

[Nuxt UI](https://ui.nuxt.com) also exposes convenience shorthand components for common callout patterns. Instead of `::callout{icon="..." color="..."}`, you can use:

```mdc
::note
Informational note.
::

::tip
A helpful tip.
::

::warning
Something to watch out for.
::

::caution
A critical warning.
::
```

These are equivalent to `::callout` with the matching `color` and default icon. They are **only available with [Nuxt UI](https://ui.nuxt.com)**. Without it, continue using `::callout{...}` directly.

## Component syntax

The MDC block and inline component syntax is identical. No changes needed in your markdown files.

```mdc
::alert{type="info"}
This works the same in both MDC and Comark.
::

:badge{label="New"}

:::card
#header
Card title

Body content.
:::
```

::tip{to="https://comark.dev/syntax/components"}
See the Component Syntax reference for blocks, inline elements, props, slots, and nesting.
::

## Binding and data passing

MDC's `{{ }}` interpolation and runtime data are supported in Comark via the [binding plugin](https://comark.dev/plugins/built-in/binding) and the shared [data-binding](https://comark.dev/syntax/components#data-binding) layer. The API differs in three important ways:

1. **Opt-in parser + renderer handler** — register `binding()` and the framework `Binding` component (not enabled by default).
2. **Namespaced paths** — use `frontmatter.`, `data.`, `meta.`, or `props.` instead of bare names from a flat parse-time bag.
3. **Data at render time** — pass runtime values with the renderer's `data` prop. Do **not** pass `{ data }` into `parseMarkdown`.

### Inline `{{ }}` interpolation

::code-group
```mdc [Before]
<!-- MDC: bare paths from parse-time `data` -->
Hello {{ user.name }}, you have {{ count }} messages.
```

```mdc [After]
<!-- Comark: qualified paths against the render context -->
Hello {{ data.user.name }}, you have {{ data.count }} messages.

<!-- Or from YAML frontmatter -->
Welcome, {{ frontmatter.user.name || guest }}.
```
::

Wire the plugin once (Nuxt example):

```vue
<script setup lang="ts">
import binding, { Binding } from '@comark/nuxt/plugins/binding'

const markdown = 'Hello {{ data.user.name }}, you have {{ data.count }} messages.'
</script>

<template>
  <Markdown
    :value="markdown"
    :plugins="[binding()]"
    :components="{ Binding }"
    :data="{ user: { name: 'Ada' }, count: 3 }"
  />
</template>
```

### Runtime `data` prop

::code-group
```typescript [Before]
// MDC: data merged at parse time
const result = await parseMarkdown(md, { data: { user, count } })
```

```typescript [After]
// Comark: parse without data, pass data when rendering
const document = await parseMarkdown(md, { plugins: [binding()] })

// Vue / Nuxt
// <MarkdownDocument :value="document" :data="{ user, count }" :components="{ Binding }" />
```
::

`:prefixed` component props (`:label="data.user.name"`, `:answer="frontmatter.theAnswer"`) resolve against the same context **without** the binding plugin. Use the plugin only when you need inline `{{ path }}` text interpolation (or `::if` with the renderer `If` component).

::tip{to="https://comark.dev/plugins/built-in/binding"}
Full reference: binding plugin, `||` defaults, `::if` / `#else`, and per-renderer imports.
::

## Quick reference

| Task                          | MDC                                                   | Comark                                                                              |
| ----------------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------- |
| Install (core only)           | `@nuxtjs/mdc`                                         | `comark`                                                                            |
| Install (Nuxt)                | `@nuxtjs/mdc`                                         | `@comark/nuxt`                                                                      |
| Nuxt module                   | `@nuxtjs/mdc`                                         | `@comark/nuxt`                                                                      |
| Global plugin config          | `mdc: { ... }` in nuxt.config                         | — (no global config)                                                                |
| Reusable configured component | —                                                     | `defineMarkdownComponent({ plugins, components })`                                  |
| Reusable configured renderer  | —                                                     | `defineMarkdownDocumentComponent({ components })`                                   |
| Parse                         | `parseMarkdown(md)`                                   | `parseMarkdown(md)`                                                                 |
| Render to Markdown            | `stringifyMarkdown(body, data)`                       | `renderMarkdown(document)`                                                          |
| Reusable parser               | `await createMarkdownParser(opts)`                    | `createMarkdownParser(opts)`                                                        |
| AST root                      | `result.body` (`MDCRoot` object)                      | `document.nodes` (tuple array)                                                      |
| Frontmatter                   | `result.data`                                         | `document.frontmatter`                                                              |
| TOC                           | `result.toc` (built-in)                               | `document.meta.toc` (toc plugin)                                                    |
| Excerpt                       | `result.excerpt` (built-in)                           | `document.meta.summary` (summary plugin)                                            |
| Renderer                      | `<MDCRenderer :body :data>`                           | `<MarkdownDocument :value>`                                                         |
| All-in-one                    | `<MDC :value>`                                        | `<Markdown :value>`                                                                 |
| Highlight                     | `mdc.highlight` in nuxt.config                        | `shiki()` plugin in `defineMarkdownComponent`                                       |
| Binding `{{ var }}`           | Built-in, bare paths + parse-time `data`              | `binding()` + `Binding`; paths like `data.x` / `frontmatter.x`; render-time `:data` |
| Prop data binding             | Parse-time `data` + `{{ }}`                           | `:prop="path"` always; `data` prop on renderer                                      |
| Prose components              | `components/prose/Prose*.vue`                         | `components/prose/Prose*.vue`                                                       |
| Render slot                   | `<MDCSlot />`                                         | `<slot />`                                                                          |
| Unwrap slot                   | `<MDCSlot unwrap="p" />` or `<slot mdc-unwrap="p" />` | `<slot unwrap="p" />`                                                               |
| Streaming                     | Not supported                                         | `streaming` prop + `autoClose`                                                      |

---

- [Document Model](https://comark.dev/getting-started/document-model)
- [MDC (legacy)](https://github.com/nuxt-content/mdc)


## Sitemap

See the full [sitemap](https://comark.dev/sitemap.md) for all pages.
