Skip to main content

Custom Turndown Rules

next-markdown-mirror uses Turndown to convert HTML to Markdown. You can add custom rules to handle your specific HTML patterns.

How Turndown rules work​

A Turndown rule has two parts:

  • filter — determines which HTML elements the rule applies to (tag name, class, attribute, or a function)
  • replacement — a function that returns the Markdown string for matched elements

Rules are evaluated in order. The first matching rule wins.

Adding rules via config​

Pass custom rules through the turndownRules option in your route handler config:

app/md-mirror/[...path]/route.ts
import { createMarkdownHandler } from 'next-markdown-mirror/nextjs';

export const GET = createMarkdownHandler({
baseUrl: process.env.NEXT_PUBLIC_SITE_URL!,
turndownRules: {
callout: {
filter: (node) => {
return (
node.nodeName === 'DIV' &&
node.classList.contains('callout')
);
},
replacement: (content, node) => {
const type = node.getAttribute('data-type') || 'note';
return `> **${type.charAt(0).toUpperCase() + type.slice(1)}:** ${content.trim()}\n\n`;
},
},
},
});

This converts <div class="callout" data-type="warning">Be careful</div> into:

> **Warning:** Be careful

Rule structure​

Each rule is an object with filter and replacement:

{
filter: string | string[] | ((node: HTMLElement, options: Options) => boolean),
replacement: (content: string, node: HTMLElement, options: Options) => string,
}

Filter types​

String — match by tag name:

filter: 'aside'

String array — match multiple tag names:

filter: ['aside', 'section']

Function — match by any condition:

filter: (node) => node.classList.contains('highlight')

Replacement function​

The replacement function receives:

  • content — the already-converted inner content of the element
  • node — the original HTML element
  • options — Turndown options

It must return a string (the Markdown output).

Example: custom component​

Convert a custom alert component to a Markdown blockquote:

turndownRules: {
alert: {
filter: (node) => node.getAttribute('role') === 'alert',
replacement: (content) => {
return `> ${content.trim()}\n\n`;
},
},
}

Example: preserving HTML elements​

Sometimes you want to keep certain elements as raw HTML in the Markdown output:

turndownRules: {
preserveVideo: {
filter: 'video',
replacement: (_content, node) => {
return `\n\n${node.outerHTML}\n\n`;
},
},
}

Example: custom code blocks​

Convert syntax-highlighted <pre> blocks with a language class:

turndownRules: {
codeBlock: {
filter: (node) => {
return (
node.nodeName === 'PRE' &&
node.firstChild?.nodeName === 'CODE'
);
},
replacement: (_content, node) => {
const code = node.querySelector('code');
const lang = code?.className.match(/language-(\w+)/)?.[1] || '';
const text = code?.textContent || '';
return `\n\n\`\`\`${lang}\n${text}\n\`\`\`\n\n`;
},
},
}

Using registerCustomRules directly​

If you're using the HtmlToMarkdown class directly (see Standalone Usage), you can register rules on any TurndownService instance:

import TurndownService from 'turndown';
import { registerCustomRules } from 'next-markdown-mirror';

const service = new TurndownService();

registerCustomRules(service, {
callout: {
filter: (node) => node.classList.contains('callout'),
replacement: (content) => `> ${content.trim()}\n\n`,
},
});

This is useful when you need full control over the Turndown instance rather than using the config-based approach.