API Reference

Render from Tree

Parse once anywhere — server, build step, or API — then pass the ComarkTree to ComarkRenderer to render it separately without bundling the parser on the client.

Comark separates parsing from rendering into two independent steps. The parse function converts markdown into a serializable ComarkTree, and ComarkRenderer turns that tree into UI. Because the steps are decoupled, you can parse anywhere — a server route, a build script, a background job, or an API — and hand the plain JSON tree to ComarkRenderer wherever rendering happens.

The parser (comark) only needs to run where you call parse. ComarkRenderer in @comark/vue or @comark/react is all the client needs — no parser, no plugins, no extra bundle weight.

How It Works

Server                           Client
──────────────────────────────   ──────────────────────────────
markdown string                  ComarkTree (plain JSON)
    │                                │
    ▼                                ▼
parse() → ComarkTree  ──────▶  ComarkRenderer → DOM
  1. Server — call parse() to produce a ComarkTree
  2. Transfer — serialize the tree as JSON (in a response body, page props, etc.)
  3. Client — pass the tree to <ComarkRenderer> and render

Vue / Nuxt

Nuxt Server Route

Parse in a Nuxt server route and return the tree as JSON. The client page receives it via useFetch and renders it directly.

import { createParse } from 'comark'
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'

const parse = createParse()

export default defineEventHandler(async (event) => {
  const slug = getRouterParam(event, 'slug')
  const markdown = await readFile(join('content', `${slug}.md`), 'utf-8')
  return parse(markdown)
})

Nuxt Server Component

With Nuxt server components, the parse call never leaves the server context at all.

components/DocContent.server.vue
<script setup lang="ts">
import { parse } from 'comark'
import { ComarkRenderer } from '@comark/vue'
import { readFile } from 'node:fs/promises'

const props = defineProps<{ slug: string }>()
const markdown = await readFile(`content/${props.slug}.md`, 'utf-8')
const tree = await parse(markdown)
</script>

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

Standard Vue with API

App.vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ComarkRenderer } from '@comark/vue'
import type { ComarkTree } from 'comark'

const tree = ref<ComarkTree | null>(null)

onMounted(async () => {
  const res = await fetch('/api/content/introduction')
  tree.value = await res.json()
})
</script>

<template>
  <ComarkRenderer v-if="tree" :tree="tree" />
</template>

React / Next.js

Next.js Server Component

Server Components run exclusively on the server. Call parse directly inside the component — it is never included in the client bundle.

import { parse } from 'comark'
import { ComarkRenderer } from '@comark/react'
import { readFile } from 'node:fs/promises'
import Alert from '@/components/Alert'

interface PageProps {
  params: { slug: string }
}

export default async function DocsPage({ params }: PageProps) {
  const markdown = await readFile(`content/${params.slug}.md`, 'utf-8')
  const tree = await parse(markdown)

  return (
    <ComarkRenderer
      tree={tree}
      components={{ Alert }}
    />
  )
}

Next.js API Route + Client Component

Use this pattern when the page itself must be a Client Component.

import { createParse } from 'comark'
import { readFile } from 'node:fs/promises'
import { NextResponse } from 'next/server'

const parse = createParse()

export async function GET(
  _req: Request,
  { params }: { params: { slug: string } }
) {
  const markdown = await readFile(`content/${params.slug}.md`, 'utf-8')
  const tree = await parse(markdown)
  return NextResponse.json(tree)
}

SvelteKit

Parse in the load function — it runs on the server during SSR and only the tree is sent to the browser.

import { parse } from 'comark'
import { readFile } from 'node:fs/promises'
import type { PageServerLoad } from './$types'

export const load: PageServerLoad = async ({ params }) => {
  const markdown = await readFile(`src/content/${params.slug}.md`, 'utf-8')
  const tree = await parse(markdown)
  return { tree }
}

Node.js API Server

Use createParse to initialize the parser once at startup, then reuse it for every request.

server.ts
import express from 'express'
import { createParse } from 'comark'
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'

const app = express()
app.use(express.json())

// Parser is initialized once when the server starts
const parse = createParse({
  plugins: [
    highlight({ themes: { light: githubLight, dark: githubDark } }),
    toc(),
  ],
})

app.post('/api/parse', async (req, res) => {
  const tree = await parse(req.body.markdown)
  res.json(tree)
})

app.listen(3000)

ComarkRenderer Props

ComarkRenderer is available in @comark/vue, @comark/react, and @comark/svelte. It accepts the same core props across all frameworks.

PropTypeDefaultDescription
treeComarkTreeRequired. The parsed tree returned by parse()
componentsRecord<string, Component>{}Map of tag names to custom components
componentsManifestComponentManifestundefinedDynamic component resolver for lazy-loaded components
streamingbooleanfalseEnable streaming mode (use with auto-close)
caretboolean | { class: string }falseAppend a blinking caret to the last text node
ComarkRenderer only renders. It does not parse. You must pass a ComarkTree — calling parse() inside a client component defeats the purpose of server-side parsing.

Vue Example

DocsPage.vue
<script setup lang="ts">
import { ComarkRenderer } from '@comark/vue'
import type { ComarkTree } from 'comark'
import CustomAlert from './Alert.vue'
import CustomCode from './Code.vue'

defineProps<{ tree: ComarkTree }>()

const components = {
  alert: CustomAlert,
  pre: CustomCode,
}
</script>

<template>
  <ComarkRenderer :tree="tree" :components="components" />
</template>

React Example

DocsPage.tsx
import { ComarkRenderer } from '@comark/react'
import type { ComarkTree } from 'comark'
import CustomAlert from './Alert'
import CustomCode from './Code'

const components = {
  alert: CustomAlert,
  pre: CustomCode,
}

export function DocsPage({ tree }: { tree: ComarkTree }) {
  return <ComarkRenderer tree={tree} components={components} />
}

With Plugins

Plugins run at parse time on the server. The results (highlighted code, TOC, etc.) are embedded in the ComarkTree and arrive at the client already processed — no plugin code is shipped to the browser.

server.ts
import { createParse } from 'comark'
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'

const parse = createParse({
  plugins: [
    // All plugin output is embedded in the ComarkTree — no plugin JS sent to client
    highlight({ themes: { light: githubLight, dark: githubDark } }),
    toc({ depth: 3 }),
    emoji(),
  ],
})

const tree = await parse(markdown)
// tree.meta.toc    → table of contents
// tree.nodes       → rendered highlight spans already embedded

See Also

Copyright © 2026