A Quarto website theme with one distinctive property: the built site
keeps working when it is opened from a plain file tree — file://, or
the chrome:// address of help bundled into a Zotero plugin. Search,
math, the table of contents that follows the scroll, the dark-mode
toggle: all of it, with no request ever leaving the page (fonts and
MathJax ship with the site).
The look: a centred three-column layout with a frosted-glass header, self-hosted Noto Sans, quiet sidebars with a soft accent pill on the current page, and Quarto's native callouts. Light and dark, switchable. Built for the zotQDA documentation sites; generic enough for any documentation site with the same needs.
quarto add easyqda/easyQDA-quarto-themethen select the format in _quarto.yml:
format:
easyqda-theme-html: defaultThe format brings toc, code-copy, the light/dark pair and an
adaptive syntax-highlight pair (github / github-dark) with it.
Copy font/ into the site project and declare it:
project:
resources:
- font/
format:
easyqda-theme-html:
css: font/noto.cssThe woff2 subsets keep their unicode-range, so a page only loads the
scripts it uses. Without this block the theme falls back to the system
font stack — still without any external request.
Quarto's output assumes http. offline/postprocess.py repairs the
three things that die on file:// — quarto.js is an ES module, the
search fetches its index, MathJax comes from a CDN:
quarto render mysite
python3 offline/postprocess.py mysite/_site
python3 offline/smoketest.py mysite/_site # headless-browser proofEach repair matches exact strings in Quarto's output and fails
loudly when a Quarto update changes them, instead of shipping a site
that breaks only for offline readers. The smoketest then loads real
pages in a headless Chromium-family browser over file:// and checks
scroll tracking, search, math, fonts and the absence of any CDN
reference. QDA_SMOKE_BROWSER overrides the browser autodetection.
print/make_pdf.py renders a language project as a single PDF: the
chapter order is the sidebar order and top-level sidebar sections
become book parts (so the PDF cannot fall behind a page), the engine
is Typst (bundled with Quarto — no TeX toolchain), Noto Sans is
embedded from print/font/, the table of contents and the citations
are linked, and one bibliography sits at the end. The cover's subtitle
comes from the front matter of the language's index.qmd, and a
project's asset/ directory is copied into the book so its figures
appear in the PDF too.
Every page but the cover carries a footer set like the running header
and ruled off the same way: the version on the left, the build date on
the right, so a saved copy says which state of the documentation it
is. The version comes from --version or DOC_VERSION, the date from
--date or DOC_DATE (today, UTC, by default). Three habits of the
book format are put right on the way: the part page no longer lays the
part's contents over its title, a part ends where it ends instead of
swallowing the chapters after it, there are no blank pages — none
before a chapter to reach a right-hand page, none after the cover for
a copyright that is not there — and no untitled list of figures trails
the contents.
python3 print/make_pdf.py mysite/en mysite/_site/docs.pdf "Jane Doe" --version 1.2.0Give the site a navbar icon pointing at the file
(icon: file-pdf, href: docs.pdf — Quarto adjusts the relative
path per page) and call the script from the site's build; the zotQDA
sites do both. One quirk is codified in the script: Quarto's Typst
book cover requires an author and fails without one — pass it as the
third argument or set author: in index.qmd's front matter.
script/gen_langmap.py makes the header's language switcher send
readers to the translation of the page being read instead of the
other language's front page. It takes the language project directories
as arguments, walks their sidebars in parallel — they must list the
same pages in the same order, which is checked, failing the build
loudly when a page was added to one language only — and writes the
page map plus the retargeting logic into each project as
theme/scripts.html for include-after-body. The map carries all
translations per page, so a third language is one more argument and
one more directory.
script/versions.js is the banner that says which version a reader is
looking at, with a menu to the same page in any other one and a warning
when the version is not the current release. Pass it to the generator
above — gen_langmap.py --extra-js ../script/versions.js en de — and it
is inlined into the same theme/scripts.html. It needs versions.json
at the site root, one directory above the version directories, which
script/gen_versions.py writes from the project's own manifest.
Everything it does is conditional on the address carrying a version segment, so in a plain file tree and in help bundled inside an application it does nothing at all — which is correct: that copy belongs to the release it shipped with.
example/scaffold/ is the layout that grew out of the first big
bilingual site on this theme, fed back as a copyable starting point:
one asset/ tree mirrored into the language projects, all scripts
vendored under code/ (they find their resources in either layout,
unpatched), figures generated from code/abbildungen/ by an
incremental make asset step so the repository stays text, a chunk
guard keeping the pages generator-neutral, and a language switch at
the site root.
Both the styling rules and the offline repairs are written against a
concrete Quarto version (currently 1.10.18, see the CI workflow).
Pin it in your build, and treat upgrades as deliberate events: render,
run the smoketest, look at one site, then move the pin. This
repository's CI does exactly that with example/minimal/;
example/site-publish.yml (GitHub Actions) and
example/site-publish-gitlab.yml (GitLab Pages, also self-hosted CE)
are templates for a documentation site's own publish workflow.
_extensions/easyqda-theme/— the format:_extension.yml,easyqda.scss(shared defaults and all rules),easyqda-light.scss/easyqda-dark.scss(palettes only)font/— Noto Sans woff2 subsets +noto.css(SIL OFL 1.1)offline/postprocess.py— the three offline repairs, loud on driftoffline/smoketest.py— the headless-browser proofoffline/mathjax/— vendored MathJax (Apache 2.0)print/— the one-PDF builder and the Noto Sans TTFs it embedsscript/gen_langmap.py— the language switcher's page-to-page mapscript/versions.js— the version banner and switcher, inlined bygen_langmap.py --extra-js; needsversions.jsonat the site rootexample/minimal/— the site the CI renders and smokesexample/scaffold/— a copyable project layout for a full bilingual site (build script, figure Makefile, root language switch)
The rules layer targets Quarto's HTML classes (.sidebar-item-text,
#toc-title, .quarto-color-scheme-toggle, …). Those are stable in
practice but not a documented contract — the pin plus the smoketest is
the answer, an eyeball on one page after upgrades is still wise. Three
overrides need !important or extra specificity because Quarto's own
rules carry weight; each is commented in easyqda.scss where it
happens.
MIT (see LICENSE). Bundled: Noto Sans under the SIL Open Font
License 1.1 (font/OFL.txt), MathJax under Apache 2.0
(offline/mathjax/LICENSE).