Markdown to HTML in JavaScript, No Framework

Convert Markdown to HTML strings in JavaScript without a framework, for server rendering, static sites, RSS feeds, and emails.

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.

Rendering a document without Markdown serialization

@comark/html renders HTML without loading the Markdown serializers. Components receive the renderer state, but state.handlers only contains the html and comment handlers, and the output is always HTML. To render a document back to Markdown, use render or renderMarkdown from comark/render.

If you already have a parsed document, import from @comark/html/render. It leaves out the parser, so it doesn't load YAML either:

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

const html = await renderHtmlFromDocument(document)

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
transformNodeNodeTransform | NodeTransform[]undefinedChange the tag or props of element nodes right before they render
autoClosebooleantrueClose incomplete Markdown and components before parsing
autoUnwrapbooleantrueRemove a single paragraph wrapper inside components
linkifybooleantrueDeprecated. Use registerDefaultPlugins: false and register linkify() instead
registerDefaultPluginsbooleantrueRegister default plugins (linkify, 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>`
    }
  }
})

transformNode

Change an element's tag or props right before it renders, using values that are only known at render time. The function receives the node's tag, its resolved props, and the render context. It returns a new tag, new props, or nothing to keep the node as it is. See Transforming nodes before render for the full rules.

import { renderHtml } from '@comark/html'

const user = { id: 42 }

const html = await renderHtml(`
::cta
Open your dashboard
::
`, {
  // Render every `::cta` as a link to the user's dashboard
  transformNode: ({ tag, props }) =>
    tag === 'cta' ? { tag: 'a', props: { ...props, href: `/users/${user.id}/dashboard` } } : undefined,
})

Comark renders what the function returns as-is, without sanitizing it, and the security plugin doesn't apply to it. Validate anything you build from untrusted data, such as URLs from data.

A returned tag goes through components like any other tag.

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
transformNodeNodeTransform | NodeTransform[]Change the tag or props of element nodes right before they render

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) })
}
© 2026 Vercel, Inc.