Skip to content

MD090 - Horizontal rules should not precede headings

Aliases: no-hr-before-heading

Disabled by default. This rule is opt-in: enable it explicitly with extend-enable. There is no markdownlint equivalent; this is a rumdl-specific structure rule.

What this rule does

Flags a horizontal rule (---, ***, ___, or a spaced form such as - - -) whose only separation from the heading below it is blank lines. The heading already marks the section boundary, so the rule draws the same line twice.

Only blank lines may sit between the rule and the heading. A comment, a reference definition, or any other line in between means the rule is not directly above the heading, and nothing is reported. Both the rule and the heading must be at the top level of the document: a horizontal rule inside a blockquote or a list item belongs to that container and is left alone. Containers whose body is ordinary Markdown, such as a fenced div or a MyST directive, are treated like top-level content, so a rule directly above an ATX heading inside one is still reported. Only ATX headings are reported there: a container's opening marker followed by a dash run (note above ---) is parsed as a setext heading though it opens a container rather than a section, and since removing a rule deletes a line, this rule leaves every setext heading inside a container alone rather than guessing which ones are real.

Containers whose body is indented rather than fenced, such as a MkDocs admonition or a content tab, are not reported at all: an indented dash run is not read as a horizontal rule anywhere in rumdl. A MyST directive written with backticks (```{note}) rather than colons is silent for the same reason: its body starts out as a code fence, so nothing inside it is read as a horizontal rule. Write the directive with colons to have it checked.

A --- written directly under a line of text is not a horizontal rule at all. It is the underline of a setext heading, and this rule never touches it.

Why this matters

Generated Markdown, LLM output in particular, separates almost every section with a horizontal rule right before the next heading. Rendered, that is a line followed by a larger, bolder line; in source it is noise between sections that the headings already delimit. Removing the redundant rules keeps the heading structure as the single expression of the document's outline.

Why it is opt-in

Slide formats (Marp, Slidev, reveal.js, Pandoc slide decks) use a horizontal rule as the slide separator, and the next slide almost always opens with a heading. In those documents every rule this check would remove is a slide boundary, so the rule stays off unless a project turns it on.

Configuration

This rule has no configuration options.

[global]
extend-enable = ["MD090"]

Examples

Incorrect

## Topic

Prose.

---

## Next Topic

Correct

## Topic

Prose.

## Next Topic

A horizontal rule between two paragraphs is fine; the rule only speaks when a heading follows:

Prose.

---

More prose under the same heading.

Automatic fixes

The rule is removed together with the blank lines between it and the heading. The blank line above the rule is kept, so the heading stays separated from the paragraph before it. When no blank line sits above the rule, the fix leaves one behind, so the text above never touches the heading. A run of rules directly above one heading is removed in one pass.