Skip to content
Issue docs

Heading levels skipped in the document outline

Standardheading_levels_skippedIssue 71

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

  1. Find the jump. The finding names it — for example <h1> followed by <h3> — and includes the offending heading's text and the full headingTree.

  2. Fix the level, not the look. Change the <h3> to an <h2> and set the size in CSS:

    .section-title { font-size: 1.25rem; }
  3. Read the outline on its own. Strip the styling mentally and check the nesting makes sense: h1 → h2 → h3, with h3s only inside h2s.

  4. 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.

  5. 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

  1. HTML parsing. The crawler collects every <h1>–<h6> in the body, in source order, into a heading tree.

  2. 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.

  3. 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 headingTree for context. One finding per page, not one per skip.

  4. Not flagged: going back up any number of levels, and a page whose first heading is not an <h1> (that is h1_heading_missing).

  5. Independence. A page can raise this and h1_heading_missing at the same time; they are separate codes precisely so both survive.

  6. 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

Further reading