Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

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.

index.mjs
// 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.

index.mjs
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.

index.mjs
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.

index.mjs
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

Swap the directives and the shape of the collected data; the placeholder → collect → aggregate structure stays the same.