Render Comark to HTML
The @comark/html package provides framework-free rendering of Comark AST to HTML strings. Use it for server-side rendering, static site generation, RSS feeds, emails, or any context where you don't need any framework.
Installation
pnpm add @comark/htmlnpm install @comark/htmlyarn add @comark/htmlbun add @comark/htmlrender()
The quickest way to parse markdown and get an HTML string in one call.
Usage
import { render } from '@comark/html'
const html = await render(`
# Getting Started
This is a **bold** statement with a [link](https://example.com).
- Item 1
- Item 2
`)<h1 id="getting-started">Getting Started</h1>
<p>This is a <strong>bold</strong> statement with a <a href="https://example.com">link</a>.</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>Options
| Option | Type | Default | Description |
|---|---|---|---|
plugins | ComarkPlugin[] | [] | Array of plugins |
components | Record<string, fn> | {} | Custom component renderers |
data | Record<string, any> | undefined | Data passed to component renderers |
plugins
See ComarkPlugin for available plugins.
import { render } from '@comark/html'
import highlight from '@comark/html/plugins/highlight'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'
const html = await render('```js\nconsole.log("hi")\n```', {
plugins: [
highlight({
themes: { light: githubLight, dark: githubDark }
})
],
})components
Map component names to async render functions. Each function receives the element as [tag, attrs, ...children] and a context with render to process nested content:
import { render } from '@comark/html'
const html = await render(`
::alert{type="warning"}
This is a warning message!
::
`, {
components: {
alert: async ([, attrs, ...children], { render }) => {
return `<div class="alert alert-${attrs.type}" role="alert">${await render(children)}</div>`
}
}
})<div class="alert alert-warning" role="alert"><p>This is a warning message!</p></div>data
Pass external data to every component renderer via the context object:
import { render } from '@comark/html'
const html = await render(`
::header
Welcome!
::
`, {
data: { siteName: 'My Blog' },
components: {
header: async ([, , ...children], { render, data }) => {
return `<header><h1>${data?.siteName}</h1>${await render(children)}</header>`
}
}
})<header><h1>My Blog</h1><p>Welcome!</p></header>createRender()
Creates a reusable parse+render function. The underlying parser is initialized once and reused on every call — more efficient when rendering many documents.
Usage
import { createRender } from '@comark/html'
import highlight from '@comark/html/plugins/highlight'
const render = createRender({
plugins: [highlight()],
})
// Reuse the same configured parser
const html1 = await render('# Document 1\n\n...')
const html2 = await render('# Document 2\n\n...')Options
Same as render().
renderHTML()
Renders a pre-parsed ComarkTree directly — no parsing step. Use it when you already have a tree from a prior parse, build step, or API call.
Integration
Parse on the server and render in a separate step — no parser or plugin code needed at render time:
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)
})Pass the pre-parsed tree to renderHTML:
import { renderHTML } from '@comark/html'
const tree = await $fetch(`/api/content/${slug}`)
const html = await renderHTML(tree)Options
| Option | Type | Description |
|---|---|---|
components | Record<string, fn> | Custom component renderers |
data | Record<string, any> | Data passed to component renderers |
Overriding HTML Elements
Pass native HTML tag names as keys in components to override how standard elements render:
import { createRender } from '@comark/html'
const render = createRender({
components: {
h1: async ([, attrs, ...children], { render }) => {
const anchor = attrs.id ? `<a href="#${attrs.id}">#</a>` : ''
return `<h1 id="${attrs.id}" class="heading">${anchor}${await render(children)}</h1>`
},
a: async ([, attrs, ...children], { render }) => {
const external = attrs.href?.startsWith('http') ? ' target="_blank" rel="noopener"' : ''
return `<a href="${attrs.href}"${external}>${await render(children)}</a>`
}
}
})TypeScript Support
import type { ComarkElement, ComarkNode } from 'comark'
import { createRender } from '@comark/html'
type RenderContext = {
render: (nodes: ComarkNode[]) => Promise<string>
data?: Record<string, any>
}
type ComponentRenderer = (element: ComarkElement, ctx: RenderContext) => Promise<string>
const components: Record<string, ComponentRenderer> = {
alert: async ([, attrs, ...children], { render }) => {
return `<div class="alert alert-${attrs.type}">${await render(children)}</div>`
}
}
const render = createRender({ components })Use Cases
Static Site Generation
import { readFile, writeFile } from 'node:fs/promises'
import { createRender } from '@comark/html'
import highlight from '@comark/html/plugins/highlight'
const render = createRender({ plugins: [highlight()] })
async function buildPage(filePath: string) {
const source = await readFile(filePath, 'utf-8')
const html = await render(source)
await writeFile('out/index.html', `
<!DOCTYPE html>
<html>
<head><title>My Page</title></head>
<body>${html}</body>
</html>
`)
}RSS Feed
import { createRender } from '@comark/html'
const render = createRender()
async function generateRSSItem(source: string) {
const html = await render(source)
return `
<item>
<description><![CDATA[${html}]]></description>
</item>
`
}API Response
import { createRender } from '@comark/html'
const render = createRender()
async function handleRequest(markdownContent: string) {
return Response.json({ html: await render(markdownContent) })
}