---
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 })`
- **Markdown files**: no changes needed

## 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
```
</code-group>

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
```
</code-group>

## 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)
```
</code-group>

#### 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)
```
</code-group>

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.
</tip>

### 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)
```
</code-group>

`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
  }
}
```
</code-group>

**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...
```
</code-group>

**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.
</tip>

### 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 }
    })
  ]
})
```
</code-group>

#### 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
```
</code-group>

#### 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
```
</code-group>

#### 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()] })
```
</code-group>

### 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
}
```
</code-group>

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.
</tip>

## 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)
})
```
</code-group>

`@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" />
```
</code-group>

Props that changed:

| MDC `<MDCRenderer>` | Comark `<MarkdownDocument>`  | Notes                                                                                                                               |
| ------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `body` (`MDCRoot`)  | `value` (`MarkdownDocument`) | Different shape, see AST section                                                                                                    |
| `data`              | —                            | `document.frontmatter` contains frontmatter, not a separate prop                                                                    |
| `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>
```
</code-group>

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.
</tip>

### `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/plugins/shiki'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'
import toc from 'comark/plugins/toc'
import emoji from 'comark/plugins/emoji'
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>
```
</code-group>

#### 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" />
```
</code-group>

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>
```
</code-group>

### 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).
</note>

### 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.
</tip>

## Unsupported features

Comark does not currently support the following MDC features:

### Binding syntax

MDC's binding syntax (`{{ variable }}`) for interpolating data inside markdown content is not supported in Comark. This means you cannot reference variables or expressions inline within your markdown text.

```mdc
<!-- MDC: worked -->
Hello {{ user.name }}, you have {{ count }} messages.

<!-- Comark: not supported -->
<!-- The {{ }} syntax will be rendered as plain text -->
```

### Props binding and data passing

Because Comark does not support binding syntax, dynamic props binding and data passing from a parent context into markdown content are also not supported. In MDC, you could pass data to the markdown parser and reference it inside the document. This pattern has no equivalent in Comark.

```typescript
// MDC: data available inside markdown via {{ }}
const result = await parseMarkdown(md, { data: { user, count } })

// Comark: no equivalent
```

<warning>
These features may be added in a future release. If your project relies on binding syntax or data passing, [create a feature request](https://github.com/comarkdown/comark/issues) on GitHub so we can prioritize accordingly.
</warning>

## 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`      |
| 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.
