Blog

How to convert HTML to Markdown

How each HTML element maps to Markdown, where the conversion loses information, and how to automate it with Turndown or markdownify.

February 10, 2026

You’re moving the company blog off WordPress, and the export is 400 posts of HTML soup. Rewriting each one by hand would take the rest of the quarter. Converting them to Markdown is mostly mechanical, as long as you know which parts survive the trip and which don’t.

What Markdown is

Markdown is a lightweight markup language John Gruber created in 2004. It uses plain punctuation for formatting. A line starting with # is a heading, text wrapped in ** is bold, and a link looks like [text](url).

The raw file stays readable, even to someone who has never seen the syntax. That’s why GitHub, GitLab, Stack Overflow, Reddit, Obsidian and most documentation tools use it.

How HTML elements map to Markdown

Headings

<h1> to <h6> become one to six # characters.

HTML
<h1>Main title</h1>
<h2>Section title</h2>
<h3>Subsection</h3>
Markdown
# Main title
## Section title
### Subsection

Paragraphs and line breaks

Paragraphs become text separated by a blank line. Line breaks are sneakier. A <br> inside a paragraph needs two trailing spaces or a backslash at the end of the line. A plain newline isn’t enough, because most Markdown renderers quietly join the two lines back into one.

HTML
<p>First paragraph.</p>
<p>Second paragraph.</p>
<p>Line one<br>Line two</p>
Markdown
First paragraph.

Second paragraph.

Line one\
Line two

Inline formatting

HTMLMarkdown
<strong> or <b>**bold**
<em> or <i>*italic*
<del> or <s>~~strikethrough~~ (GitHub Flavored Markdown)
<code>`inline code`

Links and images

Links and images share a syntax. Images start with an exclamation mark, and the alt text goes in the brackets. An optional title goes in quotes after the URL.

HTML
<a href="https://example.com/lisbon" title="Lisbon guide">Three days in Lisbon</a>
<img src="tram-28.jpg" alt="Yellow tram on a steep street">
Markdown
[Three days in Lisbon](https://example.com/lisbon "Lisbon guide")
![Yellow tram on a steep street](tram-28.jpg)

Lists

Unordered lists use - (or *), ordered lists use 1., 2. and so on. Nested lists are indented under their parent item.

HTML
<ul>
  <li>Coffee</li>
  <li>Snacks
    <ul>
      <li>Pastel de nata</li>
    </ul>
  </li>
</ul>
<ol>
  <li>Grind the beans</li>
  <li>Pretend you'll clean the grinder later</li>
</ol>
Markdown
- Coffee
- Snacks
  - Pastel de nata

1. Grind the beans
2. Pretend you'll clean the grinder later

Code blocks

A <pre><code> block becomes a fenced code block. If the HTML carries a language class such as language-javascript, you can put the language after the opening fence to keep syntax highlighting.

HTML
<pre><code class="language-javascript">function greet(name) {
  return "Hello, " + name;
}</code></pre>
Markdown
```javascript
function greet(name) {
  return "Hello, " + name;
}
```

Blockquotes

Each line of a quote starts with >. A line with only > separates paragraphs inside the quote.

HTML
<blockquote>
  <p>The best way to predict the future is to invent it.</p>
  <p>Alan Kay</p>
</blockquote>
Markdown
> The best way to predict the future is to invent it.
>
> Alan Kay

Converting HTML tables

Tables are where things get awkward. Markdown tables come from GitHub Flavored Markdown (GFM), a popular extension of the original syntax. They allow one header row, no merged cells (colspan or rowspan), and only inline content in each cell. Simple tables convert cleanly. Fancy ones don’t.

HTML
<table>
  <thead>
    <tr><th>Feature</th><th>Markdown</th></tr>
  </thead>
  <tbody>
    <tr><td>Headings</td><td># to ######</td></tr>
    <tr><td>Bold</td><td>**text**</td></tr>
    <tr><td>Links</td><td>[text](url)</td></tr>
  </tbody>
</table>
Markdown
| Feature | Markdown |
| --- | --- |
| Headings | # to ###### |
| Bold | **text** |
| Links | [text](url) |

Colons in the separator row set column alignment: :--- for left, :---: for centre and ---: for right. If a table uses merged cells, keep it as raw HTML in the Markdown file instead.

Using the online converter

  1. Open the HTML to Markdown converter and paste your HTML, either a fragment or a full page.
  2. Select Convert, then Copy the Markdown output.

The conversion runs in your browser, so nothing you paste is uploaded. Here’s what to expect from the output:

  • It converts headings, paragraphs, bold, italic, strikethrough, inline code, links, images, nested lists, blockquotes, code blocks, horizontal rules and tables.
  • Tables use the first row as the header when there is no <thead>, and short rows are padded with empty cells. Alignment is not set.
  • Link and image title attributes are dropped, and fenced code blocks have no language tag. Add those by hand if you need them.
  • <br> becomes a plain newline. Add a trailing backslash if you need a hard line break.
  • <script>, <style> and <head> are removed. Containers such as <div> and <section> are unwrapped and their content kept.

Common use cases

CMS migration

Moving from WordPress, Drupal or Joomla to a Markdown-based site generator such as Hugo, Astro or Next.js with MDX means converting every existing article. A converter does most of the work. You fix the odd edge case by hand, usually the one post someone built entirely out of nested tables in 2011.

Documentation

APIs and older docs often hold HTML descriptions. Converting them lets you reuse the text in README files or in Markdown doc sites like Docusaurus and VitePress.

Notes

Content copied from emails or web pages into Obsidian, Logseq or Bear keeps its structure without the pasted markup if you convert it first.

Edge cases

Real-world HTML is messy. These are the usual troublemakers:

  • Classes and inline styles have no Markdown equivalent and are dropped. If styling matters, keep that element as raw HTML, which most Markdown renderers allow.
  • Divs and spans are unwrapped: their content stays, the wrapper goes.
  • Iframes and embeds such as videos and maps cannot be expressed in Markdown. Keep them as raw HTML or replace them with a link.
  • Nested blockquotes use repeated > characters, which get hard to read past two levels.
  • HTML entities such as &amp;, &lt; and &nbsp; should be decoded to their characters. An HTML parser does this for you.

Converting in code

For 400 posts, you’ll want a script. In JavaScript, Turndown is the usual choice. Its defaults produce underlined headings and * bullets, so set the options you want. Add turndown-plugin-gfm to get tables and strikethrough.

convert.mjs
import fs from "node:fs";
import TurndownService from "turndown";
import { gfm } from "turndown-plugin-gfm";

const turndown = new TurndownService({
  headingStyle: "atx", // # headings instead of underlined ones
  codeBlockStyle: "fenced", // ``` blocks instead of indented code
  bulletListMarker: "-",
});
turndown.use(gfm);

const html = fs.readFileSync("wp-export/post-42.html", "utf-8");
fs.writeFileSync("content/posts/post-42.md", turndown.turndown(html));

In Python, markdownify does the same job.

Python
from markdownify import markdownify as md

html = '<h1>Welcome</h1><p>This is a <a href="https://example.com">link</a> with <strong>bold</strong> text.</p>'

print(md(html, heading_style="ATX").strip())
# # Welcome
#
# This is a [link](https://example.com) with **bold** text.

Need data from a webpage as JSON?

PulpMiner turns any public page into structured JSON and can save it as a live API. See it work on a demo page, no sign-up.

Try it on a demo page