If you work with long Google Docs—guides, reports, lesson plans, ebooks, or AI-generated drafts—navigation quickly becomes painful.
Headings exist… but readers still scroll endlessly.
In this post, we’ll build a smart Apps Script that:
✅ Scans all headings in a document
✅ Automatically generates a clickable Table of Contents
✅ Inserts it at the top of the document
✅ Updates it anytime with one click
✅ Avoids duplicates and broken formatting
No add-ons. No external APIs. Just clean, reliable Apps Script.
What We’re Building
A custom menu in Google Docs:
Doc Tools
├─ Insert / Update Table of Contents
└─ Remove Existing Table of Contents
The script:
- Finds all H1–H4 headings
- Builds internal anchor links
- Creates a clean, readable TOC
- Replaces the old one safely if it already exists
Why This Is Useful
This solves a real, recurring problem:
- AI-generated documents often lack structure
- Built-in Docs TOC is limited and fragile
- Writers want control + automation
- Editors need TOCs updated repeatedly
This script gives you a foundation you can customize:
- numbering
- indentation by heading level
- styling
- selective heading inclusion
Full Working Source Code
1️⃣ Add the custom menu
function onOpen() {
DocumentApp.getUi()
.createMenu('Doc Tools')
.addItem('Insert / Update Table of Contents', 'insertOrUpdateTOC')
.addItem('Remove Existing Table of Contents', 'removeTOC')
.addToUi();
}
2️⃣ Insert or update the Table of Contents
function insertOrUpdateTOC() {
const doc = DocumentApp.getActiveDocument();
const body = doc.getBody();
removeTOC(); // ensure no duplicates
const headings = collectHeadings_(body);
if (headings.length === 0) {
DocumentApp.getUi().alert('No headings found.');
return;
}
const tocHeader = body.insertParagraph(0, 'Table of Contents');
tocHeader.setHeading(DocumentApp.ParagraphHeading.HEADING1);
let insertIndex = 1;
headings.forEach(h => {
const p = body.insertParagraph(insertIndex++, h.text);
p.setIndentStart(h.indent);
p.setLinkUrl('#' + h.anchor);
});
DocumentApp.getUi().alert('✅ Table of Contents inserted.');
}
3️⃣ Collect headings and create anchors
function collectHeadings_(body) {
const headings = [];
for (let i = 0; i < body.getNumChildren(); i++) {
const el = body.getChild(i);
if (el.getType() !== DocumentApp.ElementType.PARAGRAPH) continue;
const p = el.asParagraph();
const heading = p.getHeading();
if (!isSupportedHeading_(heading)) continue;
const text = p.getText().trim();
if (!text) continue;
const anchor = createAnchor_(text, i);
p.setLinkUrl('#' + anchor);
headings.push({
text,
anchor,
indent: headingIndent_(heading)
});
}
return headings;
}
4️⃣ Helpers: heading detection & formatting
function isSupportedHeading_(heading) {
return [
DocumentApp.ParagraphHeading.HEADING1,
DocumentApp.ParagraphHeading.HEADING2,
DocumentApp.ParagraphHeading.HEADING3,
DocumentApp.ParagraphHeading.HEADING4
].includes(heading);
}
function headingIndent_(heading) {
switch (heading) {
case DocumentApp.ParagraphHeading.HEADING2: return 18;
case DocumentApp.ParagraphHeading.HEADING3: return 36;
case DocumentApp.ParagraphHeading.HEADING4: return 54;
default: return 0;
}
}
function createAnchor_(text, index) {
return text
.toLowerCase()
.replace(/[^\w]+/g, '-') +
'-' + index;
}
5️⃣ Remove the existing TOC safely
function removeTOC() {
const body = DocumentApp.getActiveDocument().getBody();
for (let i = body.getNumChildren() - 1; i >= 0; i--) {
const el = body.getChild(i);
if (el.getType() !== DocumentApp.ElementType.PARAGRAPH) continue;
const p = el.asParagraph();
const text = p.getText().trim();
if (text === 'Table of Contents') {
safeRemove_(body, el);
continue;
}
if (p.getLinkUrl() && p.getLinkUrl().startsWith('#')) {
safeRemove_(body, el);
}
}
}
function safeRemove_(body, el) {
if (body.getNumChildren() === 1) {
el.asParagraph().setText(' ');
return;
}
body.removeChild(el);
}
How This Works (Conceptually)
- Scan document structure
- Only paragraphs
- Only heading styles
- Generate anchors
- Predictable
- Collision-safe
- Link TOC entries
- Internal
#anchorlinks
- Internal
- Indent visually
- Mirrors document hierarchy
- Rebuild safely
- No duplicates
- No “last paragraph” errors
How Readers Can Extend This
Encourage readers to build their own version:
- Add numbering:
1.2.3 - Limit to H2+ only
- Style TOC entries
- Add page-break before TOC
- Convert to a sidebar tool
- Bundle as a Workspace add-on
- Pair with AI-generated outlines