GitHub Flavored Markdown (GFM)
Configure the five GFM constructs (strikethrough, tables, autolink literals, task lists, footnotes) via markdown.gfm.
zfb's Markdown pipeline supports the five GitHub Flavored Markdown (GFM) constructs implemented by the markdown-rs parser: strikethrough, tables, autolink literals, task list items, and footnote definitions. The markdown.gfm config key controls which of the five are active.
Conservative default
When markdown.gfm is absent — including when the whole markdown block is omitted — zfb applies a conservative default: strikethrough, tables, and autolink literals are on; task list items and footnote definitions are off. Those three are the constructs GFM-accustomed authors expect without config; task lists and footnotes change document structure, so they stay opt-in rather than turning on the full GFM surface unconditionally.
import { defineConfig } from "zfb/config";
export default defineConfig({
// markdown.gfm omitted — conservative default applies:
// strikethrough + table + autolink literals on, task lists + footnotes off
});Value shapes
markdown.gfm accepts three shapes:
Absent (default)
Omit the field entirely (or omit markdown altogether) to get the conservative default shown above.
Boolean shorthand
export default defineConfig({
markdown: {
gfm: true, // every GFM construct on
},
});gfm: true turns every construct on, including autolink literals, task list items, and footnote definitions. gfm: false turns every construct off — including strikethrough and tables.
Per-construct object
export default defineConfig({
markdown: {
gfm: {
taskListItem: true,
autolinkLiteral: false,
// strikethrough, table, footnoteDefinition omitted here fall back
// to the conservative default, not to `false`
},
},
});Fields you omit from the object keep the conservative-default value for that construct (strikethrough: true, table: true, autolinkLiteral: true, the other two false). Fields you set explicitly are honored verbatim — the example above opts in to task lists and out of autolink literals.
Constructs
| Construct | Object key | Conservative default | Renders as |
|---|---|---|---|
Strikethrough (~~text~~) | strikethrough | On | <del>text</del> |
| Pipe tables | table | On | <table> |
Autolink literal (bare https:) | autolinkLiteral | On | <a href="..."> |
Task list items (- [x] / - [ ]) | taskListItem | Off | Checkbox — see below |
Footnote definitions ([^ref]: ...) | footnoteDefinition | Off | Footnote section — see below |
Task list checkboxes
Enabling taskListItem renders each - [x] / - [ ] item's checked state as a checkbox: a disabled <input type="checkbox"> opens the item's own paragraph, followed by a single space, carrying a checked attribute only when the item is checked. A plain list item with no [x]/[ ] marker is unaffected.
The checkbox goes inside the item's paragraph rather than beside it so it renders on the same line as its label. zfb does not unwrap tight-list paragraphs, so a checkbox emitted as a sibling of the <p> would sit on its own line above the text.
- [x] Done
- [ ] Not done<ul>
<li><p><input type="checkbox" disabled checked /> Done</p></li>
<li><p><input type="checkbox" disabled /> Not done</p></li>
</ul>This is minimal, GitHub-compatible checkbox rendering, not GitHub's full reference markup — there is no contains-task-list/task-list-item class on the <ul>/<li>, and the checkbox is always disabled (this is static, server-rendered output with no client-side toggle handler). If you are diffing zfb's output against github.com's own rendering, expect the checkbox itself to match but the surrounding markup to be plainer.
Footnotes
Enabling footnoteDefinition renders [^ref] references and their [^ref]: ... definitions as a collected footnote section at the end of the document, matching GitHub's own footnote behavior:
Numbering is by first-reference order, not definition source order — a definition written first but referenced second gets the higher number.
IDs are
user-content-fn-{slug}for a definition anduser-content-fnref-{slug}for a reference, where{slug}is the footnote label slugified the same way heading anchors are (case-folded ASCII, punctuation stripped, non-ASCII such as CJK passed through unchanged). A label that slugifies to an empty string (e.g.[^!!!]) falls back to the footnote's 1-based number instead (user-content-fn-1).Repeated references to the same definition share one number but each get their own backreference id, so the definition can link back to the specific occurrence that was clicked.
Duplicate definitions for the same label collapse to one — the first definition wins, later ones are discarded silently (matching CommonMark's rule for duplicate link reference definitions).
Unreferenced definitions render nothing — numbering follows reference order, so a definition nobody points at has no place in the sequence.
The section carries
data-footnotes, a visually-hiddenrole="heading"landmark reading "Footnotes" (a<div>, not a real<h2>— a real heading tag would be picked up and rewritten by zfb's own heading-anchor/TOC plugins), and both reference and backreference links carry accessibility attributes (aria-describedby/aria-label) matching GitHub's markup.The landmark is hidden by an inline
style, not by thesr-onlyclass it also carries. zfb ships no stylesheet defining.sr-only, and because the class is emitted by zfb itself it never appears in any file Tailwind scans, so Tailwind's ownsr-onlyutility is never generated either — a class-only approach would leave the landmark visible in most projects. The class remains as a styling hook. To make the heading visible instead, override the inline style with!important:.footnotes [role="heading"] { position: static !important; width: auto !important; height: auto !important; margin: 0 !important; clip: auto !important; clip-path: none !important; }
Footnotes[^note] keep reading order.
[^note]: The footnote text.<p>Footnotes<sup><a href="#user-content-fn-note" id="user-content-fnref-note" data-footnote-ref aria-describedby="footnote-label">1</a></sup> keep reading order.</p>
<section data-footnotes class="footnotes">
<div role="heading" aria-level="2" class="sr-only" style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);clip-path:inset(50%);white-space:nowrap;border:0" id="footnote-label">Footnotes</div>
<ol>
<li id="user-content-fn-note">
<p>The footnote text.</p>
<a href="#user-content-fnref-note" data-footnote-backref aria-label="Back to reference 1">↩</a>
</li>
</ol>
</section>Autolinks are never created inside a link
A bare URL that appears inside an existing link's label is left as plain text, matching the GFM spec — cmark-gfm's autolink extension never descends into a link node. So a link whose label happens to be a URL renders exactly one anchor:
[http://localhost:4321](http://localhost:4321)<p><a href="http://localhost:4321">http://localhost:4321</a></p>Without this rule the label's URL would autolink too, producing an <a> nested inside an <a> — invalid HTML that fails validators with element-permitted-content. The same applies to a URL merely embedded in a label ([see https:), to the www. form, and inside an MDX <a> element.
Interaction with CJK-friendly handling
autolinkLiteral interacts with markdown.cjkFriendly (on by default): a bare URL flush against CJK text would otherwise swallow the trailing CJK run into the link href. See CJK-friendly emphasis for the boundary rule.
See also
Markdown Features — the full feature map.
CJK-friendly emphasis — the
autolinkLiteral+ CJK boundary interaction.