Writing Markdown
PublicWrite markdown with one sentence per line, no wrapping, dash bullets, ATX headings and tagged code fences, and run markdownlint-cli2 after every edit.
Writing Markdown
Write markdown with one sentence per line, no wrapping, dash bullets, ATX headings and tagged code fences, and run markdownlint-cli2 after every edit.
Applies to:
- All
.mdfiles in a repository (docs, README, CHANGELOG). - Markdown rendered on GitHub: PR descriptions, issue bodies, release notes, PR and issue comments.
Does not apply to commit messages. See Good Commit Messages for commit message rules.
Rules
- One sentence per line. A new sentence starts on a new line. This enables line-level diffs and makes review comments land on the specific sentence that changed.
- No line length limit. Do not wrap prose at column 80 or any other column. Long URLs, long code references, and long sentences stay on a single line.
- Bullets with multiple sentences split across lines with 2-space indent continuation. The second and later sentences go on new lines, indented by 2 spaces so they render as part of the same bullet.
- Dash for bullets (
-). Never*or+. - ATX-style headings (
#,##,###). Never setext (===,---under the text). - Fenced code blocks require a language tag.
Use
```dart,```bash,```text,```jsonc, etc. Use```textfor plain output when no language fits. - File ends with a trailing newline.
- Blank line above headings is required. A blank line below a heading is optional. Blank lines around lists, fenced code blocks, and tables are optional, adjacent label-then-list or heading-then-table patterns are allowed.
- No required h1. Files do not need to start with a top-level heading.
One sentence per line
Good:
The trainer broadcasts power every 250ms.
Clients subscribe to the Cycling Power Service.
When the trainer disconnects the session pauses automatically.
Bad (multiple sentences on one line):
The trainer broadcasts power every 250ms. Clients subscribe to the Cycling Power Service. When the trainer disconnects the session pauses automatically.
Bad (prose wrapped at column 80 mid-sentence):
The trainer broadcasts power every 250ms. Clients subscribe to the Cycling
Power Service. When the trainer disconnects the session pauses
automatically.
Bullets with multiple sentences
Good:
- The device manager holds all connected devices.
It also re-publishes the controller source list when a device attaches late.
- Controllers debounce rising edges across paired endpoints.
Bad (second sentence inline):
- The device manager holds all connected devices. It also re-publishes the controller source list when a device attaches late.
- Controllers debounce rising edges across paired endpoints.
Bad (second sentence as a sibling bullet):
- The device manager holds all connected devices.
- It also re-publishes the controller source list when a device attaches late.
- Controllers debounce rising edges across paired endpoints.
Fenced code blocks need a language tag
Good:
```dart
final beacon = Beacon.writable(0);
```
Good (plain output, no language fits):
```text
Connected: Wahoo Kickr
```
Bad (no language):
```
final beacon = Beacon.writable(0);
```
Linting
Run markdownlint-cli2 after every markdown edit, before declaring the task done:
markdownlint-cli2 --config ~/.markdownlint-cli2.jsonc path/to/file.md
Always pass --config ~/.markdownlint-cli2.jsonc explicitly.
markdownlint-cli2 only walks up to the current working directory when searching for config, so a config in $HOME is not auto-discovered from a project subdirectory.
Passing --config makes the tool behave consistently regardless of cwd.
Fix every violation the tool reports. Do not dismiss violations as "pre-existing".
Installing markdownlint-cli2
macOS:
brew install markdownlint-cli2
Other platforms:
npm install -g markdownlint-cli2
Global config
The shared config lives at ~/.markdownlint-cli2.jsonc and must be passed via --config.
It disables:
MD013(line-length)MD031(blanks around fences)MD032(blanks around lists)MD033(inline HTML, needed for<details>, sized images, etc.)MD041(first line must be h1)MD058(blanks around tables)
It pins bullet style to dash and heading style to ATX, sets MD024 to siblings_only: true so that repeated subheadings (e.g., ### Purpose under two different ## sections) are allowed, and relaxes MD022 so that a blank line below a heading is optional.
Everything else comes from the default: true ruleset.
What markdownlint cannot enforce
- One sentence per line.
There is no markdownlint rule for this.
Check manually: look for
.(period followed by space) inside paragraphs and bullet text. - Whether a continuation sentence should live inside the current bullet or become a new bullet. Use judgment: if the second sentence elaborates on the first, keep it in the same bullet. If it is a separate point, make it a sibling bullet.