Markdown to HTML in JavaScript, No Framework
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/htmlnpm install @comark/htmlyarn add @comark/htmlbun add @comark/htmlrenderHtml()
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
`)<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 |
transformNode | NodeTransform | NodeTransform[] | undefined | Change the tag or props of element nodes right before they render |
autoClose | boolean | true | Close incomplete Markdown and components before parsing |
autoUnwrap | boolean | true | Remove a single paragraph wrapper inside components |
linkify | boolean | true | Deprecated. Use registerDefaultPlugins: false and register linkify() instead |
registerDefaultPlugins | boolean | true | Register default plugins (linkify, frontmatter, html, alert, task-list, components, attributes) |
unwrap | boolean | string | string[] | false | Remove 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>`
}
}
})<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 { 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>`
}
}
})<header><h1>My Blog</h1><p>Welcome!</p></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,
})<a href="/users/42/dashboard">Open your dashboard</a>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:
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:
import { renderHtmlFromDocument } from '@comark/html'
const document = await $fetch(`/api/content/${slug}`)
const html = await renderHtmlFromDocument(document)Options
| Option | Type | Description |
|---|---|---|
components | Record<string, fn> | Custom component renderers |
data | Record<string, any> | Data passed to component renderers |
transformNode | NodeTransform | 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
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
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
import { createHtmlRenderer } from '@comark/html'
const renderHtml = createHtmlRenderer()
async function handleRequest(markdownContent: string) {
return Response.json({ html: await renderHtml(markdownContent) })
}