Skip to main content

Data & APIs

GitHub Flavored Markdown vs CommonMark: Why the Same File Renders Differently

Vibeus Moonscript

5 min read

GitHub Flavored Markdown vs CommonMark: Why the Same File Renders Differently
On this page

You write a table into a README, push it, and GitHub renders it properly. The same file goes through a static site build and comes out as a row of pipe characters sitting in a paragraph. Nothing is broken and nothing is misconfigured. The two renderers are reading different specifications, and the difference between GitHub Flavored Markdown and CommonMark accounts for most of the “but it looked fine on GitHub” bugs you’ll run into.

What separates GitHub Flavored Markdown from CommonMark

Markdown went a decade without a formal specification. The 2004 original was a description and a Perl script, and it left enough undefined that every implementation resolved the ambiguities on its own. CommonMark exists to close those gaps. It pins down the parsing rules precisely enough that two compliant parsers produce byte-identical HTML from the same input, the same way a strict JSON parser either accepts a document or tells you exactly where it gave up. If that comparison is useful to you, the common JSON parse errors and how to read them follow similar logic.

CommonMark also stops there, deliberately. Tables aren’t in it. Task lists aren’t either.

GitHub published a formal spec for its own dialect that sits on top of that baseline. The GFM specification puts it in one line: “GFM is a strict superset of CommonMark.” Every valid CommonMark document is a valid GFM document and renders identically. Version 0.29-gfm dates from April 2019, and everything GitHub adds is labeled in the spec as an extension.

Superset only helps in one direction. Write plain CommonMark and it renders anywhere. GFM renders on anything that turned the extensions on.

The five extensions, and the one nobody counts

The spec defines five:

Tables. Pipe-delimited rows with a header separator line under the first row. That separator is the part that matters. Without it you get literal pipes in a paragraph, which is exactly the README-versus-build failure above.

Task list items. - [ ] and - [x] inside a list, rendered as checkboxes.

Strikethrough. ~~text~~, which is one tilde on each side in some dialects and two in GFM.

Autolinks. A bare URL such as https://example.com becomes a link with no angle brackets and no link syntax around it.

Disallowed raw HTML. A filter that neutralizes a fixed set of tags, <script>, <iframe>, <style> and a handful of others, when they appear as raw HTML inside a document.

GitHub’s own announcement of the formal spec describes the extensions as “a strict and optional superset of the original specification” and lists four of them. It leaves out the last one. That’s defensible, since the first four are things you type and the fifth is something the parser refuses to hand through, but nearly every comparison article inherits that list of four and the security extension disappears from the conversation. I think it’s the most important one of the five, and it’s the one you can’t see by looking at a rendered page.

Which extensions your documents actually use

Feature lists are easy to write and hard to act on, so I counted what this blog does. Twenty-one posts, scanning the body of each with fenced code blocks stripped out first so that a pipe character inside a shell example doesn’t get mistaken for a table row.

Six of the 21 posts contain a real GFM table, header separator and all. Twelve use fenced code blocks, across nine languages, with JavaScript and CSS accounting for 38 of the fences between them. Zero use task lists, and zero use strikethrough.

So a hair under a third of this site’s own writing would degrade in a strict CommonMark renderer, and it would degrade silently. No error, no warning, just pipes where a table used to be. The features I’d have guessed were load-bearing, task lists in particular, turned out to be unused here, which is a reminder that the extension that breaks your build is rarely the one you were worried about.

Raw HTML is the part that bites

Markdown permits raw HTML by design. That was the point in 2004, and it means a Markdown document can carry a <script> tag, an inline onerror handler, or a link with a javascript: URL. A parser that faithfully converts Markdown to HTML will faithfully convert those too.

The disallowed-raw-HTML extension filters a specific list of tags. It’s a narrow list, and GitHub layers a stricter sanitizer of its own on top of the spec before anything reaches github.com. Your renderer almost certainly does neither of those things by default. Most widely used JavaScript parsers hand back whatever HTML the document asked for and leave the sanitizing to you.

The practical rule for anything rendering Markdown that another person wrote, whether that’s comments, documentation contributions, or a CMS field: parse the Markdown, sanitize the resulting HTML with a dedicated library, and only then put it in the page. Sanitize after parsing rather than before, because escaping the source first breaks legitimate formatting while still leaving the parser free to generate tags afterwards.

One detail worth checking in your own stack: sanitizers that need a DOM behave differently when the same code runs during a server-side render. DOMPurify, for instance, exports a factory rather than a working instance when no window object exists, so a sanitize call that works in the browser can throw during a static build. Ours did, and the error ended up in the generated HTML until we caught it.

Testing what your renderer speaks

You don’t need to read your parser’s documentation to find out which extensions are enabled. Paste a document containing one of each and look at what survives.

| Feature | Works |
| ------- | ----- |
| Tables  | ?     |

- [ ] task list item
~~strikethrough~~
https://example.com

Four results, four answers. A rendered table means the tables extension is on. Checkboxes mean task lists are on. Struck-through text and a clickable bare URL cover the other two. If the table renders as pipes, you’re on a CommonMark parser and you either enable the extension or stop using tables in files that go through it.

Most libraries take a flag. marked and markdown-it both accept a GFM option, and static site generators usually expose it in their Markdown config. Pandoc treats the dialects as separate input formats entirely, so you name gfm or commonmark when you run it.

Writing documentation that has to survive more than one renderer is a different job from writing a README, and placeholder text makes that testing easier when the real content isn’t written yet. We cover that in Lorem Ipsum Alternatives for Developers.

Write GitHub Flavored Markdown with a live preview, then convert it to HTML, plain text, or PDF.

Try it free

Markdown Editor, Viewer and Converter

Write Markdown with a live preview, then convert it to HTML, plain text, or a PDF. Paste HTML to get Markdown back.

Open Markdown Editor

Written by

Vibeus Moonscript

Writes DevBottle's guides and builds the tools they cover. About DevBottle