What Markdown Is and Which Version You Are Actually Writing
Markdown is a lightweight markup language created by John Gruber in 2004, with input from Aaron Swartz. The design goal: a Markdown file should read cleanly as plain text before anything renders it. Compare the same fragment in HTML and Markdown:
Release Notes
This build fixes a critical parser bug.
- Faster startup
- Smaller bundle
## Release NotesThis build fixes a critical parser bug.
- Faster startup
- Smaller bundle
"Markdown" is not one language. Gruber's original description, frozen at version 1.0.1 in December 2004, left many cases undefined, so implementations diverged. Three flavours matter in practice:
- CommonMark — a strict specification with a machine-readable test suite of several hundred examples. The current release is 0.31.2, published in January 2024, and it is the base layer for most modern parsers.
- GitHub Flavored Markdown (GFM) — CommonMark plus five specified extensions: tables, task lists, strikethrough, autolinks, and a filter for disallowed raw HTML. Footnotes deserve a caveat: github.com renders
[^1], but footnotes are a site feature outside the published GFM specification, so a local CommonMark parser will not produce them. - Platform subsets — Slack, Discord, and Teams take a handful of inline markers and ignore the rest. Slack's
mrkdwnhas bold, italic, strikethrough, code, and blockquotes, but no heading syntax and no tables.
Decide which target renders a document before writing it: a table works on GitHub and fails on Slack. Paste a draft into a Markdown Editor to see what your syntax actually produces instead of guessing.
Block Syntax: Headings, Paragraphs, and Lists
Block elements are separated by blank lines. That rule causes more broken Markdown than anything else.
## Major Section
Subsection
#### Detail
A paragraph is any run of lines with no blank line between them.
This second line joins the same paragraph.
This is a new paragraph because a blank line came first.
Always put a space after the hash. CommonMark requires it, so #Heading renders as literal text, while Gruber's original Perl script accepted it — a portability trap worth knowing. Use one top-level heading per document and never skip levels; screen readers and search crawlers both read heading order as structure, so a jump from H2 straight to H4 is a real defect.
For lists, pick one bullet marker and keep it. Mixing - and * at the same level starts a brand new list in CommonMark:
- First item
- Second item
- Nested item, indented two spaces
- Another nested item
- Third item
Ordered items use numbers
The numbers you type do not matter
Renderers renumber sequentially - [ ] Unchecked task (GFM only)
- [x] Completed task
Nesting depends on indentation matching the parent's content column, not on a fixed tab width: after - that column is two, after 1. it is three. Get it wrong and the sub-list renders as a sibling. Because renderers renumber ordered lists anyway, many teams write 1. on every line, so reordering never touches the numbers.
Inline Syntax and the Emphasis Rules That Trip People Up
bold *italic* *bold italic*
~~strikethrough~~ (GFM)
inline code
Prefer bold and _italic_ over __bold__ and *italic*. CommonMark's emphasis rules refuse to open or close underscore emphasis inside a word, so snake_case_name survives, while * inside a word can open emphasis. That is why un*frigging*believable italicises but un_frigging_believable does not.
For code containing backticks, widen the fence. A code span opened by a run of *n* backticks closes at the next run of exactly *n*, and one leading and trailing space is stripped so the content can itself start with a backtick:
Use ` code to show a literal backtick pair.
Markdown has no line-break syntax that reads well. Two or more trailing spaces produce a but are invisible in review; a trailing backslash does the same in CommonMark and is at least visible. Prefer a real paragraph break to either.
Links, Images, and Reference Style
Inline link
!Alt text describing the image
Reference style keeps prose readable:
See the [specification][spec] and the [test suite][tests].
[spec]: https://spec.commonmark.org/
[tests]: https://spec.commonmark.org/0.31.2/spec.json
Reference definitions can sit anywhere outside a code fence and are not rendered. Labels are case-insensitive and whitespace-normalised, and [spec][] reuses the link text as the label.
Alt text is not optional. It is the accessible name of the image and the main textual signal a crawler has for it, so  wastes both. Describe the content, not the file. And remember that relative links resolve against the rendering location, which differs between a repository view and a published docs site: keep file links repository-relative and live-site links absolute.
Code Blocks, Tables, and Blockquotes
Always tag fenced blocks with a language. The tag drives syntax highlighting and, on GitHub, language statistics.
python
def slugify(title: str) -> str:
return "-".join(title.lower().split())
Indented code blocks (four spaces) still work, but inside a list those four spaces may instead mean "continue the item". Fences are the modern default.
GFM tables need a header row and a delimiter row with the same number of cells; if the counts differ, the block is not a table at all and renders as a paragraph. Colons in the delimiter set alignment:
Flavour Tables Footnotes Raw HTML
CommonMark No No Yes
GFM Yes Site-only Filtered
Slack No No No
Which renders as:
Flavour Tables Footnotes Raw HTML
CommonMark No No Yes
GFM Yes Site-only Filtered
Slack No No No
Source columns do not need to line up — only the pipe count matters. Pipes inside a cell must be escaped as
\|, and a table cell cannot contain a fenced code block, only inline code.Blockquotes nest, and can hold other blocks:
> The quoted claim.
>
> - A bullet inside the quote
>
> > A nested quote.
Converting Between Markdown and Other Formats
Markdown is a source format, not a delivery format, so conversion is routine. Markdown to HTML is your static site generator's job, at build time. The other two directions need a tool: use HTML to Markdown when migrating a CMS or capturing a page, rather than stripping tags by hand, since nested lists and tables are where naive regex conversions fall apart; use Markdown to PDF for anything that must be signed or archived, since it paginates the rendered document.
Round-tripping is lossy in one direction. Markdown to HTML is well defined; HTML to Markdown is not, because HTML expresses things Markdown cannot — colspans, inline styles, arbitrary attributes. Expect a converter to leave raw HTML in the output there, and treat that as correct rather than a bug.
Rendering Markdown Safely
Markdown is not a safe input format. CommonMark explicitly allows raw HTML blocks to pass straight through, so a comment field that accepts Markdown and renders the parser's output accepts HTML too:
Looks harmless.
Concrete rules for handling untrusted Markdown in 2026:
Never trust the parser to sanitise. marked deprecated its built-in sanitize option in 0.7.0 and removed it in v5, and documents that sanitising is the caller's job. markdown-it disables HTML by default (html: false) — leave it off for user content.
Sanitise the rendered HTML, not the Markdown source, using a maintained sanitiser such as DOMPurify with an allow-list. Filtering the source is bypassable because Markdown has many ways to reach the same HTML.
Sanitise link URLs too. click) is valid Markdown. Allow http, https, and mailto only.
Add rel="noopener noreferrer" to external links you render with target="_blank".Note how narrow the GFM extension is: its disallowed-raw-html filter escapes nine named tags —
script, iframe, style and textarea among them — and nothing else, so the onerror above survives untouched. READMEs are safe on github.com because the site runs a full HTML sanitiser with an attribute allow-list on top of the parser. That layer is the one you build yourself.A Practical Style Guide
These conventions cost nothing and prevent most diff noise and rendering surprises.
- One sentence per line, or hard-wrap at a fixed column — but never reflow a paragraph in the same commit as a content change; the diff becomes unreviewable.
- Blank line before and after every heading, list, table, and fenced block. Missing blank lines are the top cause of "why is my list rendering as a paragraph".
- Set up a linter.
markdownlint catches inconsistent bullets, missing language tags, skipped heading levels, and trailing whitespace, and runs in CI against the same rules as your editor plugin.
Keep front matter valid. Static site generators read a YAML block delimited by ---` at the very top of the file; one bad indent breaks the build. Validate it with a YAML to JSON converter before committing.