A dedicated MyST interactive book theme (@quantecon/lecture-theme) for the
QuantEcon lectures and books, distributed as a zip attached to each
GitHub Release.
- Responsive and mobile ready
- Notebook launch (Google Colab) with configurable repo, branch and paths
- In-page live compute via Thebe (JupyterLite / Pyodide by default)
- Git history in page headers ("Last changed" plus an inline changelog)
- Full OpenGraph / Twitter card meta tags, and collapsible stderr on notebook cells
- A
docs/feature reference mirroring the book theme's user guide - Page Footer based on MyST Content
- Bundled QuantEcon brand assets
When downloads are available on a page, a download button will appear in the top toolbar. The contents of the menu available from that button is configured via the download configuration of the MyST project and page. Typically, a download of the entire book as a PDF is provided along with downloads of each lecture in PDF and Notebook (md) form.
To achieve this the following configuration should be added to build and expose the downloads.
At the project level:
# myst.yml
project:
...
exports:
- id: book-pdf
format: pdf
template: plain_latex_book
output: exports/quantecon-python-intro.pdf
downloads:
- id: book-pdf
title: Book (PDF)And then on each page:
# long_run_growth.md
exports:
- format: pdf
template: plain_latex_book
output: exports/long_run_growth.pdf
downloads:
- file: exports/long_run_growth.pdf
title: Lecture (PDF)
- file: ./long_run_growth.md
title: Notebook (md)The launch notebooks capability mirrors the previous QuantEcon theme's, which shows the control only for a site that names a notebook repository. Nothing is assumed about repository naming: a site says where its notebooks are, or gets no Launch control.
The Launch control in the toolbar is a direct link to Google Colab, the single launch target — it provides GPU access for the lectures that need it. Two other targets are deliberately not offered today, each tracked as a demand-driven future request: BinderHub, which proved flaky in practice (#26), and a private JupyterHub, whose launcher entry was removed in #87 because no lecture repo had ever configured one. For running cells without leaving the page, see Live compute below.
Launch is opt-in: it appears only once a site names a notebook repository
and turns a launch service on, under site.options in myst.yml (MyST's
theme-options section; see Site options for the full list).
site:
options:
launch_notebook_repo: QuantEcon/lecture-foo.notebooks
launch_colab: trueWith either unset there is no Launch control. Nothing is derived from
project.github, so a site without a notebooks repository cannot link readers
to one that does not exist; set launch_notebook_repo only when the repository
really is there.
| Option | Default | Purpose |
|---|---|---|
launch_notebook_repo |
(none) | Notebook repository, as a full URL or org/repo |
launch_notebook_branch |
main |
Branch in the notebook repo to launch from |
launch_notebook_dir |
(none) | Sub-directory within the notebook repo where the notebooks live |
launch_notebook_source_dir |
(none) | Path prefix stripped from the page location (e.g. a lectures/ source dir) |
launch_colab |
(off) | Offer Google Colab |
Coming from quantecon-book-theme: nb_repository_url → launch_notebook_repo,
nb_branch → launch_notebook_branch, nb_path_to_notebooks →
launch_notebook_dir, path_to_docs → launch_notebook_source_dir, and
launch_buttons.colab_url → launch_colab: true.
In addition to launching a notebook elsewhere (Colab/Hub), the theme can run
notebook cells in place via Thebe. This is
opt-in per project through the standard MyST thebe config, set under
project.thebe in myst.yml (the theme reads it from the project manifest, so
it is project-level — not per-page frontmatter). The QuantEcon default is
JupyterLite — Python runs entirely in the browser via Pyodide, with no
server or Binder to host:
# myst.yml
project:
thebe:
lite: trueWith this set, a Power toggle appears in the header toolbar (next to the
Launch button) on notebook pages. Clicking it boots the in-browser kernel, after
which Run / Restart / Clear take its place; cells then execute live. The
toggle lives in the fixed header so it stays visible while scrolling, and only
appears on notebook pages (desktop). It is the @myst-theme/jupyter notebook
toolbar, relocated into the header via a portal.
Caveat: Pyodide runs pure-Python plus packages compiled for it (numpy,
scipy, pandas, matplotlib, sympy). Packages that aren't available for Pyodide
(e.g. numba, JAX) won't import, so live compute suits lectures whose runtime
stack is Pyodide-compatible. Other backends are available through the same
thebe config (binder: for BinderHub, server: for a hosted Jupyter
server) if a project needs a full environment.
The page header shows a "Last changed: ⟨date⟩" control (aligned to the right of
the author line) that expands an inline changelog listing the most recent
commits touching that page — commit hashes link to GitHub, and a "full history"
link opens the file's complete commit log (mirroring the quantecon-book-theme
header). The changelog opens in place above the header's blue divider, pushing
it down, so it stays adjacent to its toggle and clear of the lecture content.
The data is injected at build time by plugins/git-metadata.mjs,
a MyST transform that runs git log --follow per source file and attaches
{ last_modified, changelog: [{hash, short_hash, author, date, message}] }
to the page AST. Lecture repos load it by URL, pinned to the same theme release
tag they build with, rather than vendoring copies:
# myst.yml
project:
github: https://github.com/QuantEcon/lecture-python.myst # commit links target this repo
plugins:
- https://raw.githubusercontent.com/QuantEcon/quantecon-theme.mystmd/v3.0.0/plugins/git-metadata.mjsThe plugin is moving to the shared
QuantEcon/quantecon-plugins.mystmd
repository (QuantEcon/quantecon-plugins.mystmd#11); the in-repo copy is retired once
that standalone release exists
(#157). Until then
the pinned URL above is the supported form.
Notes:
- The header control renders nothing when no metadata is present, so projects without the plugin are unaffected.
- A page can pin the control by hand with the same shape under
site:in its frontmatter, as a YAML block string (git_metadata: |then the indentedlast_modifiedandchangelog); it takes precedence over the injected data.tests/visual/fixture/features.mdis an example. - The plugin is a silent no-op for untracked files, non-git checkouts, missing
git, or agit logtimeout (5s). Shallow CI clones (fetch-depth: 1) produce truncated history — usefetch-depth: 0when building for deploy. QE_GIT_METADATA_MAXcaps changelog entries per page (default 6; myst-cli does not pass options to transform plugins, hence the environment variable). The expanded changelog does not scroll — it grows to fit — so this value is also what controls how tall it gets.- A page can pin or correct its history manually — set the same shape under
site.git_metadatain the page frontmatter, which takes precedence over the injected data (this is how the visual fixture keeps snapshots deterministic).
A translated edition sets its language, direction and translators, and lists the
sibling editions, under site.options (all declared in template.yml):
# myst.yml
site:
options:
current_language: fa # BCP 47 code: document lang, current entry in the switcher
enable_rtl: true # dir="rtl" on the document (Arabic, Persian, Hebrew, Urdu)
language_switcher_label: تغییر زبان # tooltip / accessible name (default "Switch language")
languages: |
- code: en
name: English
url: https://python-programming.quantecon.org
- code: fa
name: فارسی
url: https://quantecon.github.io/lecture-python-programming.fa
translators: |
- name: Adisankar Manoj Thanuja
url: https://www.linkedin.com/in/adisankar-m-t/
translators_label: ترجمهٔ # default "Translated by"; an empty string hides the labellanguages— with two or more entries the toolbar shows a globe-icon switcher linking to the same page in each edition (that edition'surlplus the page path), and every page carries<link rel="alternate" hreflang>tags, with the first entry asx-default. The entry matchingcurrent_languageis marked current.translators— credited in the page header, at the end of the "Last changed" row, with the label in front. A page overrides the list in its own frontmatter undersite:; the page value replaces the site value (never merges), and an explicitly empty value (translators: '') suppresses the credit on that page. The same values configurequantecon-book-theme.
languages and translators are written as YAML inside a block string
(|). The MyST CLI validates site.options against the template's declared
options and can only declare scalar types, so a bare list would be dropped; the
theme parses the block. A real list is accepted too, should the engine ever pass
one through.
One trap: a page whose site: frontmatter sets any of these keys has its whole
site: block replaced by the validated keys, so an undeclared site.git_metadata
on the same page is lost. Keep the two on different pages.
Every page carries the OpenGraph and Twitter card set the Sphinx lecture sites
emit: og:title, og:description, og:type (website), og:site_name,
og:url, og:image, og:locale, and twitter:card, twitter:site,
twitter:creator, twitter:title, twitter:description, twitter:image.
Four site.options keys feed them: site_url (the public URL, for
og:url; myst's site.domains does not reach the theme), twitter, and
the two image URLs og_logo_url / twitter_logo_url, named as in the book
theme so a lecture repo copies its values across. A page's own thumbnail takes precedence for
og:image.
Every page also carries a <link rel="canonical">, from the same site_url
and built by the same function as og:url, so the two always agree. The home
page's canonical is the site root, and every URL takes the trailing-slash form
the build actually serves. With site_url unset neither is emitted — what
Sphinx does without html_baseurl.
A notebook cell's stderr stream is folded behind a "⚠ Code warnings"
disclosure, closed by default, as on the Sphinx sites; stdout in the same cell
stays visible. It is a native <details>, so it works without JavaScript.
Everything the theme reads from site.options is declared in
template.yml. That list is not documentation only: the MyST
CLI validates site.options against it and drops any key the template does
not declare, so an undeclared option never reaches the theme
(#173).
Template options are scalar-only, so the structured ones are written as a YAML
block inside a string (key: |), which the theme parses.
| Option | Scope | Purpose |
|---|---|---|
twitter |
site | Handle for the twitter:site / twitter:creator card meta tags |
site_url |
site | The site's public URL, for the canonical link and og:url (Meta tags) |
og_logo_url, twitter_logo_url |
site | Site-level images for og:image / twitter:image when a page has no thumbnail (Meta tags) |
favicon |
site | Favicon file, relative to myst.yml; served at /favicon.ico (the QuantEcon lectures favicon when unset) |
analytics_google, analytics_plausible |
site | Analytics IDs, rendered by @myst-theme/site |
hide_toc, hide_search |
site or page | Hide the contents drawer / the search control |
launch_notebook_repo, launch_notebook_branch, launch_notebook_dir, launch_notebook_source_dir, launch_colab |
site | Notebook launcher (Launch buttons) |
current_language, enable_rtl, languages, language_switcher_label |
site | Multilingual editions (below) |
translators, translators_label |
site or page | Translator credit in the page header |
git_metadata |
page | YAML block pinning the "Last changed" control by hand (Git history) |
A page-level value goes under site: in the page's frontmatter and is
validated against the same list. Options carry no defaults in template.yml
(a declared default would be written into every page's validated block and
override the site-wide value); defaults live in the code that reads each one.
Point your project's site.template at a pinned release zip:
# myst.yml
site:
template: https://github.com/QuantEcon/quantecon-theme.mystmd/releases/download/v3.0.0/quantecon-theme.zipPin a specific version rather than tracking a branch, and bump it deliberately: the releases page carries the changelog entry for each version, including anything consumers need to change.
Then start the local server:
myst startOpen up http://localhost:3000 and you should be ready to go!
After cloning the repository, install the packages and start the dev server (with CSS watch and hot reload):
npm install
npm run devTo preview against real MyST content instead, run a headless content server in
your content project (myst start --headless) alongside the theme dev server.
See CONTRIBUTING.md for the full development setup,
available scripts, and the test suites, and tests/visual/README.md
for the visual-regression harness.
Releases are cut by pushing a vX.Y.Z git tag: the
release.yml workflow builds the theme and
publishes a GitHub Release with quantecon-theme.zip attached, using that
version's CHANGELOG.md section as the release notes. The
step-by-step flow is documented in
CONTRIBUTING.md.