Part of the jedee wiki — pages written by an AI (Claude Code), supervised by Johan.

Abbreviations

Wiki Revised

<abbr title="Cumulative Layout Shift">CLS</abbr> marks a shortened form and carries its expansion. The element is old and uncontroversial; what it is worth is not.

The expansion rides in a title attribute, and title is the least reliable way to say anything on the web — it never appears on touch, most browsers never surface it to keyboard users, and its delay, styling and truncation belong to the operating system. Tooltips covers that in full. So <abbr> is a convenience for someone with a pointer, not a mechanism anyone can depend on. WCAG 3.1.4 Abbreviations (Level AAA) asks for a mechanism to find the expanded form, and expanding on first use in the prose is the one that always works. <abbr> is the cheap second layer under it, not a substitute.

There is no <abbr> in markdown. markdown-it-abbr adds the PHP Markdown Extra syntax — a definition line anywhere in the document, and every matching word in the body is wrapped:

FOFT is a font-loading strategy.

*[FOFT]: Flash of Faux Text

Two properties of that plugin decide how it is best used. It rewrites only text tokens, so a code span or a fenced block is never touched — process.env.TZ in a sample stays plain while TZ in a sentence does not. And it wraps every occurrence, not the first, which makes a common abbreviation a row of dotted underlines down the page. The selection of what to define is therefore the whole design decision.

In jedee

markdown-it-abbr is Eleventy Excellent stock: it is already in the markdown pipeline (src/_config/plugins/markdown.js), and EE styles abbr[title] in base/global-styles.css with a dotted underline and cursor: help.

The dots are jedee’s own since 2026-10-08. A dotted text-decoration can’t set the gap between its dots, so the row is a radial-gradient background repeated along the bottom of the text: each dot is as wide as a link underline is thick (--underline-thickness, 0.2ex, so it grows with the text), dots sit 0.25em apart (ten under WCAG in body text), and they are the text color at 50%, which comes out darker on the light theme and lighter on the dark one and reads as a hint rather than a link. ⚠️ A background is placed from the bottom of the text box, not the baseline, so --abbr-lift (0.075em) lifts the row onto the line a link underline would take; it was measured against a link in Source Sans, and Source Serif headings come out the same. Neither had ever been used by the wiki. The only definition in the repo was a *[WAV]: line at the bottom of one audio post.

Definitions are per-document, which is the wrong shape for a wiki of fifty pages and growing. jedee’s addition is one glossary in the wiki’s directory data, src/wiki/wiki.11tydata.js:

glossary: {
  ARIA: 'Accessible Rich Internet Applications',
  BCP: 'Best Current Practice',
  CLS: 'Cumulative Layout Shift',
  // … 31 in total
}

and one preprocessor in eleventy.config.js that turns it into definition lines on the way past:

eleventyConfig.addPreprocessor('glossary', 'md', (data, content) => {
  if (!data.glossary) return;
  const defs = Object.entries(data.glossary).map(([short, long]) => `*[${short}]: ${long}`);
  return `${content}\n\n${defs.join('\n')}`;
});

It is registered for all markdown but gated on data.glossary, and only src/wiki/ sets that key. Appending at the end is safe: abbr_def is a block rule and the replacement is a core rule that runs after every block is parsed, so a definition never has to precede its use.

The glossary holds opaque jargon only — FOFT, POSSE, PESOS, DTCG, CUBE, EXIF, RDFa, BCP, OOM, FIT and the rest. CSS, HTML, JS, JSON, API, URL and DOM are deliberately left out, and so is EE: “CSS” alone appears 192 times across these pages, and 192 dotted underlines is noise rather than help. 31 terms produce 179 elements across the built wiki.

⚠️ The data key must not be called abbreviations

The first version of this named the key abbreviations. The build was green and most abbreviations were correct, but Layout shift also carried fifteen of these:

<abbr title="undefined">A</abbr> tall container nudged a few pixels …

Eleventy’s markdown engine calls md.render(str, data), so the page’s whole data object is markdown-it’s env. markdown-it-abbr keeps its definitions in env.abbreviations, prefixing every key with : to avoid Object.prototype collisions. A data key of that name is not merely visible to the plugin — it is the plugin’s store.

The replacement step builds its match pattern by stripping that prefix back off, then looks each match up again with the prefix restored:

Object.keys(state.env.abbreviations).map(x => x.substr(1))   // ':CLS' → 'CLS'
// …
token_o.attrs = [['title', state.env.abbreviations[':' + m[2]]]]

Against an unprefixed glossary, every key is read one character short: UA becomes the pattern A, TZ becomes Z, OG becomes G. The lookup then misses, and the title serializes as the string undefined. Every standalone “A” in the prose was wrapped.

What made it hard to see is that the site’s own definitions still worked. The preprocessor’s *[FOFT]: … lines were parsed into the same object with the : intact, so FOFT, CLS and the rest resolved correctly next to the broken single letters, and the page looked mostly right.

Renaming the key is the whole fix. The general form is worth carrying: a markdown-it plugin’s per-render state shares an object with the page data, so a front-matter or directory-data key can silently drive a plugin that never heard of it. Check which env.* keys a plugin claims before naming a data key. It is the same class of surprise as The interlinker's second render pass — a markdown-it plugin reaching further into the build than its own syntax suggests.

Raw source: src/_raw/dev-notes/How the wiki gets its abbr elements.md

Keyboard shortcuts

⌘K
Open the search
↓ ↑
Move through the search results
Enter
Open the selected result
Esc
Close the search or this list
⌥G
Show or hide the layout grid
⌥R
Show or hide the rhythm lines
⌥L
Open this list