Knowledge Base

Migrating from MDC

Step-by-step guide for moving from @nuxtjs/mdc to Comark.

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.


Package Changes

npm uninstall @nuxtjs/mdc
npm install comark @comark/vue        # Vue
npm install comark @comark/react      # React
npm install comark @comark/nuxt       # Nuxt

Nuxt Module

nuxt.config.ts
export default defineNuxtConfig({
  // Before
  modules: ['@nuxtjs/mdc'],
  mdc: { highlight: { ... }, remarkPlugins: { ... } },

  // After
  modules: ['@comark/nuxt'],
  // No plugin config here — plugins are defined per-component (see below)
})

@comark/nuxt auto-imports Comark, ComarkRenderer, defineComarkComponent, and defineComarkRendererComponent. Plugin and component configuration moves out of nuxt.config.ts and into dedicated component definitions.


Parse Functions

One-shot parsing

// Before
import { parseMarkdown } from '@nuxtjs/mdc/runtime'
const result = await parseMarkdown(markdown)

// After
import { parse } from 'comark'
const tree = await parse(markdown)

Reusable parser

// Before
import { createMarkdownParser } from '@nuxtjs/mdc/runtime'
const parser = await createMarkdownParser(options)
const result = await parser(markdown)

// After
import { createParse } from 'comark'
const parse = createParse(options)   // synchronous — no await needed
const tree = await parse(markdown)
createParse is synchronous. createMarkdownParser was async because it had to initialize the unified processor pipeline. Comark's plugin system is eagerly initialized, so the factory returns immediately.

Return Type

The shape of the parsed result changed.

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

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

Quick reference:

MDCComark
result.bodytree.nodes
result.datatree.frontmatter
result.data.titletree.frontmatter.title
result.toctree.meta.toc (requires toc plugin)
result.excerpttree.meta.summary (requires summary plugin)

AST Format

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

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

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

Mapping:

MDCComark
node.type === 'element'Array.isArray(node)
node.type === 'text'typeof node === 'string'
node.tagnode[0]
node.propsnode[1]
node.childrennode.slice(2)

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


Renderer Component

Vue

<!-- Before -->
<MDCRenderer :body="result.body" :data="result.data" :components="components" />

<!-- After -->
<ComarkRenderer :tree="tree" :components="components" />

Props that changed:

MDC <MDCRenderer>Comark <ComarkRenderer>Notes
body (MDCRoot)tree (ComarkTree)Different shape — see AST section
dataFrontmatter is in tree.frontmatter, not a separate prop
tagWrapper is always <div class="comark-content">
proseProse component resolution is automatic
unwrapUse autoUnwrap in parse options instead
classStyle the wrapper via CSS
componentscomponentsSame purpose, same shape
componentsManifestNew: dynamic async component resolver
streamingNew: streaming mode
caretNew: animated caret for streaming

All-in-one component

The <MDC> component maps to <Comark>.

<!-- Before -->
<MDC :value="markdown" :parser-options="options" />
<MDC :value="result" />   <!-- pre-parsed MDCParserResult -->

<!-- After -->
<Comark :markdown="markdown" :options="options" />
<!-- or default slot -->
<Comark>{{ markdown }}</Comark>

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


Plugins and Highlighting

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

Syntax Highlighting

// Before — rehype-based Shiki plugin via createMarkdownParser
import { createMarkdownParser, rehypeHighlight, createShikiHighlighter } from '@nuxtjs/mdc/runtime'

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

// After — Comark highlight plugin
import { createParse } from 'comark'
import highlight from 'comark/plugins/highlight'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'

const parse = createParse({
  plugins: [
    highlight({
      themes: { light: githubLight, dark: githubDark }
    })
  ]
})

Reusable components instead of global config

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 defineComarkComponent and defineComarkRendererComponent, 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.

composables/comark.ts
import { defineComarkComponent, defineComarkRendererComponent } from '@comark/vue'
import highlight from 'comark/plugins/highlight'
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 ArticleComark = defineComarkComponent({
  name: 'ArticleComark',
  plugins: [
    highlight({ 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 CommentComark = defineComarkComponent({
  name: 'CommentComark',
})
pages/article/[slug].vue
<script setup lang="ts">
import { ArticleComark, CommentComark } from '~/composables/comark'
</script>

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

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

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

composables/comark.ts
export const ArticleRenderer = defineComarkRendererComponent({
  name: 'ArticleRenderer',
  components: {
    alert: CustomAlert,
    ProsePre,
  },
})
pages/article/[slug].vue
<script setup lang="ts">
import { ArticleRenderer } from '~/composables/comark'

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

<template>
  <ArticleRenderer :tree="tree" />
</template>

Table of Contents

// Before — built into parseMarkdown
import { parseMarkdown } from '@nuxtjs/mdc/runtime'
const result = await parseMarkdown(md, { toc: { depth: 3 } })
const toc = result.toc

// After — requires toc plugin
import { parse } from 'comark'
import toc from 'comark/plugins/toc'

const tree = await parse(md, { plugins: [toc({ depth: 3 })] })
const tableOfContents = tree.meta.toc

Excerpt / Summary

// Before — built in, splits on <!-- more -->
const result = await parseMarkdown(md)
const excerpt = result.excerpt  // MDCRoot | undefined

// After — requires summary plugin
import { parse } from 'comark'
import summary from 'comark/plugins/summary'

const tree = await parse(md, { plugins: [summary()] })
const summaryNodes = tree.meta.summary  // ComarkNode[] | undefined

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

<!-- Before -->
<MDCRenderer :body="result.excerpt ?? result.body" :data="result.data" />

<!-- After -->
<Comark summary>{{ markdown }}</Comark>

Emoji

// Before — enabled by default via remark-emoji
// mdc: { remarkPlugins: { 'remark-emoji': {} } }

// After — opt-in plugin
import { parse } from 'comark'
import emoji from 'comark/plugins/emoji'

const tree = await parse(md, { plugins: [emoji()] })

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:

ElementMDC component nameComark component name
<p>ProsePProseP
<h1>ProseH1ProseH1
<a>ProseAProseA
<pre>ProsePreProsePre
<code>ProseCodeProseCode

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.


Parse Options

// Before — MDCParseOptions
{
  remark: { plugins: { /* record */ } },
  rehype: { options: {...}, plugins: { /* record */ } },
  highlight: { theme: '...', langs: [...] } | false,
  toc: { depth: 3, searchDepth: 2 } | false,
  keepComments: false,
  keepPosition: false,
}

// After — ParseOptions
{
  plugins: ComarkPlugin[],   // ordered array, not a record
  autoUnwrap: true,          // removes <p> from single-paragraph containers
  autoClose: true,           // completes incomplete syntax (useful for streaming)
  html: true,                // parse embedded HTML tags
}

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 — see Plugins.


Component Syntax

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

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

:badge{label="New"}

:::card
#header
Card title

Body content.
:::

Quick Reference

TaskMDCComark
Install@nuxtjs/mdccomark + @comark/vue
Nuxt module@nuxtjs/mdc@comark/nuxt
Global plugin configmdc: { ... } in nuxt.config— (no global config)
Reusable configured componentdefineComarkComponent({ plugins, components })
Reusable configured rendererdefineComarkRendererComponent({ components })
ParseparseMarkdown(md)parse(md)
Reusable parserawait createMarkdownParser(opts)createParse(opts)
AST rootresult.body (MDCRoot object)tree.nodes (tuple array)
Frontmatterresult.datatree.frontmatter
TOCresult.toc (built-in)tree.meta.toc (toc plugin)
Excerptresult.excerpt (built-in)tree.meta.summary (summary plugin)
Renderer<MDCRenderer :body :data><ComarkRenderer :tree>
All-in-one<MDC :value><Comark :markdown>
Highlightmdc.highlight in nuxt.confighighlight() plugin in defineComarkComponent
Prose componentscomponents/prose/Prose*.vuecomponents/prose/Prose*.vue
StreamingNot supportedstreaming prop + autoClose

See Also

Copyright © 2026