PDF styling#

PDF builds and HTML are styled completely separately: the website is styled with CSS, and the PDF is styled with LaTeX, a widely-used typesetting system with its own syntax. This page is a primer on how that works, aimed at anyone working on this theme who hasn’t used LaTeX before.

All of 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:

  1. Sphinx reads your .rst files.

  2. Sphinx then writes out a .tex file: a plain-text document full of commands.

  3. A LaTeX engine - a separate program - reads that .tex file 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, but there is no automatic link: if any of these aspects are changed in the design system they must be updated in the theme separately.

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.

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: two-sided margins that mirror left/right for binding, and chapters forced to always start on a right-hand page (inserting a blank page when needed to get there). That makes sense for something printed and bound, but these PDFs are downloaded and read on a screen, so this theme opts out of both - 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}[...]

This binds the same three fonts the website uses - Nunito Sans for body text, Hanken Grotesk for headings, Roboto Mono for code - to the .ttf files shipped in iati_sphinx_theme/fonts/.

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.

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}
}

Every LaTeX document has a default, plain title page. This throws it away and draws a logo, a coloured title, and the author/date instead.

@title, @author and @date aren’t hardcoded - they come from whatever Sphinx’s latex_documents setting in the project’s conf.py supplied, 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.py already sets a given latex_elements key, the project’s value wins outright.

  • The preamble key 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 sphinxsetup string 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.

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.