Heading levels skipped in the document outline
What is this issue?
This issue fires when the page's heading levels jump by more than one going down — an <h1> followed by an <h3>, or an <h2> followed by an <h4>.
<!-- Raises this issue -->
<h1>Guide</h1>
<h3>First section</h3>
<!-- Passes -->
<h1>Guide</h1>
<h2>First section</h2>
<h3>A detail within it</h3>Going back up by any amount is fine — h3 followed by h2 starts a new section and is not a skip.
The finding names the specific jump rather than asserting that a rule broke, because "heading hierarchy is invalid" on a page with forty headings left the reader hunting.
Why it matters
- Headings are a document outline, not a font size. Each level says "this belongs inside the previous one"; a skipped level leaves a section with no parent.
- Screen reader users lose their place. Navigating by heading level is a primary way of moving through a page, and a jump from h1 to h3 implies a missing h2 that the user goes looking for.
- It signals CSS-driven markup. The usual cause is choosing the heading level for its default size, which means the outline reflects the visual design rather than the content structure.
- It is scored below a missing h1, because it makes the page harder to navigate without changing what the page is understood to be about.
How to fix it
Find the jump. The finding names it — for example
<h1> followed by <h3>— and includes the offending heading's text and the fullheadingTree.Fix the level, not the look. Change the
<h3>to an<h2>and set the size in CSS:.section-title { font-size: 1.25rem; }Read the outline on its own. Strip the styling mentally and check the nesting makes sense: h1 → h2 → h3, with h3s only inside h2s.
Check components in isolation. A component that renders an
<h3>internally produces a valid outline in one page and a skip in another. Where this happens often, make the heading level a prop.Verify with a browser accessibility inspector's document-outline view, or re-scan.
Examples
1. h1 straight to h3
Problem — the <h3> was chosen for its size in the stylesheet, and the level below the title is simply absent:
<h1>Trail Running Shoes</h1>
<h3>Sizing</h3>
<h3>Materials</h3>Reported as: a jump from <h1> to <h3> at Sizing.
Fixed — the levels are consecutive; make the CSS match the new tags rather than the tags match the CSS:
<h1>Trail Running Shoes</h1>
<h2>Sizing</h2>
<h2>Materials</h2>2. A skip introduced by a nested component
Problem — the reviews widget renders its own <h4> internally, and it sits directly under an <h2>:
<h1>Wireless Headphones</h1>
<h2>Specifications</h2>
<h3>Battery</h3>
<h2>Reviews</h2>
<h4>Most helpful review</h4>Fixed — the widget's heading takes the level its position calls for:
<h1>Wireless Headphones</h1>
<h2>Specifications</h2>
<h3>Battery</h3>
<h2>Reviews</h2>
<h3>Most helpful review</h3>3. Going back up is not a skip
Passes — h3 → h2 closes one section and opens the next, which is the normal shape of a document. Only downward jumps of more than one level are reported:
<h1>Guide</h1>
<h2>Getting started</h2>
<h3>Installing</h3>
<h3>Configuring</h3>
<h2>Troubleshooting</h2>
<h3>Common errors</h3>Also passes — a page whose first heading is not an <h1> is not judged here. That is h1_heading_missing (#70), and the two are reported separately so a page with both problems shows both:
<h2>Troubleshooting</h2>
<h3>Common errors</h3>How PixyScan detects this
HTML parsing. The crawler collects every
<h1>–<h6>in the body, in source order, into a heading tree.Level walk. It walks the tree and compares each heading's level with the previous one. A level more than one greater than its predecessor is a skip.
First skip reported. The message names the first jump found — the tags on each side and the text of the second — and the finding carries the whole
headingTreefor context. One finding per page, not one per skip.Not flagged: going back up any number of levels, and a page whose first heading is not an
<h1>(that ish1_heading_missing).Independence. A page can raise this and
h1_heading_missingat the same time; they are separate codes precisely so both survive.Scope. Page-level.
What we store
Storage Level
Page Level — evaluated for each crawled URL.
Database Table / Prisma Model
PageSeoBasicsData
One row per crawled URL, keyed by urlId.
Fields Used
| Field | Type | Description |
|---|---|---|
| headingHierarchyValid | Boolean? | false when the walk found at least one downward jump of more than one level. This is what raises the issue |
| headingTree | Json? | Every <h1>–<h6> in source order, each {tag, text} with text truncated to 200 characters |
headingTree is what makes the finding usable. The message names the first skip — the tags on each side and the text of the second — and the stored tree is how the report shows that jump in the context of the page's other headings. On a page with forty headings, "heading hierarchy is invalid" on its own left the reader hunting.
One finding per page, not one per skip. The same two columns are read by h1_heading_missing (#70), and a page can raise both.
Detection Dependencies
- HTML Document — the body of the fetched page is parsed for its heading elements, in source order