Render from Tree
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.
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
- Server — call
parse()to produce aComarkTree - Transfer — serialize the tree as JSON (in a response body, page props, etc.)
- 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)
})
<script setup lang="ts">
import { ComarkRenderer } from '@comark/vue'
import Alert from '~/components/Alert.vue'
const route = useRoute()
const { data: tree } = await useFetch(`/api/content/${route.params.slug}`)
const components = { Alert }
</script>
<template>
<ComarkRenderer v-if="tree" :tree="tree" :components="components" />
</template>
Nuxt Server Component
With Nuxt server components, the parse call never leaves the server context at all.
<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
<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 }}
/>
)
}
interface AlertProps {
type?: 'info' | 'warning' | 'error' | 'success'
children?: React.ReactNode
}
export default function Alert({ type = 'info', children }: AlertProps) {
return (
<div className={`alert alert-${type}`} role="alert">
{children}
</div>
)
}
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)
}
'use client'
import { ComarkRenderer } from '@comark/react'
import { useEffect, useState } from 'react'
import type { ComarkTree } from 'comark'
export function DocContent({ slug }: { slug: string }) {
const [tree, setTree] = useState<ComarkTree | null>(null)
useEffect(() => {
fetch(`/api/content/${slug}`)
.then(res => res.json())
.then(setTree)
}, [slug])
if (!tree) return null
return <ComarkRenderer tree={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 }
}
<script lang="ts">
import { ComarkRenderer } from '@comark/svelte'
import type { PageData } from './$types'
export let data: PageData
</script>
<ComarkRenderer tree={data.tree} />
Node.js API Server
Use createParse to initialize the parser once at startup, then reuse it for every request.
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.
| Prop | Type | Default | Description |
|---|---|---|---|
tree | ComarkTree | — | Required. The parsed tree returned by parse() |
components | Record<string, Component> | {} | Map of tag names to custom components |
componentsManifest | ComponentManifest | undefined | Dynamic component resolver for lazy-loaded components |
streaming | boolean | false | Enable streaming mode (use with auto-close) |
caret | boolean | { class: string } | false | Append 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
<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
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.
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
- Parse API —
parse()andcreateParse()options - Vue Rendering — full
<Comark>component (parse + render in one) - React Rendering — React integration
- Streaming — incremental rendering for AI chat interfaces