# A table of contents is a retrieval map: why long articles need anchor links up top

The Mega Analyzer warns when an 800-word article has no list of anchor links near the top. What the check reads, why engines and screen readers care, and the fix.

Author: J.A. Watte
Published: September 21, 2026
Source: https://jwatte.com/blog/blog-mega-analyzer-table-of-contents/

---

The Mega Analyzer row **Long-form article has a Table of Contents** looks like a formatting nitpick. It isn't. What it measures is whether a long page is addressable in pieces: whether there are section ids to point at, and a list near the top that names each section and links to it. Without both, an engine, a screen reader, or a person on a phone gets one 2,000-word blob with a single entry point at the top.

## What the check actually tests

The row carries the same title whether it passes or fails, and its severity is warn. On a pass the detail is empty. On a fail it reads: "Articles ≥ 800 words benefit from a TOC (`<ol>` of internal anchor links near the top). Helps voice assistants + AI retrieval select sections to cite; also helps humans." It sits with the crawl-validation rows (the same set that reads robots.txt, sitemaps and canonicals), and like every warn-level row in that set it is worth three points of the SEO score.

It is gated, and the gate matters more than the heuristic. The row only fires when all three of these are true:

- **800 or more words** in the body, counted after the analyzer strips nav, header, footer, aside, forms, scripts and the usual cookie and legal containers.
- **The page looks like an article**: Article, BlogPosting or NewsArticle in the JSON-LD, or a URL path containing /blog/, /article/, /articles/, /post/, /posts/, /news/, /guide/ or /guides/, or, failing both, five or more H2s with 1,500 or more words and fewer than ten images.
- **The page does not look like a landing or hub page**: not the site root, not WebSite plus ItemList or CollectionPage in the schema, and not ten or more images alongside three or more H2s.

If any one of those is false, the row does not appear at all: a 3,000-word home page never sees it, and neither does a 700-word post.

When the gate opens, the TOC heuristic itself is small. The analyzer takes the first four `<ol>` or `<ul>` elements in document order (on the whole document, not the stripped body) and passes the page if any one of them contains at least three `a[href^="#"]` links and those links make up at least 70 percent of that list's `<li>` items.

Which means these do not count, even though a reader would call each of them a table of contents:

- **A TOC that JavaScript builds after load.** The analyzer reads the fetched HTML; a `<div id="toc"></div>` that a plugin fills in the browser is an empty div to it.
- **A TOC built from `<div>` or `<p>` elements** instead of list items.
- **A TOC that links with full URLs** (`https://example.com/post/#section`) instead of `#section`. The selector is `href^="#"`, literally "starts with a hash".
- **A TOC that sits after the fourth list.** This is the one that catches well-built sites: a header menu `<ul>`, a nested dropdown `<ul>` inside it, a breadcrumb `<ol>` and a share-button `<ul>` are four lists before the article has started, and nested lists count separately.
- **A list that mixes anchors with other links.** Three section anchors plus two tag links and an author link is 50 percent, below the bar.

Note also what the check does not read: heading ids. It never confirms that `#why-it-matters` points at an element that exists. A TOC of dead anchors passes. That is why the fix section below starts with the headings, not the list.

## Why it matters

Google has been turning in-page anchors into result-page links since 2009. The Search Central post announcing the feature says the section links are generated "completely algorithmically, based on page structure," and gives site owners two instructions: give each section "an associated anchor with a descriptive name" and include "a table of contents which links to the individual anchors." The current sitelinks documentation says sitelinks are automated and asks for headings that are "informative, relevant, and compact" and internal anchor text that is "concise and relevant to the page they're pointing to." A TOC whose link text matches the headings covers both in one block of markup.

In October 2020 Google described ranking "passages in addition to the relevancy of the overall page" and put the effect at 7 percent of queries across all languages. The announcement asks nothing of the page; the passage is found on Google's side. But a section id gives Google a URL that lands the user on that passage, and a TOC entry gives it a label for it. Section boundaries and section names are exactly what a passage system has to infer when you don't supply them.

Answer engines work the same way with less patience: they chunk a page before ranking anything, and they cite the chunk. The [passage retrieval post](/blog/blog-passage-retrieval-seo/) walks through the mechanics. The short version is that a list of anchor links at the top of a page is a machine-readable summary of what the page covers, sitting in the first few hundred bytes of body text, before any chunking happens. And when an engine does cite, a fragment URL lets the citation land on the section rather than the top of the page. Text fragment links (the `#:~:text=` syntax in the WICG draft) can do that without anchors, but they break the moment you reword the sentence they quote. An id survives edits to the prose.

For readers on phones this is arithmetic, not research: at roughly 140 words per screen, a 2,000-word article is fourteen or fifteen screens. People scan the list, pick the section, jump. Google's SEO starter guide asks for the same thing in plainer words: "Break up long content into paragraphs and sections, and provide headings to help users navigate your pages."

For keyboard and screen-reader users, WCAG 2.4.1 Bypass Blocks (Level A) lists "Adding links at the top of the page to each area of the content" (technique G124) as a sufficient technique, alongside skip links and ARIA landmarks. An in-page TOC marked up as a `<nav>` is that technique, and the nav landmark lets a screen reader user reach it from the landmarks list. WCAG 2.4.5 Multiple Ways (Level AA) is a looser fit: it is about locating a page within a set of pages, and its "Providing a Table of Contents" technique (G64) describes links to the sections of one document, whether those sections sit on one page or across several, and calls the list "particularly useful when a document is divided into multiple web pages." A per-article TOC helps at the margin rather than satisfying the criterion on its own.

One honesty note. The same starter guide says heading order does not matter to Search and there is "no magical, ideal amount of headings." A TOC is not a ranking lever. It is an addressability lever, and it happens to help three audiences at once.

## How to fix it

**1. Put a stable id on every H2 (and any H3 you plan to list).** Derive it from the heading text as a kebab-case slug so it never changes unless the heading does. Static generators do this with one plugin: markdown-it-anchor on Eleventy and other markdown-it sites, rehype-slug where the pipeline is unified (Next with MDX, for example). On WordPress, choose a TOC block or plugin that writes the id onto the heading element itself. Some inject an empty `<span id="...">` before the heading instead, which works for scrolling but leaves the heading unaddressed for anything reading the outline. The output you want is this:

```html
<h2 id="what-the-check-actually-tests">What the check actually tests</h2>
```

**2. Emit the TOC in the HTML, immediately after the intro paragraph.** Not from client-side JavaScript. Keep it an `<ol>` or `<ul>` whose items are only links, use fragment hrefs, and make sure it is one of the first four lists in the document. The shape:

```html
<nav class="toc" aria-label="Table of contents">
  <ol>
    <li><a href="#what-the-check-actually-tests">What the check actually tests</a></li>
    <li><a href="#why-it-matters">Why it matters</a></li>
    <li><a href="#how-to-fix-it">How to fix it</a></li>
    <li><a href="#when-to-leave-it-alone">When to leave it alone</a></li>
  </ol>
</nav>
```

The `<nav>` is the element the HTML spec defines for "a section of a page that links to other pages or to parts within the page," and the `aria-label` distinguishes it from the site menu in a screen reader's landmark list. If your header menu, a dropdown and a breadcrumb already use three lists, either move the share-button or tag list below the article or render those as inline links inside a `<p>` or `<div>` so they stop consuming list slots.

The [Headings Outline Audit](/tools/headings-outline/) does the tedious part. Give it the URL and it renders the H1 through H6 outline, flags a missing or duplicate H1 and skipped levels, and emits a paste-ready Markdown contents block built from your H2s and H3s:

```markdown
- [What the check actually tests](#what-the-check-actually-tests)
- [Why it matters](#why-it-matters)
  - [Readers on phones](#readers-on-phones)
```

That renders as a nested `<ul>` of fragment links, which is what the heuristic wants. When a heading already carries an id the tool uses it; otherwise it slugs the text, which assumes you finished step 1.

**3. Match link text to heading text word for word.** Then the list reads as a summary of the page as well as a menu, and it satisfies the sitelinks guidance about anchor text.

**4. Optional polish, still static HTML.** Wrap the nav in `<details><summary>Contents</summary>` if you want it collapsible on mobile; the list is still in the HTML, so the check still sees it. Add `scroll-margin-top` to headings so a sticky header does not cover the target when the browser scrolls to the fragment:

```css
h2[id], h3[id] { scroll-margin-top: 5rem; }
```

If you publish `speakable` markup (Google's documentation says it is in beta and built for news queries on Google Assistant), point its `cssSelector` at the same section ids so the sections you name in the TOC are the sections you offer for audio.

**5. Verify.** Re-run the [Mega Analyzer](/tools/mega-analyzer/) and confirm the row has left the warn list. Then, after Google's next crawl, search for the article by title and look for section links under the result. They only appear for queries where Google judges a section link useful, so absence proves nothing; presence tells you the anchors were read.

## When to leave it alone

The check already skips landing pages, hub pages and the home page by design, so the exceptions here are the article-shaped pages where a TOC would be noise.

- **A long piece with two or three sections.** A TOC with two entries is a formality. The heuristic needs three anchors anyway, so a two-section article cannot pass it; the honest response is to accept the warn, or split the piece if it really is 1,200 words under two headings.
- **A narrative meant to be read in order.** An essay or a case study with a reveal does not want readers jumping to the last section. Give the headings ids regardless (citations and jump links still benefit) and skip the visible list.
- **A transcript or a Q&A** where the sections are turns of conversation. A list of forty anchors is worse than no list. Question-and-answer pages are usually better served by FAQPage markup and a `<details>` per question.
- **Platforms that only offer a JavaScript TOC widget.** If your builder cannot emit the list server-side, the analyzer will keep warning, and there is no honest way to silence it short of hand-pasting the list into the post body. Do that for your five longest articles and let the rest ride.

If you are building the blog yourself on a static generator, the anchor plugin is a five-minute change and the TOC is a layout partial. The $97 Launch covers the rest of that stack for a solo builder who wants the whole site to work this way from the first post.

## Fact-check notes and sources

- **Source**: https://developers.google.com/search/blog/2009/09/using-named-anchors-to-identify establishes that Google generates in-result section links "completely algorithmically, based on page structure," and advises descriptive named anchors plus a table of contents linking to them (Search Central Blog, September 25, 2009).
- **Source**: https://developers.google.com/search/docs/appearance/sitelinks establishes that sitelinks are automated and that Google recommends informative, compact headings and concise, relevant internal anchor text.
- **Source**: https://blog.google/products/search/search-on/ establishes passage ranking ("passages in addition to the relevancy of the overall page") and the 7 percent of queries figure (October 15, 2020).
- **Source**: https://developers.google.com/search/docs/fundamentals/seo-starter-guide establishes the "provide headings to help users navigate your pages" guidance, and that heading order and count are not Search ranking signals.
- **Source**: https://developers.google.com/search/docs/crawling-indexing/links-crawlable establishes that Google follows `<a>` elements with an `href` and that anchor text should be descriptive, concise and relevant.
- **Source**: https://www.w3.org/WAI/WCAG22/Understanding/bypass-blocks.html establishes SC 2.4.1 (Level A) and lists G124, "Adding links at the top of the page to each area of the content," as a sufficient technique.
- **Source**: https://www.w3.org/WAI/WCAG22/Understanding/multiple-ways.html establishes SC 2.4.5 (Level AA), its scope (locating a page within a set of pages) and G64, "Providing a Table of Contents," as one of its listed techniques; the G64 text at https://www.w3.org/WAI/WCAG22/Techniques/general/G64 says a table of contents links to sections of the same document, on one page or divided across several.
- **Source**: https://html.spec.whatwg.org/multipage/sections.html#the-nav-element establishes the definition of `<nav>` as a section that links to other pages or to parts within the page.
- **Source**: https://html.spec.whatwg.org/multipage/browsing-the-web.html#scroll-to-the-fragment-identifier establishes how a browser resolves a fragment: the first element whose id matches, then a legacy `<a name>`; an empty fragment or `#top` means the top of the document, and an unmatched fragment scrolls nowhere.
- **Source**: https://wicg.github.io/scroll-to-text-fragment/ establishes the `#:~:text=` text fragment syntax as a WICG draft rather than a W3C Recommendation.
- **Source**: https://developers.google.com/search/docs/appearance/structured-data/speakable establishes that speakable is in beta, targets news queries on Google Assistant, and addresses content through `cssSelector` or `xPath`.

## Related reading

- [Passage Retrieval Is The New SEO](/blog/blog-passage-retrieval-seo/)
- [RAG Splits Your Page Into Chunks: How To Make Each Chunk Retrievable](/blog/blog-tool-chunk-retrievability/)
- [Generative Engine Optimization: Why Traditional SEO Is No Longer Enough](/blog/blog-generative-engine-optimization/)
- [Heading Outline Audits: Catching the H2-to-H4 Skip](/blog/blog-tool-headings-outline/)
- [What Topics Your Competitors Cover That You Don't: Heading Gap Audit](/blog/blog-tool-heading-gap-audit/)

*This post is informational, not legal advice. Mentions of third parties are nominative fair use. No affiliation is implied.*


---

Canonical HTML: https://jwatte.com/blog/blog-mega-analyzer-table-of-contents/
RSS: https://jwatte.com/feed.xml
JSON Feed: https://jwatte.com/feed.json
Hero image: https://jwatte.com/images/blog-mega-analyzer-table-of-contents.webp
