Rendering

Render Comark to HTML

Render Comark AST to HTML strings without any framework dependency.

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/html

render()

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
`)

Options

OptionTypeDefaultDescription
pluginsComarkPlugin[][]Array of plugins
componentsRecord<string, fn>{}Custom component renderers
dataRecord<string, any>undefinedData passed to component renderers

plugins

See ComarkPlugin for available plugins.

render.ts
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>`
    }
  }
})

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>`
    }
  }
})

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:

server/api/content/[slug].ts
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:

render.ts
import { renderHTML } from '@comark/html'

const tree = await $fetch(`/api/content/${slug}`)
const html = await renderHTML(tree)

Options

OptionTypeDescription
componentsRecord<string, fn>Custom component renderers
dataRecord<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

build.ts
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

rss.ts
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

server.ts
import { createRender } from '@comark/html'

const render = createRender()

async function handleRequest(markdownContent: string) {
  return Response.json({ html: await render(markdownContent) })
}