Rendering

Render Comark in Nuxt

Learn how to use Comark in a Nuxt application with auto-imported components and Nuxt UI integration.

The @comark/nuxt module provides zero-config Comark setup for Nuxt:

Installation

pnpm add @comark/nuxt
nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@comark/nuxt']
})

<Comark>

The <Comark> component is automatically available in all your templates — no imports needed. It handles parsing and rendering in one step.

Usage

Pass markdown via the default slot or the markdown prop:

<template>
  <Comark>{{ content }}</Comark>
</template>

Props

PropTypeDefaultDescription
markdownstringundefinedAlternative to default slot
optionsParseOptions{}Parser options (autoUnwrap, autoClose, etc.)
pluginsComarkPlugin[][]Array of plugins
componentsRecord<string, Component>{}Custom Vue component mappings
componentsManifestComponentManifestundefinedDynamic component resolver
streamingbooleanfalseEnable streaming mode
summarybooleanfalseOnly render content before <!-- more -->
caretboolean | { class: string }falseAppend caret to last text node

options

See ParseOptions for available options.

app.vue
<template>
  <Comark :options="{ autoUnwrap: true, autoClose: true }">
    {{ content }}
  </Comark>
</template>

plugins

See ComarkPlugin for available plugins.

app.vue
<script setup lang="ts">
import highlight from '@comark/vue/plugins/highlight'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'

const plugins = [
  highlight({
    themes: { light: githubLight, dark: githubDark }
  })
]
</script>

<template>
  <Comark :plugins="plugins">{{ content }}</Comark>
</template>

For math and mermaid plugins, also pass the companion components:

app.vue
<script setup lang="ts">
import math, { Math } from '@comark/vue/plugins/math'
import mermaid, { Mermaid } from '@comark/vue/plugins/mermaid'
</script>

<template>
  <Comark
    :markdown="content"
    :components="{ math: Math, mermaid: Mermaid }"
    :plugins="[math(), mermaid()]"
  />
</template>

components

Use this prop to map custom Vue components to Comark elements and use them in your markdown.

Create a Vue component

Save a component such as components/Alert.vue:

components/Alert.vue
<script setup lang="ts">
defineProps<{
  type?: 'info' | 'warning' | 'error' | 'success'
}>()
</script>

<template>
  <div class="alert" :class="`alert-${type || 'info'}`" role="alert">
    <slot />
  </div>
</template>

Map the tag to your component

Pass the components prop to <Comark>:

app.vue
<script setup lang="ts">
import Alert from '~/components/Alert.vue'
import Card from '~/components/Card.vue'

const components = { alert: Alert, card: Card }
</script>

<template>
  <Comark :components="components">{{ content }}</Comark>
</template>
Components receive props from the markdown and render children via slots.

Use it in your Markdown content

::alert{type="warning"}
This is a warning message!
::
To override how native HTML elements render (e.g. h1, pre), place components in ~/components/prose instead. See Overriding HTML Elements.

componentsManifest

For lazy-loading components on demand. Components are resolved once and cached.

app.vue
<script setup lang="ts">
const manifest = (name: string) => {
  return import(`./components/prose/${name}.vue`)
}
</script>

<template>
  <Comark :components-manifest="manifest">{{ content }}</Comark>
</template>

Server-Side Rendering

Comark fully supports multiple rendering modes in Nuxt:

Static Generation

pages/index.vue
<script setup lang="ts">
// Content is parsed at build time
const content = `# Static Content

This is rendered at build time.
`
</script>

<template>
  <Comark>{{ content }}</Comark>
</template>

Dynamic SSR

pages/blog/[slug].vue
<script setup lang="ts">
// Fetch and render on the server
const { data: content } = await useFetch('/api/article')
</script>

<template>
  <Comark v-if="content">{{ content }}</Comark>
</template>

Prerendering

nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@comark/nuxt'],
  nitro: {
    prerender: {
      routes: ['/blog', '/docs']
    }
  }
})

Overriding HTML Elements

Place Vue components in ~/components/prose to override how native HTML elements render — automatically registered, no imports or components prop needed.

Directory Structure

~/components/
  prose/
    ProseH1.vue
    ProseH2.vue
    ProsePre.vue
    ProseA.vue

Example

components/prose/ProseH1.vue
<script setup lang="ts">
defineProps<{
  id?: string
}>()
</script>

<template>
  <h1 :id="id" class="custom-heading">
    <a v-if="id" :href="`#${id}`">#</a>
    <slot />
  </h1>
</template>

Resolution Order

When Comark encounters a tag, it looks for a matching component in this order, stopping at the first match:

  1. Prose{PascalTag} — e.g., ProseH1 for h1. Nuxt auto-registers all components from ~/components/prose matching this convention.
  2. {PascalTag} — e.g., Alert for alert. PascalCase version of the tag name.
  3. {tag} — e.g., alert. Exact tag name as-is.
  4. Global — any component registered via app.component() or Nuxt auto-imports.

If none match, the tag renders as a native HTML element.


Component Bindings

Comark automatically bridges the gap between Comark syntax and your component's interface.

Prop Binding

Attributes in Comark syntax are passed as props to your component. Use the : prefix to pass typed values:

MarkdownProp value
{type="warning"}"warning" (string)
{:count="5"}5 (number)
{:active="true"}true (boolean)
{:config='{"key":"val"}'}{ key: 'val' } (object)

Named Slots

Named slots in Comark (#slotname) map to Vue named slots:

  • Default slot<slot />
  • Named slots<slot name="slotname" /> (e.g., #footer<slot name="footer" />)
<script setup lang="ts">
defineProps<{
  title?: string
}>()
</script>

<template>
  <div class="card">
    <h3 v-if="title">{{ title }}</h3>
    <slot />
    <footer>
      <slot name="footer" />
    </footer>
  </div>
</template>

Nuxt UI Integration

When @nuxt/ui is installed, Comark automatically uses Nuxt UI's prose components for enhanced styling.

No extra configuration needed. The @comark/nuxt module detects @nuxt/ui in your dependencies and configures prose components automatically.
nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@comark/nuxt', '@nuxt/ui']
})

CSS Setup

app/assets/css/main.css
@import "tailwindcss";
@import "@nuxt/ui";
nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@comark/nuxt', '@nuxt/ui'],
  css: ['~/assets/css/main.css']
})

Streaming

Enable real-time rendering as content arrives — ideal for AI chat interfaces and live previews.

Setup

Set streaming to true while content is being received, then false when done:

app.vue
<script setup lang="ts">
const content = ref('')
const isStreaming = ref(false)

async function askAI(prompt: string) {
  content.value = ''
  isStreaming.value = true

  const response = await fetch('/api/chat', {
    method: 'POST',
    body: JSON.stringify({ prompt }),
  })

  const reader = response.body!.getReader()
  const decoder = new TextDecoder()

  while (true) {
    const { done, value } = await reader.read()
    if (done) break
    content.value += decoder.decode(value, { stream: true })
  }

  isStreaming.value = false
}
</script>

<template>
  <Comark :streaming="isStreaming" caret>
    {{ content }}
  </Comark>
</template>
autoClose is enabled by default — incomplete syntax like **bold text is automatically closed on every parse. Disable with :options="{ autoClose: false }".

Caret

The caret prop appends a blinking cursor to the last text node while streaming is true:

app.vue
<!-- Default caret -->
<Comark :streaming="isStreaming" caret>{{ content }}</Comark>

<!-- Custom caret class -->
<Comark :streaming="isStreaming" :caret="{ class: 'my-caret' }">{{ content }}</Comark>
.my-caret {
  display: inline-block;
  width: 2px;
  height: 1em;
  background: currentColor;
  animation: blink 1s step-end infinite;
  vertical-align: text-bottom;
}

@keyframes blink {
  50% { opacity: 0; }
}

TypeScript Support

Use ComarkPlugin from comark to type plugin arrays, and ComarkElement to type the __node prop in components that override HTML elements:

components/ComarkWrapper.vue
<script setup lang="ts">
import type { Component } from 'vue'
import type { ComarkPlugin } from 'comark'

interface Props {
  content: string
  components?: Record<string, Component>
  plugins?: ComarkPlugin[]
}

const props = defineProps<Props>()
</script>

<template>
  <Comark :components="props.components" :plugins="props.plugins">
    {{ props.content }}
  </Comark>
</template>
components/prose/ProseH1.vue
<script setup lang="ts">
import type { ComarkElement } from 'comark'

defineProps<{
  __node?: ComarkElement
  id?: string
}>()
</script>