Build a glossary that auto-links its terms
Hyperlink Northwind's defined terms throughout a Doc to their glossary anchors.
Published Nov 2, 2025
Northwind’s longer documents — onboarding packs, proposals, process handbooks — all carry their own jargon. The terms get defined once in a glossary, but nobody links every mention back to that definition by hand, so readers either already know the term or scroll hunting for it.
This script does the linking for you. It reads a glossary of terms from a Sheet, scans a Google Doc for every occurrence of each term, and turns each one into a hyperlink pointing at that term’s anchor in the glossary section. Update the glossary sheet, re-run it, and the whole document re-links itself.
What you’ll need
- A
Glossarysheet with two columns and a header row:termanddefinition. - The glossary sheet’s file ID, copied from its URL, set as
GLOSSARY_IDin the config block. - A Google Doc whose glossary section uses bookmark-style anchors named
glossary-<slug>— for example a term “Retainer” anchors at#glossary-retainer. The slug must match whatslug()produces. - The file ID of the Doc you want to link, passed to
autoLinkGlossarywhen you run it.
The script
// The Sheet holding the glossary terms and definitions.
const GLOSSARY_ID = '1abcGlossaryId';
/**
* Scans a Doc and hyperlinks every occurrence of each glossary term
* to its anchor in the glossary section.
* @param {string} docId - The Google Doc to link.
*/
function autoLinkGlossary(docId) {
// 1. Load the glossary terms from the Sheet.
const terms = readSheet(GLOSSARY_ID);
if (!terms.length) {
Logger.log('Glossary is empty — nothing to link.');
return;
}
// 2. Open the target document.
const doc = DocumentApp.openById(docId);
const body = doc.getBody();
// 3. Walk each term and link every whole-word match in the body.
for (const t of terms) {
if (!t.term) continue;
// \b…\b matches the term as a whole word, not inside another word.
let found = body.findText('\\b' + t.term + '\\b');
while (found) {
const el = found.getElement();
el.asText().setLinkUrl(
found.getStartOffset(),
found.getEndOffsetInclusive(),
'#glossary-' + slug(t.term));
// Resume the search from where this match ended.
found = body.findText('\\b' + t.term + '\\b', found);
}
}
doc.saveAndClose();
Logger.log('Linked ' + terms.length + ' glossary terms.');
}
/**
* Turns a term into a URL-safe slug for its anchor.
* @param {string} s - The term.
* @returns {string} A lower-case, hyphenated slug.
*/
function slug(s) {
return s.toLowerCase().replace(/\W+/g, '-');
}
/**
* Reads the first sheet of a spreadsheet into an array of row objects
* keyed by the header row.
* @param {string} id - The spreadsheet file ID.
* @returns {Array<Object>} One object per data row.
*/
function readSheet(id) {
const [header, ...rows] = SpreadsheetApp.openById(id)
.getSheets()[0]
.getDataRange()
.getValues();
return rows.map((r) =>
Object.fromEntries(header.map((k, i) => [k, r[i]])));
}
How it works
autoLinkGlossarycallsreadSheetto load the glossary, which returns one object per row keyed by the header — so each term is{ term, definition }. If the glossary is empty it logs and stops.- It opens the target Doc by ID and grabs the body element, the searchable container for all the document’s text.
- For each term it calls
body.findTextwith a\b…\bregex. The word boundaries stop “art” from matching inside “start” — only whole-word hits are linked. - The
whileloop walks every match of that term. For each one it reads the text element and offsets, thensetLinkUrlturns just that span into a hyperlink pointing at#glossary-<slug>. - Passing the previous
foundresult back intofindTextresumes the search from where the last match ended, so it sweeps the whole document. sluglower-cases the term and replaces any run of non-word characters with a hyphen, producing the same anchor name the glossary section uses.saveAndClosewrites the links back to the document.
Example run
Say the Glossary sheet holds:
| term | definition |
|---|---|
| Retainer | An ongoing monthly engagement. |
| Scope creep | Work added beyond the agreed brief. |
And the Doc contains the sentence:
Every Retainer protects against scope creep by fixing the brief up front.
After a run, “Retainer” becomes a link to #glossary-retainer and “scope
creep” a link to #glossary-scope-creep. The plain text is unchanged — only
the link styling and the click target are added.
Run it
This is an on-demand job — run it whenever a document is finished or the glossary changes:
- In the Apps Script editor, paste the target Doc’s ID into
autoLinkGlossary— call it from a wrapper, or temporarily hard-code an ID to test. - Select the function and click Run.
- Approve the authorisation prompt the first time.
- Open the Doc and check the linked terms.
Watch out for
- Matching is case-sensitive.
findTextwith\bRetainer\bwill not catch “retainer” in lower case. List each casing you expect in the glossary, or normalise the term before searching. - Re-running re-links everything, including terms already linked. That is harmless — it just overwrites the same link — but it is not incremental.
- Linking happens in glossary order. If one term is a substring of another (for example “Scope” and “Scope creep”), put the longer term first so the shorter one does not grab part of it.
- The glossary anchors must already exist in the Doc. This script only creates
the links; if
#glossary-retaineris not a real anchor, the link will be dead. Build the glossary section with matching bookmark names first. findTextregex support is limited compared with JavaScript’s. Stick to simple patterns —\band literal text are reliable, anything fancier may silently fail to match.
Related
Generate personalized study guides from notes
Reformat raw notes into structured study guides — for Northwind's internal training programme.
Updated Feb 8, 2026
Build a contract-clause assembly system
Construct Northwind agreements from a library of approved clauses — drag-drop in code.
Updated Feb 1, 2026
Translate and resolve Doc comments
Localise reviewer feedback on a shared Doc so multilingual teams can collaborate.
Updated Jan 25, 2026
Auto-archive finalized Docs to dated folders
File completed Northwind Docs by month so the active folder stays focused on in-flight work.
Updated Jan 18, 2026
Build a fillable intake form inside a Doc
Create structured intake forms with placeholder fields readers can fill — for client briefs.
Updated Jan 11, 2026