Render Comark to HTML

Parse Markdown or render existing documents as HTML strings without any framework dependency.

The @comark/html package parses Markdown or renders an existing MarkdownDocument to an HTML string without a framework dependency. Use it for server-side rendering, static site generation, RSS feeds, and emails.

Installation

pnpm add @comark/html

renderHtml()

The quickest way to parse markdown and get an HTML string in one call.

Usage

import { renderHtml } from '@comark/html'

const html = await renderHtml(`
# 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
autoClosebooleantrueClose incomplete Markdown and components before parsing
autoUnwrapbooleantrueRemove a single paragraph wrapper inside components
linkifybooleantrueConvert URL-like text into links
registerDefaultPluginsbooleantrueRegister default plugins (frontmatter, html, alert, task-list, components, attributes)
unwrapboolean | string | string[]falseRemove selected wrapper tags from the parsed document

renderHtml() accepts all ParserOptions in addition to the renderer options above.

plugins

See ComarkPlugin for available plugins.

import { renderHtml } from '@comark/html'
import shiki from '@comark/html/plugins/shiki'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'

const html = await renderHtml('```js\nconsole.log("hi")\n```', {
  plugins: [
    shiki({
      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 { renderHtml } from '@comark/html'

const html = await renderHtml(`
::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 { renderHtml } from '@comark/html'

const html = await renderHtml(`
::header
Welcome!
::
`, {
  data: { siteName: 'My Blog' },
  components: {
    header: async ([, , ...children], { render, data }) => {
      return `<header><h1>${data?.siteName}</h1>${await render(children)}</header>`
    }
  }
})

createHtmlRenderer()

Creates a reusable parse+render function. The underlying parser is initialized once and reused on every call, which is more efficient when rendering many documents.

Usage

import { createHtmlRenderer } from '@comark/html'
import shiki from '@comark/html/plugins/shiki'

const renderHtml = createHtmlRenderer({
  plugins: [shiki()],
})

// Reuse the same configured parser
const html1 = await renderHtml('# Document 1\n\n...')
const html2 = await renderHtml('# Document 2\n\n...')

Options

Same as renderHtml().


renderHtmlFromDocument()

Renders a pre-parsed MarkdownDocument directly, with no parsing step. Use it when you already have a document 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 { createMarkdownParser } from 'comark'
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'

const parse = createMarkdownParser()

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 document to renderHtmlFromDocument:

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

const document = await $fetch(`/api/content/${slug}`)
const html = await renderHtmlFromDocument(document)

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 { createHtmlRenderer } from '@comark/html'

const renderHtml = createHtmlRenderer({
  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 { ElementNode, Node } from 'comark'
import { createHtmlRenderer } from '@comark/html'

type RenderContext = {
  render: (nodes: Node[]) => Promise<string>
  data?: Record<string, any>
}

type ComponentRenderer = (element: ElementNode, 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 renderHtml = createHtmlRenderer({ components })

Use Cases

Static Site Generation

build.ts
import { readFile, writeFile } from 'node:fs/promises'
import { createHtmlRenderer } from '@comark/html'
import shiki from '@comark/html/plugins/shiki'

const renderHtml = createHtmlRenderer({ plugins: [shiki()] })

async function buildPage(filePath: string) {
  const source = await readFile(filePath, 'utf-8')
  const html = await renderHtml(source)

  await writeFile('out/index.html', `
    <!DOCTYPE html>
    <html>
      <head><title>My Page</title></head>
      <body>${html}</body>
    </html>
  `)
}

RSS Feed

rss.ts
import { createHtmlRenderer } from '@comark/html'

const renderHtml = createHtmlRenderer()

async function generateRSSItem(source: string) {
  const html = await renderHtml(source)
  return `
    <item>
      <description><![CDATA[${html}]]></description>
    </item>
  `
}

API Response

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

const renderHtml = createHtmlRenderer()

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