Check heading structure hierarchy
List your headings in page order and see whether the hierarchy holds: h1, h2, h3, h2, h3 passes, while going from h2 straight to h4 is reported as a missing h3 at that exact position. Exactly one h1 is required. Free in your browser.
- Free
- Runs in your browser
- No sign-up
- Your data stays on your device
Run — free
Worked example
- Headings
- h1, h2, h3, h2, h3
- Heading count
- 5
- H1 count
- 1
A heading hierarchy can look correct on screen while its underlying tags tell a different story. This checker accepts heading tags in document order, such as h1, h2, h3, and h2, then applies two focused rules: the list must contain exactly one h1, and no heading may descend by more than one level at a time. It returns a clear validity result, the h1 count, and the exact position of each skipped level so editors and automated publishing systems can act on the findings.
Prepare the heading sequence in document order
Start with the heading tags exactly as they occur in the rendered document, not as they appear in a design file or content outline. A typical input might be h1, h2, h3, h2, h3. The order matters because hierarchy is expressed by the transition from one heading to the next.
The checker accepts tags from h1 through h6, ignores surrounding whitespace, and treats uppercase forms such as H2 the same as lowercase forms. It does not accept arbitrary HTML, heading text, CSS classes, or numbers standing alone. An empty list is rejected as invalid input because there is no document structure to assess.
A malformed entry is also rejected with its array index, which makes a faulty extraction step easier to diagnose. This separation is useful in pipelines: extraction determines which tags exist, while this capability evaluates only their order and levels. Keeping the stages distinct produces stable results and prevents visual styling from being mistaken for semantic structure.
Understand the two hierarchy rules
The first rule requires exactly one h1 in the complete list. A result with no h1 fails because the document has no single top-level heading, while two or more h1 tags fail because the top level is repeated. The second rule looks at each adjacent pair and reports a skip whenever the next number is more than one greater than the previous number.
Moving from h2 to h4 therefore reports a missing h3. Moving from h1 to h5 reports h2, h3, and h4 as missing in that transition. Moving upward, such as h4 to h2, is allowed because it closes nested material and returns to a broader section.
Repeating a level is also allowed, so consecutive h2 tags can introduce sibling sections. The checker reports every skip rather than stopping after the first one. Each record includes the zero-based index of the destination tag, the transition, and all missing intermediate tags.
The result is valid only when the h1 count equals one and the skipped-level collection is empty.
Use the result in reviews and publishing gates
Use the valid field for a simple pass or fail decision, then retain the detailed fields for a useful correction message. The issues array provides concise English summaries for logs, pull-request comments, or a content management interface. This capability intentionally does not judge heading wording, keyword use, visual size, empty heading text, or whether a page should contain more sections.
Those are separate content and markup concerns. It also does not fetch a URL or parse HTML; callers that begin with a page should extract the ordered tags first. Because the algorithm has no network access, randomness, clock dependency, or hidden state, the same list always produces the same response.
What you can do with it
Block broken article outlines
Reject a draft when its extracted tags omit the single h1 or jump from a section heading directly to an improperly nested level.
Check templates after a redesign
Compare representative rendered pages and find heading-level skips introduced by reusable components or changed theme markup.
Add structure checks to CI
Turn an ordered tag list from a static-site build into a deterministic pass or fail result before deployment.
FAQ
What does the check cost?
The calculation is deterministic and does not call external services.
Is moving from h4 back to h2 considered a skip?
No. Returning to a broader level is allowed; only a downward move that increases by more than one level is reported.
Are uppercase tags accepted?
Yes. Tags such as H1 and H3 are normalized to lowercase after validation, and surrounding whitespace is ignored.
Does this capability parse an HTML page?
No. It expects an already-extracted list of h1 through h6 tags in document order and validates only that sequence.
What happens if the page has no h1 or more than one?
The check fails either way. With no h1 the document has no single top-level heading, and with two or more the top level is repeated. The h1 count is returned so you can see which of the two cases applies.
Related tools
For developers — API access
Everything on this page is available programmatically. This section is for teams who want to wire it into their own systems; everyone else can just use the tool above.
API endpoint
Prefer to automate it? One authenticated POST creates the task; the result comes back by webhook or a signed link. The same capability also runs here on the web, by email and from Telegram — and soon from our app too.
Call it from your stack
curl -X POST https://api.kit.forhosting.com/seo/heading-structure-check \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"headings":["h1","h2","h3","h2"]}'const res = await fetch("https://api.kit.forhosting.com/seo/heading-structure-check", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"headings": [
"h1",
"h2",
"h3",
"h2"
]
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/seo/heading-structure-check",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"headings": [
"h1",
"h2",
"h3",
"h2"
]
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/seo/heading-structure-check", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"headings":["h1","h2","h3","h2"]}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"headings":["h1","h2","h3","h2"]}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/seo/heading-structure-check", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)Example request
{
"headings": [
"h1",
"h2",
"h3",
"h2"
]
}Example response
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "seo.heading_structure_check",
"status": "queued",
"_links": {
"result": "/tasks/tsk_…/result"
}
}The API is asynchronous: the call returns a task_id immediately and the result arrives by webhook. Polling is capped at 1 req/s per task.
Pricing
Published price — no tokens, no invented credits. A failed task is never charged.
Errors
| HTTP | Code | Meaning |
|---|---|---|
401 | unauthorized | Missing or invalid API key. |
402 | insufficient_balance | Your balance doesn't cover the task price. |
404 | unknown_type | That task type doesn't exist. |
429 | rate_limited | Too many requests. Use the webhook instead of polling. |
Technical notes
Prepare the heading sequence in document order
Extract those details before making the request so the input remains an unambiguous list of tags.
Use the result in reviews and publishing gates
The heading count confirms how many tags were evaluated, and h1_count explains whether the top-level rule passed. Each skipped_levels entry points to the destination index where the invalid descent occurs, so an editor or build tool can locate the relevant heading without interpreting prose.
Technical questions
What happens when the heading list is empty?
The request returns an invalid_input error because an empty sequence contains no structure that can be validated.