Developer
Markdown that renders the same everywhere: CommonMark, GitHub tables and the traps between them
Markdown was designed to be readable before it is rendered, and that promise mostly holds. The trouble starts when one file travels: a README written for GitHub is pasted into a static site generator, a chat app, a documentation portal and a PDF converter, and each shows it slightly differently. This article separates the syntax every modern renderer agrees on from the extensions only some support, explains the rules that cause most surprises, and shows real output from the table and plain-text tools on this site.
By the MaffeiTools team · Updated · 7 min read
One name, several dialects
John Gruber published the original Markdown description and a Perl script in 2004. The description left many cases open, such as how much indentation a nested list needs or whether a blank line is required before a heading, so every later implementation answered those questions its own way. For about a decade, the same text could produce different HTML depending on which library read it.
CommonMark, first released in 2014, fixed that by writing an exact specification with several hundred numbered examples, each pairing a snippet of input with the HTML it must produce. Most widely used parsers today either implement CommonMark or document where they differ from it. When people say a document is portable Markdown, CommonMark is the practical meaning.
GitHub Flavored Markdown, usually shortened to GFM, is a strict superset of CommonMark. It keeps every CommonMark rule and adds a short list of extensions: tables, task list items, strikethrough, extended autolinks and a filter that removes a few dangerous raw HTML tags. Those extensions are popular enough that many people assume they are core Markdown. They are not, and a renderer that follows CommonMark alone will show them as plain text.
Worth knowing. When a document has to render in a place you do not control, write CommonMark first and treat each GFM extension as a feature that may degrade to plain text.
The core that every modern renderer agrees on
A small set of constructs behaves identically in CommonMark, GFM and nearly every popular library. Headings are written as one to six hash signs followed by a space. Paragraphs are separated by a blank line. Emphasis uses a single asterisk or underscore on each side, strong emphasis uses two. Inline code sits between backtick characters, and fenced code blocks sit between lines of three backticks or three tildes.
Lists are the area where the specification is most detailed. A list item starts with a hyphen, plus sign or asterisk for bullets, or a number followed by a period or closing parenthesis for ordered items. Content that belongs to an item must be indented to line up with the first character after the marker, which is why a nested list under a numbered item often needs three spaces rather than two.
Links and images share one shape: square brackets around the text, then the destination in parentheses. An image adds an exclamation mark in front. Reference style links, where the destination is defined once further down the page, are also part of the core and are useful when the same URL appears several times.
- Headings: one to six hash signs, then a space, then the text.
- Blank line between paragraphs; a single line break inside a paragraph is usually rendered as a space.
- Fenced code blocks with an info string, such as the language name, right after the opening fence.
- Block quotes: a greater-than sign at the start of each quoted line.
- A horizontal rule: three or more hyphens, asterisks or underscores alone on a line.
## Setup Run **bun install**, then read [the docs](https://example.com). > Note: `NODE_ENV` must be set. 1. Clone the repository 2. Install dependencies - Bun 1.3 or later - Node 22 for the build
Tables: the most used extension, and the easiest to break
Pipe tables come from GFM, not from CommonMark. A table needs a header row, a delimiter row made of hyphens, and body rows, all separated by vertical bars. A colon on the left of a delimiter cell aligns that column left, a colon on the right aligns it right, and colons on both sides center it. The delimiter row is what turns the text into a table; without it the lines stay a paragraph.
The pipe character itself is the usual failure. A cell that contains a vertical bar ends early unless the bar is escaped with a backslash. Values such as record types written as A|AAAA, shell pipelines, or regular expressions with alternation all need that escape. A second failure is block content: a table cell holds inline content only, so a list or a code block cannot be placed inside one.
The example below was produced by the Markdown table generator on this site from three comma-separated lines. It shows the three alignments and the escaped pipe in the last column. The padding is cosmetic; renderers ignore it, but it makes the raw text readable in a diff.
| Tool | Runs | Notes | | ----------- | ---: | :-------: | | DNS lookup | 42 | A\|AAAA | | JWT decoder | 7 | base64url |
Worth knowing. A renderer without the table extension shows every row as a single paragraph of text with pipes in it. That is the most common reason a README table looks broken in a documentation tool or a chat preview.
Line breaks, blank lines and why paragraphs merge
A single newline inside a paragraph is a soft line break. CommonMark tells the renderer to output it as a newline in the HTML, which a browser then displays as a space. Writers who press Enter once and expect a new line are surprised to see two lines joined. A hard break needs either two trailing spaces at the end of the line or a backslash just before the newline, and trailing spaces are invisible and easily removed by editors, so the backslash form is the more reliable choice.
Some renderers, including GitHub comments and several chat applications, deliberately treat every newline as a hard break because that matches what people expect when typing a message. The same text therefore produces one paragraph in a strict CommonMark site and several lines in a comment box. Neither is wrong; they follow different settings.
Blank lines matter around blocks as well. In CommonMark a list can interrupt a paragraph only in specific cases, and an ordered list interrupting a paragraph must start with the number one. A heading or a fenced code block placed directly under a line of text is usually safe, but adding a blank line before every block removes a whole class of rendering differences at no cost.
Raw HTML, links and what a safe renderer removes
CommonMark allows raw HTML blocks and inline tags, and passes them through to the output. That is convenient for a details element or a centered image, and it is also the route by which untrusted Markdown can carry scripts, event handler attributes or forms. The specification describes what the syntax means, not what is safe to show.
GFM adds a tagfilter extension that neutralises a short list of tags, including script, iframe, style and textarea, by escaping their opening bracket. Applications that render Markdown written by strangers go further: they either drop raw HTML entirely or run the output through a sanitizer that keeps a small allowlist of elements and attributes. Link destinations get the same treatment, because a link whose destination uses the javascript scheme runs code when clicked.
The Markdown editor on this site takes the strict route. Its document model has no node for raw HTML, so tags pasted into the source are dropped rather than rendered, and link destinations are limited to web, mail and relative addresses. The practical advice for authors is simple: if a document needs to render in more than one place, avoid raw HTML and it will not matter which policy the next renderer uses.
Getting plain text back out
Markdown often needs to leave the Markdown world: a summary for a search index, the text of a notification, or a character count that ignores syntax. Deleting every asterisk and bracket with a find and replace breaks words that legitimately contain those characters and leaves link destinations behind. A proper stripper parses the structure, keeps the visible text and discards the markup.
The example below is real output from the Markdown stripper tool on this site, run on the setup snippet shown earlier with a short list added. Strong emphasis and inline code lose their markers, the link keeps its text and drops the address, the quote loses its marker, and the list items become plain lines.
Setup Run bun install, then see the docs. Note: NODE_ENV must be set. one two
A checklist for documents that travel
Most portability problems come from a handful of habits. Following the list below keeps a README, a changelog or a knowledge base article readable in every renderer that follows CommonMark, and degrades gracefully in the ones that do not support tables or task lists.
- Put a blank line before and after every heading, list, quote, table and code fence.
- Use a backslash at the end of a line for a hard break instead of trailing spaces.
- Give every fenced code block a language name so highlighters can colour it.
- Escape vertical bars inside table cells, and keep cells to inline content.
- Avoid raw HTML; many renderers strip it and some escape it into visible text.
- Prefer reference links for URLs used more than once in a long document.
- Check the file in a strict CommonMark renderer before publishing it anywhere else.