The common constructs

Headings use pound signs at the start of a line. Six levels available, one pound per level. There's a legacy alternative called "setext" style where you underline with === for h1 and --- for h2, but nobody writes that way anymore. The main thing worth knowing is that most renderers will silently auto-generate a fragment ID from the heading text so links like #my-heading work against the rendered HTML.

Bold is **text**. Italic is *text*. Some dialects accept __ and _ respectively; GFM accepts both. Combined bold-italic is ***three stars***. Strikethrough is ~~two tildes~~ — GFM only, not in the original spec. Inline code is backticks: `foo`. If you need a literal backtick inside inline code, use double backticks as the delimiter: ``with a ` inside``.

Unordered lists are -, *, or + (all equivalent). Ordered lists are numbers followed by a period — and the actual numbers don't matter. 1. 1. 1. will render as 1. 2. 3., which is a feature: you can reorder lines without renumbering. Nested lists use two-space indents per level (some renderers accept four, but two is safer across editors).

Links are [text](url). Images are the same shape with a leading exclamation mark: ![alt text](url). Reference-style links — where you use [text][ref] and then define [ref]: https://... at the bottom of the doc — are valid but rarely used outside heavily-cross-referenced documents. They make drafts easier to scan; they make renderers work harder.

Blockquotes use >. Multi-line quotes repeat the marker on each line. Nested quotes double it up (>> deeper). The original spec required a space after the >, but every real renderer tolerates its absence.

Horizontal rules are three or more of -, *, or _ on their own line. Avoid --- directly under a paragraph — in most renderers that's interpreted as the setext heading syntax and turns the paragraph above into an h2.

Code blocks

The modern form is the fenced block:

```language
code goes here
```

The language identifier is optional. Without it, the renderer won't syntax-highlight (or it'll try to auto-detect, which is hit-or-miss). With it, you get proper highlighting for whatever languages your renderer supports. Common identifiers: js, javascript, ts, typescript, py, python, go, rust, rb, ruby, sh, bash, sql, html, css, json, yaml, toml, diff, md.

The older form — indent every line by four spaces — still works. It's what CommonMark and GFM both parse. But you lose the language identifier, and it's fragile because any accidental de-indent breaks the block. Use fences unless you're writing for a renderer that predates them.

Inside fences, the code is treated as literal text. No Markdown processing happens, no entity escaping. You can write <, &, backticks, whatever.

Tables

GFM tables use pipes as column delimiters and a separator row of dashes:

| Column A | Column B | Column C |
|----------|:--------:|---------:|
| left     | center   | right    |
| more     | more     | more     |

The colons in the separator row control alignment: :--- left-align (default), :---: center, ---: right-align. At least three dashes per cell, by convention; many renderers accept one.

Leading and trailing pipes are optional. Column widths don't have to line up — | a | b | and |a|b| render identically. Most editors will auto-align on save. If the separator row is missing or malformed, GFM won't render the lines as a table at all; they'll come out as a single paragraph with literal pipes, which looks broken. That's one of the two most common table errors.

The other common error: you can't put multi-line content in a cell. Tables are single-row-per-source-line. If you need a paragraph with a line break inside a cell, you use <br>. If you need a list inside a cell, GFM says no — use HTML, or factor the content out of the table.

Task lists

A GFM extension. Check marks inside list items render as real HTML checkboxes:

- [ ] incomplete
- [x] complete

The x has to be lowercase. The brackets have to be immediately after the list marker with exactly one space. Some renderers accept capital [X]; most don't. Most renderers render the checkboxes as disabled (read-only) — they're visual indicators, not interactive.

Autolinks and raw URLs

GFM auto-converts bare URLs and email addresses to clickable links. https://example.com in the middle of a paragraph becomes <a href="https://example.com">https://example.com</a>. This is GFM-only — CommonMark requires angle brackets (<https://example.com>) for the same effect.

If you don't want a URL to autolink, wrap it in backticks or use HTML-escape (&lt;). Autolinks only trigger for URLs that start with a scheme (http, https, ftp) or for recognizable email-shaped tokens. A URL without a scheme (example.com/foo) stays as text.

Raw HTML passthrough

Every Markdown renderer has a policy about inline HTML. GFM's default is permissive: if you write <div class="callout">...</div> in your Markdown, it comes through unchanged. That's handy for edge cases the Markdown syntax doesn't cover — center-aligned text, details/summary blocks, custom callouts.

Some renderers sanitize by default, stripping tags or attributes that could carry scripts. The permissive-by-default ones include GitHub's own rendering (in READMEs and issues), most static-site generators, and the Tooly McToolface Markdown Editor. Cautious ones include Slack, Discord, and Reddit's front-end.

If you're rendering someone else's Markdown and displaying it on a page, sanitize. If you're rendering your own, trust yourself.

The traps worth knowing

Hard line breaks. A single newline in the middle of a paragraph doesn't make a new paragraph in CommonMark — it becomes a soft line break (whitespace). To force a real line break without starting a new paragraph, end the line with two spaces (invisible) or use GFM's \\ (backslash at end of line). A blank line between paragraphs always works and is by far the most reliable approach.

Lists inside paragraphs don't nest the way you'd think. A list item with a nested list requires the nested list to be indented. But if the nested list has less indentation than expected, the renderer will quietly lift it out to the outer list's level. Two-space indent per level is the safest rule; some editors render four-space indents as deeper nesting instead.

Tight vs loose lists. If your list items are separated by blank lines, the list becomes "loose" and each item gets wrapped in a <p>. Without blank lines, it's "tight" and items become bare <li> text. The difference matters for styling — a loose list gets more vertical spacing.

Smart punctuation. Some renderers silently convert "hello" to “hello”, three dots to , and -- to . This is called typographic replacement. It's helpful for blog posts, aggravating inside technical documentation where you actually wanted the straight ASCII. Most renderers have a flag to disable it; check your renderer's docs if you see your punctuation changing unexpectedly.

Underscores inside words. one_thing_two should render as literal text (not as italicized thing) because there are word characters on both sides of the underscores. CommonMark and GFM both agree on this. Some older Markdown implementations don't — they'll italicize. If you're targeting those, escape the underscores with backslashes.

The 100-character column convention. Some editors soft-wrap long lines at display time but save them as single-long-source-lines. Others hard-wrap at 80 or 100 columns with line breaks. Both render the same (because of soft-line-break collapsing above), but diff-reviewing a document that's been re-wrapped is painful. Pick a convention and stay consistent inside a project.

Where editors disagree

CommonMark is a specification designed to nail down ambiguities in the original Markdown. GFM builds on CommonMark with the four extensions named above (tables, task lists, strikethrough, autolinks). Beyond that, editors diverge.

Footnotes ([^1] references and [^1]: explanation definitions) are an extension most editors support but none have standardized the same way. GitHub supports them in specific contexts (issues, discussions) but not in READMEs. Pandoc supports them everywhere. Hugo and Eleventy support them through their configured Markdown engine (goldmark, markdown-it).

Math blocks ($$...$$ for display, $...$ for inline) are rendered by anything using KaTeX or MathJax. GitHub renders them natively in READMEs since 2022. Most static-site generators can be configured to. Discord, Slack, and plain GFM renderers don't.

Emoji shortcodes (:tada: → 🎉) are GitHub/Slack/Discord conventions, not part of any spec. Most Markdown renderers pass them through as literal text.

YAML frontmatter (the ---/.../--- block at the top of a file) is a convention from static-site generators. It's outside the Markdown spec entirely. Hugo, Jekyll, Eleventy, Astro, Next.js MDX, and Obsidian all use it. GitHub ignores it silently. If you paste a frontmatter-bearing file into a non-SSG renderer, you'll see the raw YAML at the top.

Callouts (> [!NOTE], > [!WARNING], etc.) are a GitHub-specific extension shipped in 2023. Obsidian has its own variant. Other renderers render them as plain blockquotes.

Companion tool

If you want to write Markdown with live preview right now: Markdown Editor. GFM-enabled parser (marked.js), syntax-highlighted code blocks (highlight.js), three view modes (split, editor-only, preview-only), and four export options (copy HTML, download HTML, download .md, print to PDF). Everything runs in your browser.

For the Markdown Editor's specific render behavior: line breaks follow CommonMark rules (soft newlines, explicit two-space-at-end-of-line for hard breaks). Raw HTML passes through unchanged (no sanitization — appropriate for writing your own Markdown; not appropriate if you're rendering third-party content). The file-export naming auto-derives from the first # heading in the document. For rendering someone else's Markdown for public display, you'd want a different tool that sanitizes, or wrap the output through DOMPurify before inserting into your DOM.