ProvenPin Install

Turndown HTML to Markdown with custom rules: working versions (tested 2026-10-15)

Convert HTML to clean Markdown with Turndown using ATX headings, fenced code blocks with languages, a strikethrough rule, a figure rule and removal of script and style tags.

passed3 passed · 0 failed2026-10-15 17:11 UTC
Status
passed
Last passed
2026-10-15 17:11 UTC
Tests
3 / 3
Language
TypeScript
Runtime
node v22.23.3
pinned versions4
turndown@7.2.4@types/node@26.6.5tsx@4.23.15@types/turndown@5.0.6
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 38775053802

The code

Exactly the files that passed. Your agent can pull them with get_recipe("turndown-html-to-markdown") (install), or see them on GitHub.

package.json
{
  "name": "turndown-html-to-markdown",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {
    "turndown": "7.2.4"
  },
  "devDependencies": {
    "@types/node": "26.6.5",
    "tsx": "4.23.15",
    "@types/turndown": "5.0.6"
  }
}
src/convert.ts
import TurndownService from "turndown";

export function createConverter() {
  const td = new TurndownService({
    headingStyle: "atx",
    codeBlockStyle: "fenced",
    bulletListMarker: "-",
    emDelimiter: "_",
  });

  td.remove(["script", "style"]);

  td.addRule("strikethrough", {
    filter: ["del", "s"],
    replacement: (content) => `~~${content}~~`,
  });

  // <figure><img><figcaption> becomes an image whose alt text is the caption.
  td.addRule("figure", {
    filter: "figure",
    replacement: (_content, node) => {
      const el = node as HTMLElement;
      const img = el.querySelector("img");
      const caption = el.querySelector("figcaption")?.textContent?.trim() ?? img?.getAttribute("alt") ?? "";
      return img ? `\n\n![${caption}](${img.getAttribute("src")})\n\n` : "";
    },
  });

  return td;
}

const converter = createConverter();

export function htmlToMarkdown(html: string): string {
  return converter.turndown(html);
}
test/convert.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { htmlToMarkdown } from "../src/convert.ts";

test("headings, emphasis, links and lists", () => {
  const md = htmlToMarkdown('<h1>Title</h1><h2>Sub</h2><p>Some <strong>bold</strong> and <em>soft</em> <a href="https://example.com">link</a>.</p><ul><li>one</li><li>two</li></ul>');
  assert.match(md, /^# Title$/m);
  assert.match(md, /^## Sub$/m);
  assert.match(md, /\*\*bold\*\*/);
  assert.match(md, /_soft_/);
  assert.match(md, /\[link\]\(https:\/\/example\.com\)/);
  assert.match(md, /^-\s+one$/m);
  assert.match(md, /^-\s+two$/m);
});

test("fenced code keeps the language from class names", () => {
  const md = htmlToMarkdown('<pre><code class="language-ts">const x = 1;\n</code></pre>');
  assert.equal(md, "```ts\nconst x = 1;\n```");
});

test("custom rules: strikethrough and figure; scripts removed", () => {
  const md = htmlToMarkdown('<p><del>old</del> new</p><script>evil()</script><figure><img src="/a.png" alt="alt"><figcaption>A chart</figcaption></figure>');
  assert.match(md, /~~old~~ new/);
  assert.match(md, /!\[A chart\]\(\/a\.png\)/);
  assert.doesNotMatch(md, /evil/);
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.