Portable Markdown headings with astro-downgrade-heading
Every post on this site gets its <h1> from the layout. That left the
Markdown body with an awkward choice: start at ##, or render a second
top-level heading.
One title too many
The layout reads the title from frontmatter and renders it before the article. That is useful everywhere except in the Markdown source, where a perfectly ordinary first-level heading becomes another <h1> on the finished page.
I could avoid the duplicate by lowering every heading by hand:
## Background
### The heading problem
The HTML is valid, but the source now describes where the document happens to be embedded rather than its actual structure. Move it somewhere else and those headings are still artificially low.
Fixing it at render time
astro-downgrade-heading moves that adjustment into the Markdown pipeline. The source keeps its natural hierarchy; during rendering, # becomes <h2>, ## becomes <h3>, and anything deeper is capped at <h6>.
The setup is intentionally short:
npm install astro-downgrade-heading
import { defineConfig } from "astro/config";
import downgradeHeading from "astro-downgrade-heading";
export default defineConfig({
integrations: [downgradeHeading()],
});
The integration detects whether Astro is using Satteri or remark and adds the corresponding plugin without replacing the rest of the Markdown configuration.
This article is also the test case. Every section heading in the source starts with #; on the rendered page, the title remains the only <h1>.
An escape hatch
Not every document sits below a layout-owned title. A page that renders its Markdown as-is can opt out in frontmatter:
---
downgradeHeading: false
---
The package also exposes its Satteri and remark plugins directly for projects that configure the Markdown pipeline themselves.
That is the whole package. It solves one small structural mismatch without making the Markdown aware of the layout around it.