Skip to content

Markdown / MDX Best Practices Skill

FieldValue
TypeSkill Resource
Source~/.copilot/skills/technical-writing/references/markdown.md
DescriptionNot specified

Source Content

Mechanical Markdown/MDX rules (formerly the standalone markdown skill), now owned by technical-writing.

Markdown / MDX Best Practices Skill

Tables: the non-negotiables

  • Never leave a cell empty. Use - as the minimum fill for any cell with no meaningful content.
  • Every table must have a header row.
  • Alignment markers: --- (left), :---: (center), ---: (right). Choose deliberately.
  • Pipe characters must align across rows for readability in source.
  • Avoid spanning / merging cells — standard Markdown tables don’t support it; reach for a list instead.
  • A teaching table earns a short italic “what to notice” line beneath it, telling the reader what to pay attention to. The docs-that-teach skill owns this rule and extends it to diagrams and charts.
<!-- Wrong -->
| Name | Role | Notes |
|---|---|---|
| Alice | Engineer | |
| Bob | | On leave |
<!-- Correct -->
| Name | Role | Notes |
|---|---|---|
| Alice | Engineer | - |
| Bob | - | On leave |

Headings

  • H1 (#) appears exactly once per document — the title.
  • Never skip levels: H1 → H2 → H3, not H1 → H3.
  • Headings are promises. Write the heading so the reader knows what they’re getting before they read the content below it.
  • No section-intro headings: no ## Introduction, ## Overview, ## Summary. The content is the introduction.
  • Named-thing comparisons always use headings. If you are comparing tools, options, approaches, or frameworks, each named thing gets its own #### heading — never bold text collapsed into one paragraph.
<!-- Wrong: options as bold in one block -->
**Option A** does X. **Option B** does Y instead.
<!-- Correct: each option gets a heading -->
#### Option A
Does X.
#### Option B
Does Y instead.
  • Thumb test: if bold text opens a paragraph and the content beneath makes sense without the bold — it’s a heading in disguise. Promote it to ####.

Paragraphs

  • Maximum 4 sentences per paragraph — the house rule, enforced by this skill’s scripts/readability.py.
  • One blank line between paragraphs.
  • No trailing spaces (they create unintended line breaks in some renderers).
  • Term-of-art consistency. Pick one form of each key term and reuse it verbatim throughout, so the reader recognizes it as shared vocabulary. Don’t drift between “institutional layer” / “institutional path” / “Institutional”, and don’t use a key word loosely as verb, noun, and modifier.

Code blocks

  • Always declare the language: ```typescript, ```bash, ```json — never a bare ```.
  • Terminal commands: ```bash or ```sh.
  • Use inline code (`backticks`) for: file names, flags, variable names, function names, short values.
  • Multi-line shell sequences get a code block, not inline code.
  • Never a bare URL in prose: [descriptive text](url), not just url.

  • Link text must make sense out of context. “Click here” or “read more” do not.

  • For repeated URLs, use reference-style links at the bottom of the document:

    See the [Mermaid docs][mermaid] for details.
    [mermaid]: https://mermaid.js.org

Images

  • Always include alt text: ![describes the image content](path/to/image.png).
  • Alt text describes what’s in the image for someone who can’t see it. “screenshot” is not alt text.
  • Decorative images with no informational value: ![](path) (empty alt deliberately).
  • scripts/markdown_lint.py checks both — empty alt on any ![]() warns (confirm it’s genuinely decorative, since this catches the honest exception above and an accidentally-forgotten alt with the same syntax), and a handful of boilerplate words (“screenshot”, “image”, “icon”…) warn as non-descriptive. Both are warnings, not errors — they don’t fail the gate. markdownlint-cli2’s own MD045 is disabled in markdownlint.json because it can’t tell the two cases apart and would hard-error on the deliberate exception.

Lists

  • Use - for unordered lists consistently. Do not mix - and *.
  • Nested lists: 2-space indent.
  • Ordered lists: use 1. for every item and let the renderer auto-number. This makes insertions safe.
  • No trailing period needed on list items unless they’re full sentences.
  • Keep list items parallel in grammatical form (all nouns, all verbs, all clauses).

Frontmatter (YAML)

  • No tabs in YAML — spaces only.
  • Quote strings that contain :, #, {, }, or leading/trailing whitespace.
  • Dates in ISO format: 2024-01-15, not Jan 15, 2024.
  • Keep frontmatter keys in a consistent order across all files in the same collection.

MDX-specific rules

Authoring a teaching MDX page? When the .mdx is a documentation page for the design system or a consuming app — a concept explainer, guide, or Storybook docs entry that uses components like Callout, CodeBlock, InteractiveMermaid, or ComparisonTable — also invoke the docs-that-teach skill. It owns page structure, the real component map (query the Storybook MCP — never invent props), format decision rules, print fallbacks, and the publish rubric. This skill still owns the mechanical rules below.

Top-level rule (the one that crashes the whole build)

The top level of an MDX file accepts only import and export statements plus JSX/Markdown content blocks. Violate this and the MDX parser (acorn) throws, which takes down every story in Storybook — not just the one file.

  • Data and variables must be exported. A bare const/let/function/class at the top level is illegal.
    • Wrong: const steps = [...]Could not parse expression with acorn
    • Right: export const steps = [...]
  • A blank line must separate the import/export block from the first content line. An import butted against prose or JSX gets folded into the ESM region.
    • Wrong: import { Card } from "..." immediately followed by <Card />Unexpected `ExpressionStatement` in code: only import/exports are supported
    • Right: one blank line between them.
  • No bare JS statements, no top-level // comments, no if/for/function calls at the top level. Logic goes inside an exported value or inside a { } expression within JSX.
import { Card } from "@/components/ui/card";
export const items = [
{ id: "a", title: "First" },
];
<div>{items.length} items</div>

After writing or editing any .mdx, validate it actually compiles before declaring done. If the repo has a validator (e.g. node scripts/validate-mdx.mjs <file>), run it; otherwise the markdown lint script below.

Storybook titles come from folders

Storybook derives a docs page’s title and sidebar slot from its file path. Put the page in the right folder and let the generated route own the title. Do not add authored title overrides as boilerplate.

Imports

  • All import statements go at the top of the file, after the frontmatter block, before any prose.
  • Unused imports are not allowed — they warn or error depending on the bundler.

Components

  • Component names must be PascalCase: <Callout />, not <callout />.
  • Self-close components with no children: <Component />.
  • Wrap multi-line JSX props in parentheses for readability.

Expressions

  • Use {expression} for dynamic content: {new Date().getFullYear()}.
  • Avoid logic-heavy expressions inline — extract to a variable above the JSX.

Prose/JSX boundary

  • Don’t mix inline HTML with MDX components in the same block.
  • Blank lines between JSX blocks and prose paragraphs.

Exports

  • Use named exports for reusable content: export const metadata = { ... }.
  • Default export is the page component (MDX files don’t usually need an explicit default export — the runtime provides one).

File location

  • New Markdown files go under docs/<subfolder>/, not at the repo root.
  • Exception: README.md, CHANGELOG.md, LICENSE.md, CONTRIBUTING.md live at the repo root by convention.

Pre-write checklist (run before drafting any .md or .mdx file)

  • Table cells — will every cell have content? Use - for blanks.
  • Heading levels — start at H1, no skipped levels.
  • Named comparisons — each named option gets ####, not bold text.
  • Code blocks — language declared on every fence.
  • Links — no bare URLs, all link text is descriptive.
  • Images — alt text written.
  • Lists — - consistently, parallel phrasing.
  • MDX imports — at the top, all used.

Lint workflow (run after writing or editing any .md / .mdx file)

Terminal window
python3 ~/.copilot/skills/technical-writing/scripts/markdown_lint.py /path/to/file.md
  • Exit 0 = clean. Warnings are printed but do not block — read them anyway.
  • Exit 1 = errors found. Fix every ERROR line, then re-run.
  • If markdownlint-cli2 is available (globally or via npx), the script runs it automatically with the config at scripts/markdownlint.json.
  • Fix → re-run → fix → re-run until exit 0 before reporting the task as done.

Self-rubric (run before responding)

  • No empty table cells — every blank is -.
  • H1 appears once. No skipped heading levels.
  • No ## Introduction / ## Overview / ## Summary section headings.
  • Named things compared via #### headings, not bold text.
  • All code blocks have a language hint.
  • No bare URLs in prose.
  • Paragraphs ≤ 4 sentences (the house rule).
  • MDX: imports at top, PascalCase components, no unused imports.