---
title: "Render Comark to HTML"
description: "Parse Markdown or render existing documents as HTML strings without any framework dependency."
canonical_url: "https://comark.dev/rendering/html"
---
# 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

<code-group>
```bash [pnpm]
pnpm add @comark/html
```


```bash [npm]
npm install @comark/html
```


```bash [yarn]
yarn add @comark/html
```


```bash [bun]
bun add @comark/html
```
</code-group>

## `renderHtml()`

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

### Usage

<code-group>
~~~typescript [Code]
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
`)
~~~


```html [Output]
<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>
```
</code-group>

### Options

| Option                                     | Type                            | Default     | Description                                                                                        |
| ------------------------------------------ | ------------------------------- | ----------- | -------------------------------------------------------------------------------------------------- |
| [`plugins`](#render-options-plugins)       | `ComarkPlugin[]`                | `[]`        | Array of plugins                                                                                   |
| [`components`](#render-options-components) | `Record<string, fn>`            | `{}`        | Custom component renderers                                                                         |
| [`data`](#render-options-data)             | `Record<string, any>`           | `undefined` | Data passed to component renderers                                                                 |
| `autoClose`                                | `boolean`                       | `true`      | Close incomplete Markdown and components before parsing                                            |
| `autoUnwrap`                               | `boolean`                       | `true`      | Remove a single paragraph wrapper inside components                                                |
| `linkify`                                  | `boolean`                       | `true`      | Convert URL-like text into links                                                                   |
| `registerDefaultPlugins`                   | `boolean`                       | `true`      | Register default plugins (`frontmatter`, `html`, `alert`, `task-list`, `components`, `attributes`) |
| `unwrap`                                   | `boolean \| string \| string[]` | `false`     | Remove selected wrapper tags from the parsed document                                              |

`renderHtml()` accepts all [`ParserOptions`](https://comark.dev/reference/parse#options) in addition to the renderer options above.

#### `plugins`

See [ComarkPlugin](https://comark.dev/plugins) for available plugins.

```typescript
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:

<code-group>
~~~typescript [Code]
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>`
    }
  }
})
~~~


```html [Output]
<div class="alert alert-warning" role="alert"><p>This is a warning message!</p></div>
```
</code-group>

#### `data`

Pass external data to every component renderer via the context object:

<code-group>
~~~typescript
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>`
    }
  }
})
~~~


```html [Output]
<header><h1>My Blog</h1><p>Welcome!</p></header>
```
</code-group>

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

```typescript
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()`](#renderhtml-options).

---

## `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:

```typescript [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`:

```typescript [render.ts]
import { renderHtmlFromDocument } from '@comark/html'

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

### Options

| Option                                     | Type                  | Description                        |
| ------------------------------------------ | --------------------- | ---------------------------------- |
| [`components`](#render-options-components) | `Record<string, fn>`  | Custom component renderers         |
| [`data`](#render-options-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:

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

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

~~~typescript [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

~~~typescript [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

```typescript [server.ts]
import { createHtmlRenderer } from '@comark/html'

const renderHtml = createHtmlRenderer()

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

---

- [Plugins](https://comark.dev/plugins)
- [Streaming API](https://comark.dev/reference/auto-close)


## Sitemap

See the full [sitemap](https://comark.dev/sitemap.md) for all pages.
