You spent hours writing a 2,500-word Squarespace post. You hit publish. A week later the average time on page is 38 seconds. Readers are bouncing before they reach the second heading. That’s not a traffic problem. It’s a navigation problem. A table of contents fixes it, and it improves your SEO in the process.
This tutorial covers two approaches:
- Free script: a copy-paste code snippet that builds a basic TOC at the top of your post. Good for simple blogs.
- Table of Contents Pro: a sticky sidebar TOC with scroll tracking, active section highlighting, and a collapsible mobile menu. One-time purchase, works on any Squarespace 7.1 site.
Start with the free version below. If you want the sidebar experience, skip to the Pro section.
Quick Copy-Paste Start
Paste the block below into Blog Post → Settings → Advanced → Page Header Code Injection if you want the TOC on every post, or place it inside a single Code Block on an individual post if you’d rather add it case-by-case.
<!-- Begin Table of Contents -->
<style>
.table-of-contents {
padding: 0 2rem 1rem;
border: 1px solid #3f342b;
border-radius: 16px;
margin: 2rem 0 1rem; /* top | left/right | bottom */
}
.table-of-contents h2 {
margin: 1rem 0;
}
.table-of-contents li {
line-height: 1.75;
/* Optional: add your own typeface
.table-of-contents li { font-family: 'YourFont'; } */
}
/* Prevent headings from hiding beneath a fixed header */
.blog-item-content h2,
.blog-item-content h3,
.blog-item-content h4 {
scroll-margin-top: 4rem; /* adjust if your header height differs */
}
@media screen and (max-width: 767px) {
.table-of-contents {
padding: 0 1rem 1rem;
margin: 1rem 0; /* simpler mobile spacing */
}
}
</style>
<script defer>
/* ---------- Helpers ---------- */
const slugify = (text) =>
text
.toLowerCase()
.trim()
.replace(/[^a-z0-9s-]/g, "")
.replace(/s+/g, "-")
.replace(/-+/g, "-");
/* Recursively turn headings into a tree */
function processNode(heading, level) {
const text = heading.textContent.trim();
const id = slugify(text);
heading.id = id;
const node = { text, id, level, children: [] };
let sibling = heading.nextElementSibling;
while (sibling) {
const match = sibling.tagName.match(/^H([2-4])$/);
if (match) {
const siblingLevel = +match[1];
if (siblingLevel <= level) break;
if (siblingLevel === level + 1) {
node.children.push(processNode(sibling, siblingLevel));
}
}
sibling = sibling.nextElementSibling;
}
return node;
}
function buildTree() {
const container = document.querySelector(".blog-item-content");
if (!container) return [];
return Array.from(container.querySelectorAll("h2")).map((h2) =>
processNode(h2, 2)
);
}
/* Render the tree into nested <ul> */
function renderTree(nodes) {
const ul = document.createElement("ul");
nodes.forEach((node) => {
const li = document.createElement("li");
const a = document.createElement("a");
a.href = "#" + node.id;
if (node.level === 2) {
const strong = document.createElement("strong");
strong.textContent = node.text;
a.appendChild(strong);
} else {
a.textContent = node.text;
}
li.appendChild(a);
if (node.children.length) li.appendChild(renderTree(node.children));
ul.appendChild(li);
});
return ul;
}
/* Inject the TOC once the DOM is ready */
function generateTOC() {
const tree = buildTree();
if (!tree.length) return;
const toc = document.createElement("nav");
toc.className = "table-of-contents";
toc.setAttribute("aria-label", "Table of Contents");
const title = document.createElement("h2");
title.textContent = "Table of Contents";
toc.appendChild(title);
toc.appendChild(renderTree(tree));
/* Place after hero banner if one exists; otherwise, top of article */
const banner = document.querySelector(
".blog-item-content-wrapper .image-block"
);
const target = banner
? banner // banner detected → put TOC after it
: document.querySelector(".blog-item-content-wrapper") || document.body; // no banner → top
target.insertAdjacentElement(banner ? "afterend" : "afterbegin", toc);
}
document.addEventListener("DOMContentLoaded", generateTOC);
</script>
<!-- End Table of Contents -->
(No JavaScript degree required. Just paste and publish.)
Two ways to inject the script
All blog posts (Blog Settings injection)
Where: Blog Settings → Advanced → Post Blog Item Code Injection (one-time setup)
Pros: Adds a TOC to every future and existing blog post automatically.
Cons: You’ll need to spot-check older posts to confirm the TOC lands in the right place (especially if layouts vary).
Single Code Block
Where: Add a Code Block at the very top (or anywhere) in a specific post.
Pros: Full control. Apply the TOC only where you want it.
Cons: Must remember to paste the snippet for each new post that needs it.
Tip: If you use the single-post method, the Code Block can sit anywhere in the layout, but placing it at the very top (just below the post title) keeps your editor organized.
Customization & Performance Tweaks
Choosing where the TOC appears
By default, the script looks for a banner image block at the very top of the post and inserts the TOC right after it. If your first image isn’t a hero banner, or your article starts with text, Squarespace still labels that first image as an .image-block, so the TOC could end up midway through the piece.
Force it to the very top if no banner exists
Replace the last lines of generateTOC() with:
const banner = document.querySelector(
".blog-item-content-wrapper .image-block"
);
const target = banner
? banner // banner detected → place TOC after it
: document.querySelector(".blog-item-content-wrapper") || document.body; // no banner → top
target.insertAdjacentElement(banner ? "afterend" : "afterbegin", toc);
This logic keeps the banner-first behavior but guarantees a top-of-article placement when no hero image exists.
Quick Tweaks at a Glance
- H2-only TOC – In the script, swap
/^H([2-4])$/➜/^H([2])$/. - Brand colors – Replace
#3f342bwith your site’s hex color. - Font-family – Add a line of CSS to use your own font for the TOC.
Want a Sticky Sidebar Instead?
The free script above drops a static list at the top of your post. It works, but it disappears as readers scroll. If your posts are long enough to need a TOC, they’re long enough to need one that follows the reader.
Table of Contents Pro sits in a sticky sidebar next to your content. As readers scroll, the active section highlights in the TOC. On mobile, it becomes a collapsible bar that slides in from the top.
Free vs. Pro at a Glance
| Free Script | TOC Pro ($38) | |
|---|---|---|
| Position | Top of post (static) | Sticky sidebar |
| Scroll tracking | No | Active section highlights as you scroll |
| Mobile | Same static list | Collapsible bar with tap-to-expand |
| Install | Paste ~130 lines of code | Paste one <script> tag |
| Custom colors | Edit CSS manually | Built-in configurator, pick your colors |
| Updates | Manual (re-paste if we improve it) | Automatic (hosted on our CDN) |
| Style options | One style | Card or minimal |
| Heading levels | H2-H4 | Configurable (H2, H3, H4) |
See it live: card style on Brentwood Therapy Collective · minimal style on Dr. Navvab Tadjvar
One script tag. Sticky sidebar. Scroll tracking.
Paste it once into Code Injection and your TOC is live. Custom colors, mobile support, and automatic updates included.
Get Table of Contents Pro — $38 →Troubleshooting & FAQ
TOC doesn’t appear
Make sure the code is in Page Header Injection and your post has at least one H2. Squarespace sometimes strips tags in Code Blocks. Use Post Settings instead.
Anchor lands too high / low
Tweak scroll-margin-top (currently 4rem) until the heading sits just below your fixed header.
Duplicate IDs warning
Rename repeated headings (“Introduction 2”) to keep slugs unique.
Styling & Branding Tips
- Typography – add your own font with one CSS line, e.g.
.table-of-contents li { font-family: 'YourFont'; } - Borders vs. shadows – prefer a soft shadow (
box-shadow: 0 2px 8px rgba(0,0,0,.06)) if your palette is minimal. - Collapsible sections – wrap the entire TOC in
<details>if you want it hidden by default on mobile.
SEO & UX Wins
Anchor links can become Google sitelinks
When your TOC includes anchor links, Google can display them directly in search results as “jump to” sitelinks. Instead of a single blue link, your result expands with clickable sub-sections. More real estate on the results page, higher click-through rate. Sitelinks aren’t guaranteed, but anchor links make you far more likely to get them. Backlinko’s sitelinks guide covers how these work in detail.
Better engagement signals
Readers who can jump to the section they need are less likely to hit the back button. That translates to a lower bounce rate and longer time on page, both engagement signals that Google tracks. Screen-reader users also gain quick landmarks for navigating your content, improving accessibility across the board.
When you need one (and when you don’t)
Not every post needs a TOC. A 400-word announcement doesn’t benefit from one. Here’s a simple rule:
- Add a TOC if your post has three or more H2 sections, or exceeds 1,000 words.
- Skip it for short updates, news posts, or anything that fits on a single screen.
If you’re writing content meant to rank, you almost certainly need a TOC.