Convert Markdown to HTML for Emails and RSS Feeds
Use renderHtml() from @comark/html to turn Markdown into an HTML string, with no framework. Map component tags to functions that return HTML, for example a button with inline styles for an email.
Emails and RSS readers receive static HTML. They don't run your Vue, React, or Svelte components. @comark/html renders each component with a function that you write, so you control the exact markup.
Render Markdown to HTML
Install the package:
npm install @comark/htmlrenderHtml() parses and renders in one call:
import { renderHtml } from '@comark/html'
const html = await renderHtml('# October update\n\nWe shipped **dark mode**.')
// <h1 id="october-update">October update</h1>
// <p>We shipped <strong>dark mode</strong>.</p>When you render many messages or feed items, create the renderer once with createHtmlRenderer(). The returned function reuses the same parser and plugins.
Add an email button component
Write the component as an async function. It receives the node as [tag, attrs, ...children] and a render() helper for the children. Escape attribute values with escapeHtml():
import { createHtmlRenderer } from '@comark/html'
import { escapeHtml } from '@comark/html/utils'
export const renderEmail = createHtmlRenderer({
components: {
button: async ([, attrs, ...children], { render }) =>
`<a href="${escapeHtml(String(attrs.href))}" style="display:inline-block;padding:12px 20px;background:#0f172a;color:#ffffff;border-radius:6px;text-decoration:none">${await render(children)}</a>`,
},
})The email content uses the component like any other Comark component:
Thanks for signing up.
:button[Confirm your email]{href="https://example.com/confirm"}@comark/html returns only the content HTML. Put it inside your email layout. Email clients differ in their CSS support. We recommend that you inline the styles of your layout and of the Markdown elements with a separate CSS inliner tool.
Pass per-recipient data
Pass values in the data option. Every component function receives them in its context:
import { renderHtml } from '@comark/html'
import { escapeHtml } from '@comark/html/utils'
const html = await renderHtml('::greeting\nYour order has shipped.\n::', {
data: { name: 'Ada' },
components: {
greeting: async ([, , ...children], { render, data }) =>
`<p>Hi ${escapeHtml(String(data?.name))},</p>${await render(children)}`,
},
})Build an RSS feed
Put the title and the HTML of each item in CDATA sections, and escape the link URL, so the feed stays valid XML. A CDATA section can't contain ]]>, so split that sequence:
import { createHtmlRenderer } from '@comark/html'
const renderHtml = createHtmlRenderer()
// Keep `]]>` in the value from closing the CDATA section early.
const cdata = (value: string) => `<![CDATA[${value.replaceAll(']]>', ']]]]><![CDATA[>')}]]>`
const escapeXml = (value: string) =>
value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')
export async function renderItem(post: { title: string, url: string, markdown: string }) {
const html = await renderHtml(post.markdown)
return `<item>
<title>${cdata(post.title)}</title>
<link>${escapeXml(post.url)}</link>
<description>${cdata(html)}</description>
</item>`
}Make links absolute
Emails and feed readers can't resolve relative URLs such as /docs. Override the native a tag and resolve each href against your site:
import { createHtmlRenderer } from '@comark/html'
import { escapeHtml } from '@comark/html/utils'
export const renderEmail = createHtmlRenderer({
components: {
a: async ([, attrs, ...children], { render }) =>
`<a href="${escapeHtml(new URL(String(attrs.href), 'https://example.com').href)}">${await render(children)}</a>`,
},
})Use the same approach for img and its src attribute. If the Markdown comes from users, also add the security plugin with a blockedTags list.
FAQ
@comark/html for email. Register a component for each target.renderHtmlFromDocument() renders a stored MarkdownDocument without a parse step.Next steps
CLIs and agents
Print Markdown as styled ANSI output in Node.js CLIs and coding agents, with highlighted code, tables, and alerts, using @comark/ansi.
Markdown for AI chat
Compare Streamdown, Markstream, Comark, @humanspeak/svelte-markdown, markdown-to-jsx, and plain parsers for rendering streamed LLM output. See how each handles incomplete Markdown, components, security, and frameworks.