All 14 Table of Contents sections

See every section

What goes in the list

Updated Table of Contents 1.8.1Tested with WordPress 7.1

Heading levels decides which of H1–H6 are collected. H2, H3 and H4 are on by default.

H1 is off on purpose. On a well-built theme the only H1 on the page is the post title, and a contents list whose first entry is the title of the page you are already reading tells nobody anything.

Minimum headings is the threshold. Below it, no list is drawn — three links above a post with three sections is furniture, not navigation. The headings still get their anchors, so links into the post keep working.

Exclude headings by text takes one heading per line, and stands for anything: Step leaves out every numbered step at once. Exclude headings by CSS class does the same by CSS class, which is the easier way to deal with headings your theme prints inside related-post blocks and widgets.

To leave out one heading in the block editor, select it and switch on Leave out of the table of contents in the Table of Contents panel of its sidebar. That adds the class trtc-exclude to the heading, and a heading with that class is always left out, whatever Settings says — so it stays out when you rename it. In the classic editor, give the heading that class by hand.

Anchors#

Every heading in the list gets an id, generated the way WordPress generates post slugs: lowercase, accents removed, punctuation replaced with hyphens. Two headings with the same wording get -2 and -3, so an anchor is always unique on the page.

A heading that already has an id keeps it. That id is somebody's shared link — from another page, from an email, from a search result — and rewriting it would break every one of them. Switch that off only if you know nothing points at your existing anchors: the old id is then replaced by the generated one.

A generated id never takes one the page already uses. The comment form's #email and #comments, WooCommerce's #reviews and the landmarks themes print, such as #content and #main, are set aside, so a heading called "Email" gets #email-2 instead of taking the comment form's label away from its field. An id you put on a heading yourself is kept as it is. Developers can add to the list with the trtc_reserved_ids filter.

Keep old heading links working is the answer to the problem the paragraph above only half solves. An anchor is made from a heading's words, so editing the words changes the anchor, and every link anybody had already shared to it stops arriving anywhere. You cannot put an explicit id on a heading in advance, because you do not know which link somebody shared until after you have broken it.

With this on, each save writes down the post's anchors. The next time you save, any anchor that has gone is compared with the headings that are there now, and if one of them is recognisably the same heading under a new name, the old anchor is kept as a second way of reaching it — an empty target printed beside the heading, so an old link lands exactly where it used to.

It will not guess. A heading rewritten from scratch is a different heading, and sending an old link to it would put the reader somewhere they were not going, which is worse than the broken link they have now. Where two headings could each plausibly be the rename, neither is chosen. Rename a heading twice and both old names follow it; rename it back and nothing is left over. A post keeps its last twenty-five old anchors.

Turning it off stops new ones being recorded and stops the ones already recorded being printed.

Anchor prefix puts something in front of every generated id, for the rare theme that already uses common ones like #introduction.

Posts split into pages#

A post split into pages with the Page Break block (the <!--nextpage--> tag in the classic editor) is shown one page at a time. Settings → On posts split into pages decides what the list on each page holds:

  • List every page's headings on each page, the default on a new site. Every page shows one list for the whole post. An entry for a heading on the page being read scrolls to it as usual. An entry for a heading on another page opens that page at the heading, below your sticky header, and carries a small Page 2 mark, which a screen reader hears as "on page 2". The minimum number of headings is counted over the whole post, so a post of three pages with two headings each gets a list on every page.
  • List only this page's headings. Each page lists its own headings, and a page with fewer than the minimum has no list. This is how split posts were always listed, and a site that had the plugin before keeps it until somebody changes it. The Dashboard offers the change once, naming how many published split posts it affects, with a button to switch it on and one to say no thanks. It counts the post types your Placement rules put the automatic list on, and makes no offer while automatic placement is off, since the setting only changes the automatic list.

Worth knowing:

  • Every page keeps exactly the heading links it had. Each page is its own document, so the same words on two pages each get the plain link on their own page, and the entry for each points at the right page.
  • The other pages are read without running their shortcodes. A shortcode on another page can send a form, count a view or load a video player, and none of that should happen because somebody opened a different page. So a heading a shortcode prints on another page is not listed. A section a layout shortcode is wrapped round is listed as before, such as a Shortcodes Ultimate box, note, spoiler, tabs, accordion, row or column, or a WPBakery row, column, text block, section, tab or toggle. A section any other shortcode is wrapped round is not listed, since it may be a membership plugin's shortcode round a members-only section: it is not listed for anybody, members included, though it shows on its own page as usual. A shortcode in a heading's own words, such as a year, is run, so the entry reads as its page does. A developer can name their theme's own layout shortcodes with the trtc_split_layout_shortcodes filter.
  • The other pages' blocks are drawn to find their headings, as they always were. So a form, a player or a counter placed as a block on another page is drawn whenever the pages are read (see the next point), and a form's count of views can go up by one then. The scripts and stylesheets such a block asks WordPress to load are taken back off the page being read.
  • The other pages are read on a signed-out visitor's view and kept with the post for the next visitor. That includes a visitor shown a membership plugin's preview: what is kept is the pages as you saved them, not the preview, and that visitor is given none of it. They are read again after you save the post, after you save a synced pattern it uses, and after an hour whatever happens, so a block that shows something new without being saved reaches the list within the hour. Somebody signed in is given that same list while there is one for the post as it is saved. So a section a block shows only to members is not in their list, though they see it on its page, and a heading a block shows only to visitors, such as a sign-up section, can be. Until a visitor has opened the post since it was last saved, the pages are read for the signed-in reader, sections only they are shown included. A site with a persistent object cache, such as Redis, keeps that reading for them for an hour. Without one, the pages' blocks are read again on each of their views, which on a site whose readers are all signed in, such as a members-only site, is every view. What is read for somebody signed in is never kept for visitors. Visitors who are not signed in are all treated alike: a section a site shows only to some of them, by country or by a cookie, can be listed for all of them.
  • The pages are read only on an ordinary page view. When a form on the page is sent, that one answer lists only the page's own headings if nothing has been kept yet.
  • Only the post's own page counts as the page. A plugin that inserts a reusable piece of content with a shortcode or a block, such as a content block, runs WordPress's content filter over that piece. A piece inserted in the post is listed as part of it, and one in a template column before or after the post is left alone. Neither is taken for the page. The page is told by its own words, so a page made only of one synced pattern has nothing to tell it by: there a piece inserted beside it can be taken for the page, and the lists keep to that page.
  • The list names the other pages' headings only when the page being read reached the reader as it is written: at least half its headings and most of its words. It is compared with the page as you saved it, what its shortcodes are wrapped round included, and with the headings its blocks draw, such as a synced pattern's. Where a membership plugin shows a preview in the page's place, before this plugin's filter or after it, no list in the post names the other pages' headings. The automatic list, and a [toc] or block placed in the post, even one the preview keeps from above the More tag, are drawn again from the headings the preview shows, or left out where it shows too few. The sidebar copy and a [toc] or block after the article name none of the other pages' headings either. The same happens on a page whose theme leaves out most of its headings, where a block hides most of its words, and, for a reader not shown it, where most of the page sits inside a shortcode that shows it only to some readers.
  • Where every list keeps to the page being read, with List only this page's headings or the trtc_split_whole_post filter, a list a preview keeps names only the headings the preview shows, not those below the More tag.
  • What it cannot catch: a list placed before the article, such as the block in a template column before the post or in a sidebar printed first, is drawn before the page is, so it names every page as read for that reader. A preview that repeats the page's own words, such as a short opening page shown in full, passes for the page. So does a preview that leaves out only paragraphs a block draws, with no heading among them: the words a block draws are not compared, only its headings. A page with no words and no headings of its own, written or drawn, has nothing to check. Where that matters, the trtc_split_whole_post filter keeps every list to the page being read: return false for the posts to protect.
  • The other side of it: a page whose headings are mostly drawn by a block that shows different ones from view to view, such as a block of random products, can look like a preview, and then lists only its own headings.
  • Every list takes the page being read's own entries from the page as the reader got it. A [toc] or block placed in the post is drawn again once the page has reached the reader, so a heading a shortcode prints or wraps on its own page is in it, as in the sidebar copy and a list after the article.
  • The section highlight follows only the page being read. A copied link to a heading on another page is that page's address.
  • The Contents (sidebar) block and widget copy the post's list, so they show what it shows: where the page turned out to be a preview, only the headings the preview shows, or nothing. A list placed with [toc] or the block always listed the whole post; its entries for other pages now open those pages, whichever option is chosen. Every page of the post gets the heading links such a list needs: the heading levels it asks for, even ones Settings leaves out, and on pages the Placement rules keep the automatic list off.
  • A post behind a password lists none of its headings until the visitor has given the password, on any of its pages, and neither does [toc] or the block placed outside it, in a sidebar or a template.
  • The post's Table of Contents box counts page by page: "6 headings across 3 pages" with every page's headings, or which pages get a list with only their own. It counts pages made with the Page Break block; a post another plugin splits some other way is counted, and listed, as one page.

Still stuck? Ask the developers who wrote it. We aim to reply within one business day.