How it works
This page explains the pattern so you can reuse it for your own “collect from many pages, show in one place” features.
This pattern works because a MyST build runs the document stage for all pages before running the project stage for any page.
See src/index.mjs for the full plugin code and snippets below for examples.
The shared stash¶
MyST loads a plugin module once per build, so anything at module scope persists across every page.
We define a plain Map at the top level of the module that we can add to.
// Term records keyed by source file, so re-building one page in `myst start`
// replaces that page's entry instead of appending duplicates.
const termsByFile = new Map();
A placeholder directive¶
The term-glossary directive emits an empty node at parse time.
There is nothing to aggregate yet, so it is just a marker the project-stage transform can find later.
const glossaryDirective = {
// `glossary` is a built-in MyST directive, so we use a distinct name.
name: 'term-glossary',
doc: 'Placeholder, replaced with the aggregated site-wide glossary.',
run() {
return [{ type: 'div', class: 'keyterm-glossary', children: [] }];
},
};
A document-stage transform collects from each page¶
stage: 'document' transforms run once per page.
Ours finds every keyterm node on the current page and adds it in the stash, keyed by that page’s file path.
const collectTransform = {
name: 'collect-keyterms',
doc: "Record this page's key terms in the shared Map.",
stage: 'document',
plugin: (_opts, utils) => (tree, vfile) => {
const source = pageName(vfile.path);
const terms = utils
.selectAll('admonition[class~="keyterm"]', tree)
.map((node) => ({ source, ...node.data }));
termsByFile.set(vfile.path, terms);
},
};
A project-stage transform fills in the placeholder using the stash¶
stage: 'project' transforms run after every page’s document stage.
Ours reads the now-complete stash, builds a definitionList, and replaces the placeholder’s children.
const buildTransform = {
name: 'build-glossary',
doc: 'Fill glossary placeholders with the aggregated definition list.',
stage: 'project',
plugin: (_opts, utils) => (tree) => {
const placeholders = utils.selectAll('div[class~="keyterm-glossary"]', tree);
if (!placeholders.length) return; // only the glossary page has one
const terms = [...termsByFile.values()].flat().sort((a, b) => a.term.localeCompare(b.term));
placeholders.forEach((node) => {
node.children = [glossaryList(terms)];
});
},
};
Adapting it for other use-cases¶
The same three pieces work for many other kinds of objects, here are a few examples
List of figures - collect figure captions in the document stage, render an index in the project stage.
Site-wide TODO list - a
tododirective collects items, atodo-listplaceholder aggregates them.Contributor index - collect author tags per page, list them on an “about” page.
Swap the directives and the shape of the collected data; the placeholder → collect → aggregate structure stays the same.