Published
Markdown feels like a format and behaves like a family of dialects. The same file can render three different ways in a repository, a documentation site and a chat client, and the constructs that diverge are exactly the ones people use most: line breaks, nested lists, tables and raw HTML. This guide explains where the differences come from, which ones will actually bite you, and how to write a document that arrives intact.
There is no single Markdown
Markdown was published in 2004 as a Perl script and a page of prose. There was no grammar, no test suite, and no statement of what should happen for the ambiguous cases — and Markdown has a great many ambiguous cases. Every implementation that followed resolved them independently, which is why a document could render one way in one tool and differently in another with neither being wrong.
CommonMark, first published a decade later, is the attempt to fix that: an actual specification with several hundred worked examples and a conformance suite, versioned so implementations can state which revision they follow. It deliberately did not add features. Its contribution was to decide the edge cases, and most modern renderers now start from it.
GitHub Flavored Markdown is defined as a strict superset of CommonMark with a small set of extensions. Nearly everything else you meet — the dialects in documentation generators, static site builders, wikis, chat clients and note-taking apps — is CommonMark or GFM plus a further layer of local extensions and options. Knowing which of those three layers a construct belongs to tells you immediately how portable it is.
What CommonMark pinned down
The rules CommonMark settled are unglamorous and are the source of most surprises when a document moves between an old renderer and a modern one. List item indentation is the biggest: the content of a nested item must be indented to the column where its parent's content begins, which is two spaces under a hyphen-and-space marker and three under a 1-dot-space marker. Older implementations accepted four spaces universally, so a document indented for those can produce an unintended code block.
Emphasis was the other long-running ambiguity. CommonMark distinguishes asterisks from underscores specifically so that intraword underscores stay literal: snake_case_identifier renders as written, while snake*case*identifier emphasises the middle. This one rule removes an entire category of accidental italics in technical writing, and it is the reason a document written for a pre-CommonMark renderer can suddenly look different.
It also fixed the boundaries of things: when a fenced code block ends, when an HTML block starts and stops, whether a list is tight or loose and therefore whether its items get wrapped in paragraph tags, and when a list is allowed to interrupt a paragraph without a blank line. None of these are choices you make while writing; they are choices the renderer used to make for you, differently.
What GFM adds
The GFM specification adds five things to CommonMark: pipe tables, task list items, strikethrough, extended autolinks that turn a bare URL or a www-prefixed hostname into a link, and a filter that disallows a list of raw HTML tags. That is the whole extension set, and it is implemented by many renderers outside GitHub, which makes these constructs reasonably portable.
What is not in that specification is worth knowing separately, because it is the source of documents that only look right on GitHub. Footnotes, mathematical expressions delimited with dollar signs, emoji shortcodes, alert blockquotes, and automatic heading anchors are GitHub product features layered on top of GFM rather than parts of the published spec. They render on github.com and frequently render nowhere else.
Tables deserve a specific warning because they are the most-used extension and the most fragile. A pipe table cell holds inline content only: no lists, no fenced code, no paragraphs. A literal pipe inside a cell must be escaped with a backslash, including inside inline code, which is the one place the usual "code spans are literal" intuition does not hold. And a line break inside a cell requires an HTML break element, because a newline ends the row.
Portability drops with each row down the table. The first group is safe everywhere a spec-compliant renderer is used.
| Construct | Layer | Portability |
|---|---|---|
| Headings, lists, links, fenced code, blockquotes | CommonMark | Everywhere |
| Pipe tables | GFM extension | Very wide, but cells are inline-only |
| Strikethrough with double tildes | GFM extension | Wide |
| Task list items | GFM extension | Wide; interactive only on GitHub |
| Bare URL autolinking | GFM extension | Wide |
| Footnotes | GitHub feature | Inconsistent |
| Dollar-delimited math | GitHub feature | Inconsistent |
| Emoji shortcodes | GitHub feature | Inconsistent |
| Heading anchor slugs | Renderer-specific | Differs per renderer |
The constructs that actually differ
Line breaks are the single most common portability failure. CommonMark gives you two ways to force a break inside a paragraph: two or more trailing spaces, or a trailing backslash. Trailing spaces are invisible in an editor, stripped by many save-on-format configurations, and impossible to see in a code review — the backslash form exists precisely because of that. Separately, many renderers offer a "soft break equals line break" option that turns every newline into a break; GitHub enables it in issue and pull request comments but not in files rendered from a repository, which is why a comment that looked right becomes one long paragraph when it is pasted into a README.
Nested list indentation is the second. Two spaces is correct under a hyphen marker and three under an ordered marker, but plenty of documents use four consistently and many editors auto-indent that way. Under CommonMark four spaces after a hyphen marker is still inside the list item, so it usually works — the failure appears with deeper nesting and with ordered lists, where the accumulated indentation crosses into code block territory.
The rest are smaller but worth recognising: heading anchor generation differs by renderer, so a link to a heading may only work in the place it was written; some renderers apply typographic substitution and turn straight quotes into curly ones and three dots into an ellipsis, which matters when the text is code-adjacent; and the handling of a list that follows a paragraph without a blank line varies enough that inserting the blank line is simply the right habit.
Hard line break, three ways:
line one·· two trailing spaces, invisible and fragile
line one\ trailing backslash, CommonMark, visible in review
line one<br> HTML, works almost everywhere
Intraword emphasis, CommonMark rules:
snake_case_name stays literal
snake*case*name the middle word is emphasised
Nested lists, aligned to the parent's content column:
- parent
- child two spaces under "- "
1. parent
1. child three spaces under "1. "
Table cells are inline-only:
| column | note |
| ------ | ----------------- |
| a \| b | escaped pipe |
| one<br>two | break needs HTML |Raw HTML and sanitisation
CommonMark permits raw HTML blocks and inline tags, and passes them through to the output untouched. That is a language decision, not a security decision, and it means a Markdown renderer on its own is a mechanism for producing arbitrary HTML from untrusted text. Anywhere user-supplied Markdown is rendered, the output must be sanitised after conversion, with a parser-based sanitiser and an allowlist of elements and attributes. Relying on the Markdown parser to be safe is a misunderstanding of what it is for.
GFM's disallowed-raw-HTML extension filters a specific set of tags — script, style, iframe, title, textarea, xmp, plaintext, noembed and noframes — by escaping them rather than emitting them. This is a useful hardening measure and is not a sanitiser: it does nothing about event handler attributes, about a javascript: URL in a link, or about the many other ways markup can carry behaviour. GitHub itself runs a separate allowlist-based sanitiser on top.
For your own documents, the practical consequence is that raw HTML is the least portable thing you can write. It survives on GitHub for a modest set of tags, is stripped entirely by some documentation pipelines, and breaks outright in renderers configured to escape HTML. Use it when there is no alternative — a break inside a table cell, a collapsible details block, an image with an explicit width — and prefer a Markdown construct in every other case.
Writing Markdown that survives its destination
The reliable strategy is to decide where a document will be read and then write to the smallest common feature set that covers those places. A README rendered on a code host, a docs page built by a static site generator, and a snippet pasted into a chat client are three different targets; a document that has to work in all three should use CommonMark plus tables and nothing else.
A few habits carry most of the weight. Always leave a blank line before a list, a heading, a table and a fenced code block, because the rules for interrupting a paragraph are exactly where renderers disagree. Always put a language on a fenced code block, both for highlighting and because some pipelines treat an unlabelled fence differently. Use reference-style links for long URLs to keep paragraphs readable in source form. Prefer a backslash or an explicit break element over trailing spaces. And do not rely on a heading anchor unless you control the renderer that generates it.
One habit is worth adopting for reasons beyond rendering: write one sentence per line. It makes no difference to the output, since a single newline is a soft break, and it transforms the diff. A reworded sentence becomes a one-line change instead of a reflowed paragraph, which makes documentation reviewable in the same way code is.
- Blank line before every list, heading, table and code fence.
- A language identifier on every fenced code block.
- Reference-style links for anything long enough to wrap.
- A backslash or a break element instead of trailing spaces.
- One sentence per line, so the diff shows the edit and not the rewrap.
A preview workflow that is worth the time
Previewing is not the same as checking. A preview shows you one renderer's interpretation, and the whole problem is that renderers differ. The workflow that catches real problems is to preview in a spec-compliant renderer first, which tells you whether the document is valid CommonMark and where your constructs actually land, and then to check the specific constructs you are unsure about in the destination itself.
Look at the structural things rather than the prose. Did every list nest at the depth you intended, or did one level flatten? Did a table render as a table, or as a paragraph full of pipe characters? Did a code fence close where you expected, or did it swallow the section below it? Did an underscore in an identifier turn into italics? These are the failures that a quick glance misses and a reader notices immediately.
Keep the loop short. Paste the document into a preview, fix what is visibly wrong, and only then commit it to the place it belongs. When a document must render correctly in more than one destination, check it in each one at least once — the second check almost always finds something, and it is almost always a table, a nested list, or a line break.
What to remember
- Place every construct you use in one of three layers — CommonMark, the GFM extensions, or a renderer-specific feature — because that tells you how far it will travel.
- Write hard line breaks as a trailing backslash or an explicit break element rather than trailing spaces, which are invisible and routinely stripped on save.
- Indent nested list content to the parent's content column, two spaces under a hyphen and three under an ordered marker, and leave a blank line before every list, table, heading and code fence.
- Treat rendered Markdown as untrusted HTML: the parser is not a sanitiser, and user-supplied documents need an allowlist sanitiser applied after conversion.
- Preview in a spec-compliant renderer to confirm the structure, then check tables, nested lists and line breaks again in the destination that will actually publish the document.