How to use Nuxt MDC to render markdown documents in your Nuxt or Vue project

Are you building a Nuxt or Vue application and want to use Markdown for writing your content? Do you want to render Markdown documents with syntax highlighting and MDC (MarkDown Components)?

The @nuxtjs/mdc module is the perfect solution 🎉!

What is Nuxt MDC?

The @nuxtjs/mdc module is a powerful tool for Nuxt.js and Vue.js applications that allows you to render Markdown documents directly in your project. It comes with built-in support for MarkDown Components (MDC), enabling you to use Markdown syntax to define reusable components in your markdown content.

One of the standout features of Nuxt MDC is its integration with Shiki, a syntax highlighter, which allows for beautiful and readable code snippets in your Markdown documents. This makes @nuxtjs/mdc an excellent choice for projects where you want to write content in Markdown, with the added benefits of syntax highlighting and built-in Vue component support.

Support for Vue projects

Until recently, the package was only functional as a Nuxt module; however, while working on a new developer portal proof-of-concept project at Kong, I needed to be able to render markdown documents that contained MDC in our non-Nuxt Vue ecosystem.

After submitting a Pull Request and collaborating with @a_birang from Nuxt Labs while at Vue Amsterdam, you can now use Nuxt MDC's exported components and utilities in your Vue project as well!

How to use Nuxt MDC in your Vue application

Since using the @nuxtjs/mdc module in a Nuxt application is well-documented, this article will focus on how to use the package in a regular Vue project.

The <MDCRenderer> component in combination with a few exported package utilities may also be utilized inside a normal (non-Nuxt) Vue project.

Check out a working example on CodeSandbox

To implement in your standard Vue project, follow the instructions below.

Install the package

First, install the @nuxtjs/mdc package in your project:

Stub Nuxt module imports

Since you're not using Nuxt, you'll need to stub a few of the module's imports in your Vue projects's Vite config file. This is necessary to avoid errors when the module tries to access Nuxt-specific imports.

Create a new file in your Vue project's root directory, such as stub-mdc-imports.js, and add the following content:

// stub-mdc-imports.js
export default {}

Next, update your Vue project's Vite config file (e.g. vite.config.ts) to alias the module's imports to the stub file:

import { defineConfig } from 'vite'
import path from 'path'

export default defineConfig({
  resolve: {
    alias: {
      '#mdc-imports': path.resolve(__dirname, './stub-mdc-imports.js'),
      '#mdc-configs': path.resolve(__dirname, './stub-mdc-imports.js'),
    }
  }
})

Usage

Next, let's create a new Vue composable to handle parsing the markdown content, as well as adding syntax highlighting to code blocks with Shiki.

// composables/useMarkdownParser.ts
// Import package exports
import {
  createMarkdownParser,
  rehypeHighlight,
  createShikiHighlighter,
} from '@nuxtjs/mdc/runtime'
// Import desired Shiki themes and languages
import MaterialThemePalenight from 'shiki/themes/material-theme-palenight.mjs'
import HtmlLang from 'shiki/langs/html.mjs'
import MdcLang from 'shiki/langs/mdc.mjs'
import TsLang from 'shiki/langs/typescript.mjs'
import VueLang from 'shiki/langs/vue.mjs'
import ScssLang from 'shiki/langs/scss.mjs'
import YamlLang from 'shiki/langs/yaml.mjs'

export default function useMarkdownParser() {
  let parser: Awaited<ReturnType<typeof createMarkdownParser>>

  const parse = async (markdown: string) => {
    if (!parser) {
      parser = await createMarkdownParser({
        rehype: {
          plugins: {
            highlight: {
              instance: rehypeHighlight,
              options: {
                // Pass in your desired theme(s)
                theme: 'material-theme-palenight',
                // Create the Shiki highlighter
                highlighter: createShikiHighlighter({
                  bundledThemes: {
                    'material-theme-palenight': MaterialThemePalenight,
                  },
                  // Configure the bundled languages
                  bundledLangs: {
                    html: HtmlLang,
                    mdc: MdcLang,
                    vue: VueLang,
                    yml: YamlLang,
                    scss: ScssLang,
                    ts: TsLang,
                    typescript: TsLang,
                  },
                }),
              },
            },
          },
        },
      })
    }
    return parser(markdown)
  }

  return parse
}

Now import the useMarkdownParser composable we just created along with an exported type interface into your host project's Vue component, and utilize them to process the raw markdown and initialize the <MDCRenderer> component.

<script setup lang="ts">
import { onBeforeMount, ref, watch } from 'vue'
// Import package exports
import MDCRenderer from '@nuxtjs/mdc/runtime/components/MDCRenderer.vue'
import type { MDCParserResult } from '@nuxtjs/mdc/runtime/types/index'
import useMarkdownParser from './composables/useMarkdownParser';

const md = ref(`
# Just a Vue app

This is markdown content rendered via the \`<MDCRenderer>\` component,
including MDC below.

::alert
Hello MDC
::

\`\`\`ts
const a = 1;
\`\`\`
`);

const ast = ref<MDCParserResult | null>(null)
const parse = useMarkdownParser()

onBeforeMount(async () => {
  ast.value = await parse(md.value)
})
</script>

<template>
  <Suspense>
    <MDCRenderer v-if="ast?.body" :body="ast.body" :data="ast.data" />
  </Suspense>
</template>