SPHINXEXTERNALTOC(1)

πŸ“¦ version 1.0.1, in debian trixie, ubuntu resolute, ubuntu stonking

✨ NAME ✨

sphinxexternaltoc β€” Sphinx External ToC πŸ’–

Okay bestie, so this is a sphinx extension that lets you define your whole documentation site-map (aka the Table of Contents, the ToC, the whole vibe) outside of your documentation files. Like, in one place. And yes β€” this is literally what Jupyter Book uses! πŸŽ€

In regular ol' Sphinx, you build your site-map bottom-up: you sprinkle toctree directives inside your pages and hope for the best. πŸ™ˆ

This extension? It lets you go top-down instead, defining the entire site-map structure inside a single YAML file. Slay. πŸ’…

[image: ToC graphic] [image] Example ToC.

It also lets documents that aren't specified in the ToC get auto-excluded. Not me being efficient for once 😌


🌸 SPHINX 🌸

πŸ’– Configuration πŸ’–

Add this to your conf.py:

extensions = ["sphinx_external_toc"]
external_toc_path = "_toc.yml"  # optional, default: _toc.yml
external_toc_exclude_missing = False  # optional, default: False

Heads up though: external_toc_path is always read as a Unix path, and you can either give it relative to the source directory (which is what we recommend, bestie 🫢) or as an absolute path.

πŸ¦‹ Basic Structure πŸ¦‹

A minimal ToC just defines the top level root key, for a single root document file:

root: intro

The value of the root key is a path to a file, in Unix format (folders split by /), relative to the source directory β€” and it can be with or without the file extension. Either way, we're good. ✨

NOTE: This root file will be set as the master_doc.

Then document files can have a subtrees key β€” that's a list of individual toctrees for that document β€” and in turn, each subtree should have an entries key, which is a list of children links. And those children are one of:

  • file: path to a single document file in Unix format, with or without the file extension (same rules as root above)
  • glob: path to one or more document files via Unix shell-style wildcards (similar to fnmatch, but single stars don't match slashes.)
  • url: path for an external URL (starting e.g. http or https)
IMPORTANT: Each document file can only occur once in the ToC! Not twice. Don't do it. πŸ’€

And this can proceed recursively to any depth. Go off. 🌈

root: intro
subtrees:
- entries:
  - file: doc1
    subtrees:
    - entries:
      - file: doc2
        subtrees:
        - entries:
          - file: doc3
  - url: https://example.com
  - glob: subfolder/other*

This is equivalent to having a single toctree directive in intro, containing doc1, and a single toctree directive in doc1, with the :glob: flag and containing doc2, https://example.com and subfolder/other*.

Now here's the cute shorthand: the entries key can sit at the same level as the file, which means "a document with a single subtree." So this file is exactly equivalent to the one above, no cap:

root: intro
entries:
- file: doc1
  entries:
  - file: doc2
    entries:
    - file: doc3
- url: https://example.com
- glob: subfolder/other*

πŸŽ€ File and URL titles πŸŽ€

By default, the initial header within a file document gets used as its title in the generated Table of Contents. But with the title key you can set an alternative title for a document β€” and also for url, bestie:

root: intro
subtrees:
- entries:
  - file: doc1
    title: Document 1 Title
  - url: https://example.com
    title: Example URL Title

πŸ’… ToC tree options πŸ’…

Each subtree can be configured with a bunch of options (see also the sphinx toctree options, iykyk):

  • caption (string): A title for the whole the subtree, e.g. shown above the subtree in ToCs
  • hidden (boolean): Whether to show the ToC within (inline of) the document (default False). By default it is appended to the end of the document, but see also the tableofcontents directive for positioning of the ToC.
  • maxdepth (integer): A maximum nesting depth to use when showing the ToC within the document (default -1, meaning infinite).
  • numbered (boolean or integer): Automatically add numbers to all documents within a subtree (default False). If set to True, all sub-trees will also be numbered based on nesting (e.g. with 1.1 or 1.1.1), or if set to an integer then the numbering will only be applied to that depth.
  • reversed (boolean): If True then the entries in the subtree will be listed in reverse order (default False). This can be useful when using glob entries.
  • titlesonly (boolean): If True then only the first heading in the document will be shown in the ToC, not other headings of the same level (default False).

You can set these options at the level of the subtree:

root: intro
subtrees:
- caption: Subtree Caption
  hidden: False
  maxdepth: 1
  numbered: True
  reversed: False
  titlesonly: True
  entries:
  - file: doc1
    subtrees:
    - titlesonly: True
      entries:
      - file: doc2

Or, if you're using the shorthand for a single subtree, you set the options under an options key:

root: intro
options:
  caption: Subtree Caption
  hidden: False
  maxdepth: 1
  numbered: True
  reversed: False
  titlesonly: True
entries:
- file: doc1
  options:
    titlesonly: True
  entries:
  - file: doc2

And you can also use the top-level defaults key to set default options for all subtrees:

root: intro
defaults:
  titlesonly: True
options:
  caption: Subtree Caption
  hidden: False
  maxdepth: 1
  numbered: True
  reversed: False
entries:
- file: doc1
  entries:
  - file: doc2
WARNING: numbered should not generally be used as a default, since numbering cannot be changed by nested subtrees, and sphinx will log a warning. 😭 Not a cute look.
NOTE: By default, title numbering restarts for each subtree. If you want want this numbering to be continuous, check-out the ‐ sphinx-multitoc-numbering extension.

🌟 Using different key-mappings 🌟

For certain use-cases, it's helpful to map the subtrees/entries keys to mirror e.g. an output LaTeX structure.

The format key can be used to provide such mappings (and also initial defaults). Currently available:

  • jb-article:
    • Maps entries -> sections
    • Sets the default of titlesonly to true
  • jb-book:
    • Maps the top-level subtrees to parts
    • Maps the top-level entries to chapters
    • Maps other levels of entries to sections
    • Sets the default of titlesonly to true

For example:

defaults:
  titlesonly: true
root: index
subtrees:
- entries:
  - file: doc1
    entries:
    - file: doc2

is equivalent to:

format: jb-book
root: index
parts:
- chapters:
  - file: doc1
    sections:
    - file: doc2
IMPORTANT: These change in key names do not change the output site-map structure. The structure stays iconic, only the names change. periodt. πŸ‘‘

🧚 Excluding files not in ToC 🧚

By default, Sphinx will build all document files, regardless of whether they are specified in the Table of Contents β€” as long as they:

  1. Have a file extension relating to a loaded parser (e.g. .rst or .md)
  2. Do not match a pattern in exclude_patterns

To automatically add any document files that do not match a file or glob in the ToC to the exclude_patterns list, add this to your conf.py:

external_toc_exclude_missing = True

Note that, for performance, files that are in hidden folders (e.g. in .tox or .venv) will not be added to exclude_patterns even if they are not specified in the ToC. You should exclude these folders explicitly. πŸ™

IMPORTANT: This feature is not currently compatible with orphan files. 🫠

πŸ› οΈ COMMAND-LINE πŸ› οΈ

This package comes with the sphinx-etoc command-line program, plus a few extra tools riding along with it. Bestie, it's a whole little toolkit. πŸ’–

Wanna see all the options? Say less:

$ sphinx-etoc --help
Usage: sphinx-etoc [OPTIONS] COMMAND [ARGS]...

  Command-line for sphinx-external-toc.

Options:
  --version   Show the version and exit.
  -h, --help  Show this message and exit.

Commands:
  from-project  Create a ToC file from a project directory.
  migrate    Migrate a ToC from a previous revision.
  parse      Parse a ToC file to a site-map YAML.
  to-project    Create a project directory from a ToC file.

🌱 Building a template project from a ToC file

To build a template project from only a ToC file, it's giving:

$ sphinx-etoc to-project -p path/to/site -e rst path/to/_toc.yml

And listen β€” you can also add extra files in meta/create_files and append text to the end of files with meta/create_append, like so:

root: intro
entries:
- glob: doc*
meta:
  create_append:
    intro: |
      This is some
      appended text
  create_files:
  - doc1
  - doc2
  - doc3

πŸ”„ Building a ToC file from an existing site

$ sphinx-etoc from-project path/to/folder

πŸ“ The rules it plays by

  • Files/folders will be skipped if they match a pattern added by -s (based on fnmatch Unix shell-style wildcards)
  • Sub-folders with no content files inside will be skipped
  • File and folder names will be sorted by natural order
  • If there's a file called index (or whatever name you set with -i) in any folder, it gets treated as the index file β€” otherwise, the first file by ordering gets used. Periodt. πŸ‘‘

✨ Title-guessing behaviour

The command can also guess a title for each file, based on its path:

  • The folder name is used for index files, otherwise the file name
  • Words are split by _
  • The first "word" is removed if it's an integer

So for example, for a project with files:

index.rst
1_a_title.rst
11_another_title.rst
.hidden_file.rst
.hidden_folder/index.rst
1_a_subfolder/index.rst
2_another_subfolder/index.rst
2_another_subfolder/other.rst
3_subfolder/1_no_index.rst
3_subfolder/2_no_index.rst
14_subfolder/index.rst
14_subfolder/subsubfolder/index.rst
14_subfolder/subsubfolder/other.rst

it will create the ToC:

$ sphinx-etoc from-project path/to/folder -i index -s ".*" -e ".rst" -t
root: index
entries:
- file: 1_a_title
  title: A title
- file: 11_another_title
  title: Another title
- file: 1_a_subfolder/index
  title: A subfolder
- file: 2_another_subfolder/index
  title: Another subfolder
  entries:
  - file: 2_another_subfolder/other
    title: Other
- file: 3_subfolder/1_no_index
  title: No index
  entries:
  - file: 3_subfolder/2_no_index
    title: No index
- file: 14_subfolder/index
  title: Subfolder
  entries:
  - file: 14_subfolder/subsubfolder/index
    title: Subsubfolder
    entries:
    - file: 14_subfolder/subsubfolder/other
      title: Other

Look at that structure. It ate. πŸ’…


🧠 API 🧠

The ToC file gets parsed into a SiteMap, which is a MutableMapping subclass. Its keys are docnames mapping to a Document, which stores the info about the toctrees it should contain:

import yaml
from sphinx_external_toc.parsing import parse_toc_yaml
path = "path/to/_toc.yml"
site_map = parse_toc_yaml(path)
yaml.dump(site_map.as_json())

Would produce e.g.

root: intro
documents:
  doc1:
    docname: doc1
    subtrees: []
    title: null
  intro:
    docname: intro
    subtrees:
    - caption: Subtree Caption
      numbered: true
      reversed: false
      items:
      - doc1
      titlesonly: true
    title: null
meta: {}

πŸ¦‹ EXAMPLE PROJECT πŸ¦‹

These pages provide an example of different aspects of the ToC. Peep this project's _toc.yml for how they are added. iykyk πŸ’œ

πŸ“„ Sub-section Page

This is a page of a subsection.

🌸 Sub-section Glob Page 1

A page added via globbing.

🌈 Sub-section Glob Page 2

Another page added via globbing.


πŸ‘‘ AUTHOR πŸ‘‘

Executable Book Project

2021, Executable Book Project


okay bestie that's the whole thing, we made it to the end together 🫢 this page is from the sphinx-external-toc docs, dated Apr 06, 2025, and it's the SPHINXEXTERNALTOC(1) manual β€” go forth and slay your ToC, fr fr βœ¨πŸŽ€

πŸ“¦ where this page comes from