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:
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 elementnode— the original HTML elementoptions— 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.