PDF styling#
PDF builds and HTML are styled completely separately: the website is styled with CSS, and the PDF is styled with LaTeX, a typesetting system with its own syntax. This page is a primer on how that works for anyone working on this theme who hasn’t used LaTeX before.
The PDF-specific configuration lives in iati_sphinx_theme/latex.py.
How PDF output works#
Building the website and building a PDF share the first step, then diverge completely:
Sphinx reads your
.rstfiles.Sphinx then writes out a
.texfile: a plain-text document full ofcommands.A LaTeX engine - a separate program - reads that
.texfile and lays out the actual PDF pages.
latex.py only touches step 2. It doesn’t write any of your content - it injects a block of setup commands, called a preamble, before Sphinx’s generated content.
A handful of LaTeX terms cover almost everything in that preamble:
| Term | What it means |
|---|---|
| preamble | The setup block before real content starts. Anything defined here - a colour, a font, a page margin - applies for the rest of the document. |
usepackage{x} |
An import. LaTeX ships with a small core; almost everything useful (colour, custom headers, hyperlinks, tidy tables) comes from a package you opt into by name. |
command{}[] |
A function call. Curly braces {} are required arguments; square brackets [] are optional ones. definecolor{iatiorange}{HTML}{DB584B} reads as “define a colour named iatiorange, from an HTML hex code, equal to DB584B.” |
renewcommand |
Overrides a command LaTeX (or a package) already defines. |
| engine | The program that actually compiles the .tex file into a PDF. Sphinx defaults to pdflatex; this theme changes that default (see Building PDFs locally). |
What latex.py configures#
latex.py incorporates styling from the IATI design system. Most of it - the brand colours and the logo - is derived from the design system automatically at build time (see Keeping in sync with the design system below). The fonts are the one deliberate exception.
Brand colours#
\usepackage{xcolor}
\definecolor{iatiorange}{HTML}{DB584B}
\definecolor{iatiteal}{HTML}{155366}
\definecolor{iatigrey}{HTML}{121212}
\definecolor{iatigreen}{HTML}{0A9172}
\definecolor{iatipurple}{HTML}{6F3AAF}
Same idea as CSS custom properties: each brand colour is defined once, then used by name in every block below instead of repeating hex codes.
These definecolor lines are generated from the design system’s colour tokens at build time (see Keeping in sync with the design system).
Page geometry#
\usepackage{geometry}
\geometry{
a4paper,
margin=2.5cm,
top=3cm,
bottom=3cm
}
Sets the paper size and the white space around content.
Document layout#
"extraclassoptions": "oneside,openany"
LaTeX’s manual/report document classes default to book-style layout, but these PDFs are downloaded and read on a screen, so this line sets that up: oneside keeps margins identical on every page, and openany lets chapters start on the very next page instead of skipping one to land on an odd page number.
This is set via Sphinx’s extraclassoptions latex_elements key (passed as document class options) rather than the raw preamble, since by the time the preamble runs, the document class has already been loaded with its default options.
Brand fonts#
\setmainfont{NunitoSans}[...]
\setsansfont{HankenGrotesk}[...]
\setmonofont{RobotoMono}[...]
These are the brand fonts: Nunito Sans for body text; Hanken Grotesk for headings; Roboto Mono for code; provided by the .ttf files shipped in iati_sphinx_theme/fonts/.
Unlike the colours and logo, these font files are committed to the repository rather than pulled from Google Fonts at build time to ensure that there’s always a local font file on disk for LaTeX to use. See Keeping in sync with the design system.
It’s set via Sphinx’s fontpkg latex_elements key rather than the raw preamble, since Sphinx already loads the fontspec package itself (as part of its own fontenc handling) when the engine is XeLaTeX or LuaLaTeX. fontpkg just needs to say which fonts to use.
Headings use sffamily to switch to Hanken Grotesk, since setsansfont binds a font to LaTeX’s existing “sans family” concept rather than introducing a new command.
Note
fontspec (and therefore these brand fonts) only works under the XeLaTeX or LuaLaTeX engines, not the default pdflatex - see Building PDFs locally.
Chapter and section headings#
\usepackage{titlesec}
\titleformat{\chapter}[display]
{\normalfont\sffamily\huge\bfseries\color{iatiorange}}
{\chaptertitlename\ \thechapter}
{20pt}
{\Huge}
\titleformat{\section}
{\normalfont\sffamily\Large\bfseries\color{iatiteal}}
{\thesection}
{1em}
{}
Chapters are large and orange; sections and subsections step down in size and switch to teal - the same hierarchy as the HTML theme, expressed in LaTeX’s more verbose syntax.
Hyperlinks#
\usepackage{hyperref}
\hypersetup{
colorlinks=true,
linkcolor=iatiteal,
urlcolor=iatiorange,
citecolor=iatigreen
}
Internal cross-references are teal, external URLs are orange, citations are green.
Custom title page#
\renewcommand{\sphinxmaketitle}{
\begin{titlepage}
\centering
\includegraphics[width=0.5\textwidth]{logo-colour.png}
{\sffamily\Huge\bfseries\color{iatiorange}\@title\par}
{\Large\color{iatigrey}\@author\par}
{\large\color{iatigrey-light}\@date\par}
\end{titlepage}
}
This configures the title page to have a logo, a coloured title, and the author/date.
@title and @author aren’t hardcoded - they come from Sphinx’s latex_documents setting. In the standardised IATI setup the conf.py holding latex_documents is synced identically across repos, so it derives those values from the project’s project_info.py (the project name and author) rather than having anyone hand-edit conf.py. (@date isn’t part of latex_documents - it defaults to the build date.) So every project gets its own cover with the shared IATI look.
Important
This has to override sphinxmaketitle, not the standard maketitle command. Sphinx’s document classes call sphinxmaketitle directly and never call plain maketitle, so a renewcommand{maketitle}{...} here would silently have no effect at all - which is exactly what happened until this was caught.
Tables, code blocks and admonitions#
\usepackage{booktabs}
\renewcommand{\arraystretch}{1.3}
The rest is set via Sphinx’s sphinxsetup element rather than raw LaTeX: nicer table rules, and colour-coded borders/backgrounds for .. note::, .. warning::, .. tip:: and .. important:: blocks, matching the same four brand colours used everywhere else in this document.
How it’s wired into Sphinx#
latex.py doesn’t run on its own - iati_sphinx_theme/__init__.py connects its configure_latex_defaults function to Sphinx’s config-inited event, a point early in the build where every project’s conf.py has just finished executing, but nothing has been generated yet.
That function merges the theme’s defaults into whatever latex_elements a project already set, without clobbering it:
If a project’s
conf.pyalready sets a givenlatex_elementskey, the project’s value wins outright.The
preamblekey is the exception: the theme’s preamble is prepended to the project’s, so a project can add its own LaTeX underneath the branding without losing it.Likewise, a project’s
sphinxsetupstring is appended to the theme’s.
The same function also switches the default latex_engine from Sphinx’s own default (pdflatex) to lualatex, but only if a project hasn’t already chosen a different engine - see below.
SVG images#
Sphinx’s LaTeX builder only accepts PDF, PNG and JPEG images, so an SVG referenced in your docs is silently dropped from the PDF - even though it renders fine on the website. To avoid that, the theme registers the sphinxcontrib.rsvgconverter extension automatically (in iati_sphinx_theme/__init__.py), which converts SVGs to PDF - as crisp vectors - during the PDF build using the rsvg-convert tool from librsvg.
The upshot: you can use SVG images in your docs and they render in both HTML and PDF, with no need to keep a separate PNG copy.
System requirement. rsvg-convert must be present in the build environment:
Read the Docs - add it via
build.apt_packagesin.readthedocs.yaml:build: apt_packages: - librsvg2-bin
Local, Debian/Ubuntu (including the devcontainer) -
apt-get install librsvg2-bin.Local, macOS -
brew install librsvg.
If rsvg-convert is missing, SVGs are simply skipped in the PDF (with a warning), exactly as before - HTML builds are never affected.
Note
Consumer sites get the extension automatically from the theme (it’s a theme dependency, and the theme registers it - no conf.py change needed). Each site only needs to install librsvg2-bin in its own build environments: its .readthedocs.yaml and its devcontainer.
Keeping in sync with the design system#
The HTML theme consumes the IATI design system directly: it’s an npm dependency, pinned in package.json, and the CSS is compiled from it by npm run build. The PDF pipeline needs the same branding, but in forms LaTeX can use - colours as hex codes, the logo as a raster image - so rather than hand-copying them (where they drift out of sync), it derives them from the same pinned design system at build time.
scripts/generate-brand-assets.mjs, which runs as part of npm run build, produces:
iati_sphinx_theme/_generated/brand_colors.json- the brand colours, read from the design system’stokens/_color.scss.latex.pyreads this to emit thedefinecolorlines and the admonition border colours.iati_sphinx_theme/static/logo-colour.svgandlogo-colour.png- copied and rasterised from the design system’s logo SVG. The HTML header uses the SVG; the PDF title page uses the PNG. (SVG images in docs content are converted to PDF automatically - see SVG images - but the title-page logo deliberately uses the pre-rasterised PNG.)
These are gitignored build artifacts, exactly like the compiled CSS - they aren’t committed to this repository. They’re regenerated wherever the CSS already is: local development, Read the Docs (in its pre_install step), and the PyPI publish workflow (before python -m build, so they’re baked into the released package).
The pinned version is the unit of sync. Builds are reproducible against whatever iati-design-system version is pinned in package.json; brand values only change when someone bumps that pin deliberately - the same moment the CSS would change. To adopt design-system updates, bump the pin and rebuild.
To catch the design system moving ahead of the pin, the .github/workflows/design-system-drift.yml workflow runs on a schedule: it regenerates the brand assets against the latest published iati-design-system and fails (with a warning annotation) if the colours or logo would differ from the pinned version, as a prompt to bump the pin.
The fonts are the exception to all of this - see Brand fonts for why they’re vendored rather than generated.
Building PDFs locally#
make -C docs latexpdf
This requires a LaTeX distribution (for example TeX Live) that includes the lualatex engine, since the brand fonts depend on the fontspec package, which only works under XeLaTeX or LuaLaTeX - not the pdflatex engine Sphinx uses by default. This theme sets the default engine to lualatex automatically, so no extra configuration is needed in a project’s conf.py.
Note
Of the two fontspec-compatible engines, this theme picks lualatex over xelatex: Sphinx’s mechanism for force-wrapping long unbroken tokens (URLs, in particular) inside code blocks silently fails under XeLaTeX, letting them overflow the page instead of wrapping. The same content wraps correctly under LuaLaTeX.
Known limitations#
Warning
Sphinx marks wrapped long lines in code blocks with a special character (U+2423, “open box”), which Roboto Mono doesn’t include. LaTeX skips the missing glyph with a warning rather than failing the build, so wrapped code lines just lose that wrap indicator.