Converting Markdown
Turning Markdown into something else: renderers such as markdown-it, Pandoc for HTML, Word and PDF, static site generators, and previews in your editor.
From text to output
Markdown is written for people, but browsers do not understand it. Before anyone sees formatted text, a program has to convert it. That happens in two steps, even when a single tool does both.
A Markdown parser reads the plain text and decides what each part is: this line is a heading, these three lines form a list, this word is emphasized. It applies the rules of one Markdown flavor, for example CommonMark. The result is not text any more but a structure in memory.
That structure is an
abstract syntax tree
(AST): a tree in which the document is the root, blocks such as
headings, paragraphs and lists are its branches, and the inline
pieces (plain text, emphasis, links, code) are the leaves.
"Abstract" means that the punctuation is gone: the tree records
"strong emphasis", not whether you typed ** or
__. A renderer then walks the tree and writes the
output format. The parsing step is the same idea as
parsing JSON:
text in, structure out.
Here is a short outage note and the HTML that markdown-it makes of it:
# Portal outage
Logins fail for **some** customers.
- Check the status page
- Post an update every 30 minutes
<h1>Portal outage</h1> <p>Logins fail for <strong>some</strong> customers.</p> <ul> <li>Check the status page</li> <li>Post an update every 30 minutes</li> </ul>
Between the two, the parser held this tree in memory:
document
├── heading (level 1)
│ └── text "Portal outage"
├── paragraph
│ ├── text "Logins fail for "
│ ├── strong
│ │ └── text "some"
│ └── text " customers."
└── bullet list (tight)
├── list item
│ └── paragraph → text "Check the status page"
└── list item
└── paragraph → text "Post an update every 30 minutes"
Tools that check or rewrite Markdown, such as linters and
formatters, work on this tree too. That is why they can tell a
# that starts a heading from a # inside a
code block.
Renderers in code: markdown-it and others
Most applications that show Markdown use a library: the website, ticket tool or chat app passes the text to the library and inserts the HTML it returns. The library decides which flavor you get, and its options decide what is allowed.
| Library | Language | Rules | Where you meet it |
|---|---|---|---|
| markdown-it | JavaScript | CommonMark, plus tables and strikethrough; plugins | VS Code's preview, many web apps, the examples on this site |
| cmark and commonmark.js | C, JavaScript | CommonMark (the reference implementations) | Built into other tools and language bindings |
| Python-Markdown | Python | Close to the original Markdown.pl, not CommonMark | MkDocs |
| kramdown | Ruby | Its own superset of Markdown | Jekyll and GitHub Pages |
| Goldmark | Go | CommonMark-based | Hugo |
markdown-it and its defaults
markdown-it is the library that renders every Markdown example on
this site, version 15.0.2 (September 2026). Its default preset
follows the CommonMark specification and adds two extensions from
GitHub Flavored Markdown: tables and strikethrough. Task lists,
footnotes and alerts need plugins. In a Node.js project you would
install it with npm install markdown-it and call it
like this:
const markdownit = require("markdown-it");
const md = markdownit(); // default options
const html = md.render("Power is **back** in *Deventer*.");
console.log(html);
The defaults are deliberately strict. Raw HTML in the source is
escaped, so it shows up as text instead of becoming part of the
page, and links with a javascript: address are refused.
That makes the default safe for text typed by users; see
HTML in Markdown and Safety
for why that matters.
## Maintenance tonight
Portal **offline** from 22:00 to 23:00 UTC.
Use <b>the phone line</b> for urgent calls.
| System | Impact |
| ------ | ------ |
| Portal | ~~Partial~~ Full |
<h2>Maintenance tonight</h2> <p>Portal <strong>offline</strong> from 22:00 to 23:00 UTC. Use <b>the phone line</b> for urgent calls.</p> <table> <thead> <tr> <th>System</th> <th>Impact</th> </tr> </thead> <tbody> <tr> <td>Portal</td> <td><s>Partial</s> Full</td> </tr> </tbody> </table>
Notice two things in the HTML. The <b> tags were
escaped to <b>, so the reader sees them
literally. And the line break after "UTC." is kept in the HTML but a
browser shows both lines joined: a single newline is a
soft line break.
Options change the output
Four options are worth knowing, because each one makes the same
Markdown produce different HTML. The examples below set one option
each (in markdown-it: markdownit({ breaks: true }) and
so on).
breaks turns every single newline into a
<br>, which is how GitHub comments and most chat
apps behave:
Portal **offline** from 22:00 to 23:00 UTC.
Use the phone line for urgent calls.
<p>Portal <strong>offline</strong> from 22:00 to 23:00 UTC.<br> Use the phone line for urgent calls.</p>
linkify turns bare web addresses into links. In
markdown-it 15 only addresses with a scheme such as
https:// are linked; a bare www. address
stays plain text unless you switch on "fuzzy" links. Older guides
that promise otherwise describe version 14.
Status: https://status.example.com or www.example.com
<p>Status: <a href="https://status.example.com">https://status.example.com</a> or www.example.com</p>
typographer replaces straight quotes with curly ones
and a few character sequences with symbols. Good for prose, bad for
commands that people copy:
Type "restart" -- it's safe (c) Kestrel Energy...
<p>Type “restart” – it’s safe © Kestrel Energy…</p>
The fourth, html, lets raw HTML through unchanged. Only
switch it on for Markdown you wrote yourself, or sanitize the result
afterward.
Pandoc: one document, many formats
Pandoc is a free command-line
converter between document formats, written by John MacFarlane, who
also wrote the CommonMark specification. It reads Markdown, HTML,
Word (.docx), LaTeX and many more, and writes most of
them back out. The latest release is 3.12 (as of October 2026). It
is the usual answer when someone asks for the runbook "as a Word
file" or a README as a standalone web page.
# Markdown to Word, for a colleague who does not use Git
pandoc portal-runbook.md -o portal-runbook.docx
# A complete HTML page (with <html>, <head> and a title) instead of a fragment
pandoc README.md --standalone --metadata title="Portal README" -o readme.html
# Read the input as GitHub Flavored Markdown instead of Pandoc's own dialect
pandoc --from gfm CHANGELOG.md -o changelog.docx
# The other way round: turn an old Word procedure into Markdown
pandoc old-procedure.docx -o old-procedure.md
Pandoc picks the formats from the file extensions, or from
--from and --to. By default it reads
Pandoc's Markdown, its own extended dialect, so if you write for GitHub, say
--from gfm to get the same rules.
Converting from Word to Markdown is useful when a team moves old procedures into a repository, but expect to tidy up: colored text, merged table cells and comments have no Markdown equivalent and are dropped or simplified.
Markdown to PDF
Markdown-to-PDF conversion is harder than it sounds, because Markdown says nothing about pages: no paper size, margins, page breaks, headers or footers. Something has to lay the text out on pages, and that is a separate program. There are two common routes:
-
Via LaTeX or Typst. Pandoc converts the Markdown
and hands it to a PDF engine:
pandoc report.md -o report.pdf. The default engine is LaTeX, which is a large separate install; Typst is a smaller alternative (--pdf-engine=typst). The result looks like a printed book or paper. - Via HTML. Convert to HTML first, then print it to PDF with a browser ("Print → Save as PDF") or an HTML-to-PDF tool. The layout then comes from CSS, which web people already know, including print styles.
Whichever route you take, treat the PDF as an export, not as the source. Keep the Markdown in version control, change it there, and create a new PDF when needed; a PDF that is edited by hand drifts away from the source within weeks.
Static site generators
A static site generator converts a whole folder of Markdown files into a website: it renders each file, wraps it in a page template with navigation and search, and writes plain HTML files that any web server can host (see static hosting). The settings at the top of each file, the front matter, tell the generator the page title, date or layout.
| Generator | Markdown renderer | Status (as of October 2026) |
|---|---|---|
| Jekyll | kramdown by default | Latest 4.4.1; GitHub Pages still runs Jekyll 3.10 with kramdown 2.4 |
| Hugo | Goldmark (CommonMark, compatible with GitHub Flavored Markdown) | Frequent releases, v0.167.0 in September 2026 |
| MkDocs | Python-Markdown (not CommonMark) | Last stable 1.6.1 (2024); 2.0 development builds with breaking changes appeared in 2026 |
| Material for MkDocs | (theme and plugins on top of MkDocs) | Maintenance mode since November 2025: fixes only; the same team builds a successor, Zensical, still at version 0.0.x |
| Docusaurus | MDX: Markdown plus JSX components | Version 3.10 |
| Astro | GitHub Flavored Markdown, MDX via an integration | Version 7 |
The renderer column matters more than it looks. A page that renders fine in VS Code (CommonMark) can come out differently in MkDocs, because Python-Markdown follows older rules; for example, it expects nested list items to be indented by four spaces. Test with the generator you actually publish with, and keep its version pinned.
MkDocs takes its settings from a mkdocs.yml file next
to a docs/ folder; mkdocs serve shows the
site locally and mkdocs build writes the HTML to
site/:
site_name: Kestrel Portal documentation
nav:
- Home: index.md
- Runbooks:
- Portal returns 503: runbooks/portal-503.md
- Certificate renewal: runbooks/certificate-renewal.md
- Changelog: changelog.md
Live preview in your editor
A live preview is a pane in an editor that shows the rendered Markdown and updates as you type. In Visual Studio Code, Ctrl+Shift+V opens the preview in place of the editor, and Ctrl+K followed by V opens it side by side. Most other editors, and note apps such as Obsidian, have something similar.
A preview catches the obvious mistakes: a heading without a space
after the #, a table without its delimiter row, a list
that swallowed the next paragraph. But it shows what
the editor's renderer makes of the file. VS Code's preview
uses markdown-it and targets CommonMark; its documentation says
plainly that it does not support GitHub Flavored Markdown. It does
add some extras of its own, such as math (with KaTeX) and Mermaid
diagrams.
Task lists are a good example. GitHub renders them as checkboxes, but a plain CommonMark renderer such as markdown-it's default shows the brackets as text:
- [x] Change approved
- [ ] Rollback tested
<ul> <li>[x] Change approved</li> <li>[ ] Rollback tested</li> </ul>
For a file that will live on GitHub, the most reliable preview is GitHub itself: the "Preview" tab when you edit a file or write a comment, or the rendered file in a pull request.
Choosing a route
| You need | Use |
|---|---|
| To check how a file looks while writing | The editor's live preview, then the target's preview |
| A README or docs that live in a repository | Nothing: GitHub and GitLab render them for you |
| A Word file or PDF for someone outside the team | Pandoc (with a reference document or a PDF engine) |
| A documentation website with navigation and search | A static site generator, built in CI |
| Markdown typed by users inside your own application | A library such as markdown-it with safe defaults (raw HTML off), or a sanitizer on the output |