Building a static HTML site

CLI Beta

ubc build html renders your project into a self-contained static website — one HTML page per indexed document, plus the assets those pages need. It is ubc doing the rendering, not Sphinx: there is no conf.py, no extension loading and no Python environment involved, so the whole site comes out of the same index the editor and ubc check already use.

The builder is Beta: details such as an option, the emitted markup or the wording of a finding may still change between minor releases, so pin the minor version you build with. See Differences from a Sphinx build for what a site built this way does not yet match in a full sphinx-build. Out of scope or not there yet: autodoc (ubCode does not run your code) and Graphviz — see Unsupported syntax. Sections, figures, tables, code blocks and equations are numbered as in Sphinx, and :numref: / :eq: print those numbers: see Numbering sections, figures and equations.

Without a license, ubc build html builds small projects only; see What works without a license for the limit.

Quickstart

Run the command from anywhere inside your project:

ubc build html

It discovers the closest ubproject.toml, indexes the project, and writes the site next to that file:

Building index for: /home/me/project/ubproject.toml
Processing 3 files
3 page(s): 3 rendered, 0 reused; 3 written, 0 unchanged; 6 asset(s).
Site written to: /home/me/project/_build/html
Root page: /home/me/project/_build/html/index.html
Done in 0.12s

The last two lines are the ones you want: the site directory, and the page to open first. On a terminal the run also draws its progress as it goes, one row per phase — see while it works.

Principal options

Option

What it does

[PATH]

The project root to build. Defaults to the current directory.

-o, --output <DIR>

Where to write the site. Defaults to _build/html under the project root; a relative path is resolved against the current directory.

-w, --show-warnings

Print the detail of every build and indexing warning. A build that fails the quality gate prints it anyway, so this matters mainly once the gate has been lowered (see Warnings).

--fresh

Re-render every page, ignoring any existing build manifest.

--strict-sources

Fail if a source file changes during the build, instead of re-indexing and retrying once.

--no-cache

Index from scratch rather than reading the on-disk cache. It implies --fresh.

--deny <LEVEL>

Move the failure threshold for this run: none, info, warning (the default) or error. See Building for production.

--max-warnings <N>

Fail if more than N warnings are found, counting both streams.

--lint-extend-ignore <CODES>

Additional diagnostic codes to ignore, comma separated.

-c, --config <TOML>

A configuration override, repeatable; each is its own TOML document, and the last wins on a repeated key (tables are merged, arrays of tables appended) — for example -c 'build_tags = ["html"]'.

-v / --verbose additionally names the client assets the binary carries — the Mermaid and KaTeX versions, and the KaTeX / copy-button bundle’s content hash — which is what to quote when the rendered output looks stale or unexpected. Code highlighting is not in that list: it happens during the build, so there is no bundle to be stale.

Run ubc build html --help for the complete list.

The output

Each document maps straight to a file: an indexed document at docname guide/setup becomes guide/setup.html — the source layout is mirrored, with no guide/setup/index.html rewriting — and everything shared lives under _static/.

It has no server-side component of any kind. Opening index.html straight from disk — a file:// URL — gives you the whole site, including full-text search, which is what makes a built site usable on an air-gapped machine or straight out of a ZIP file. See Search for what the search does and the three files it adds.

Alongside the pages, the output root always carries an objects.inv — the standard Sphinx inventory of the project’s std domain (one row per document, per label and per glossary term), written on every build with no option to turn it off, exactly as sphinx-build writes one. It is what makes a published site an intersphinx target: another project pointing [intersphinx.projects] (or Sphinx’s own intersphinx_mapping) at the site’s base URL can then resolve :ref:, :doc: and :term: references into it. The header’s project and version lines come from [project] name and [project] version, so setting both is what makes the inventory identify itself correctly to consumers. Use ubc intersphinx inspect to read one back.

A needs.json sidecar joins it when [needs] build_json is enabled.

The output directory

Pages that are no longer produced — because you deleted or renamed a document — are removed from the output directory on the next build, which is reported as Removed N stale file(s). That pruning only happens in a directory the build owns. A build owns its output directory when, before it starts, the directory

  • does not exist,

  • is empty, or

  • carries the marker file .ubc_build_html that an earlier ubc build html wrote there.

An owned directory is stamped with that marker at the start of the build, so a build that is interrupted still leaves a directory the next build can clean up.

Pruning removes only files the builder makes: any *.html page, anything under _static/, _images/ and _downloads/, and the needs.json and objects.inv files at the root. Anything else — a CNAME, a .nojekyll, a data.json of your own — is never touched, even in an owned directory. A 404.html you wrote yourself is a page by that rule, so keep hand-written pages out of an owned directory.

A directory that already has content and no marker — a hand-authored site, or -o . over the source tree — is never owned: the build writes its pages into it, deletes nothing, does not add the marker, and warns build.stale_files_kept on every build. Point -o at a fresh directory to get stale-file cleanup back.

Rebuilds

Rebuilds are incremental in two independent steps, and the count line reports both:

3 page(s): 0 rendered, 3 reused; 0 written, 3 unchanged; 6 asset(s).
rendered / reused

Whether each page’s body had to be rendered again. A page whose inputs are unchanged has its stored body reused.

written / unchanged

Whether the assembled page differed from what is already on disk. Identical output is not rewritten, so file timestamps stay put and tools watching the directory are not woken for nothing.

The two are separate, and the second is the stricter one. --fresh re-renders every page but still byte-compares the result, so a fresh build of unchanged sources honestly reports 3 rendered, 0 reused; 0 written, 3 unchanged.

Reach for --fresh when you want to rule the manifest out — after upgrading ubc, or when a page looks stale. --no-cache goes one step further and re-indexes from scratch as well.

If a source file changes during the build, the build re-indexes and retries once rather than emitting a half-stale site. --strict-sources turns that into an error instead, which is the right setting for a release pipeline where a moving source tree means something is wrong.

Warnings

The build reports two independent counts.

Indexing warnings are the ordinary project diagnostics — the same findings ubc check reports.

Build warnings are raised by the render phase itself and all carry a build.* code: an unreadable extra_css file, a diagram file that cannot be resolved, a directive or role ubCode has no renderer for, a search index that has grown large. The ones tied to a [build.html] setting are documented with that setting on the [build.html] configuration page, and the rest in the build warning codes table. All of them use the ordinary lint configuration, so you can silence one by code or by message.

Both counts are summarised, and -w / --show-warnings prints the detail, one severity-headed block per finding (see Linting on the command line):

$ ubc build html -w
3 page(s): 1 rendered, 2 reused; 1 written, 2 unchanged; 6 asset(s).
Found 1 indexing warning(s).
warning[std.ref]
  --> index.rst:13:5
  undefined label: 'nowhere-at-all'
Found 1 build warning(s).
warning[build.directive_unhandled]
  --> index.rst:11
  target-notes: 1 site rendered as a placeholder — no handler for this directive yet
Site written to: /home/me/project/_build/html
Root page: /home/me/project/_build/html/index.html
Build failed the quality gate: 2 warning(s) found (1 build, 1 indexing), and any warning-or-worse finding fails this build (the default --deny warning).
  Silence a code with ignore = ["<code>"] under [lint] in ubproject.toml.
  Or relax the bar: --deny error (errors only), --deny none (never fail).
Done with warnings in 0.02s

Important

Both counts feed one quality gate. Any warning-or-worse finding — from either count — fails the build with exit code 1, and prints its detail without needing -w. See Building for production for the bar and how to move it.

That last point is why you will usually see the detail without asking for it. Under the default bar, a warning both fails the build and prints itself, so a failing run is self-contained — you never have to repeat it with -w just to find out what went wrong. The flag comes into its own once the bar is lowered (--deny error, --deny none, or [build.html] deny), where warnings are reported without failing the build and the summary is otherwise all you would get.

Note

ubc build index does not share the second half of that behaviour: it gates the detail strictly on -w, so a failing build index prints only the counts.

Code blocks are syntax highlighted

Added in version 0.36.0: The colouring moved from the reader’s browser into the build.

A code-block, a literalinclude, a :: literal block and a fenced block are coloured during the build, from the language the block names, by a bundled collection of Sublime Text syntax definitions — around two hundred and twenty languages, the set the bat project curates, plus the grammars ubCode writes itself. Nothing is installed, nothing is downloaded and the reader’s browser runs no JavaScript for it: the colours are in the bytes of the page. ubc build pdf uses the same engine and the same language table, so a construct is coloured the same way on the site and on paper. An inline :code: chip with a :language: option is coloured too, on the site and on paper.

A language is named the way you already name it. python, py, bash, toml, rst, cypher and the rest resolve through ubCode’s own table; Languages lists every name you can write. text, none, plain, plaintext, raw and output are deliberately uncoloured and are not a failure, and a block that names no language at all is left alone. A name nothing answers to is reported as build.highlight_language_unknown, naming the token, counting the places it is written, offering the nearest name the build does know, and up to three of the positions; the code is drawn in full, without colours. That finding is a warning, which the default --deny warning treats as a failure, so a project that names a language this build has no grammar for stops building until the name is corrected, an alias is added, or the code is silenced with ignore under [lint].

Your own names for languages. [build.highlight] aliases maps a name you write to the language it should be coloured as, for every output format at once:

[build.highlight]
aliases = { mylang = "python", ourdsl = "text" }

See the configuration reference for the whole key.

Recolouring. Each run carries one hl- class — hl-keyword, hl-type, hl-function, hl-name, hl-built-in, hl-label, hl-attr, hl-variable, hl-number, hl-literal, hl-operator, hl-string, hl-escape, hl-comment, hl-section, hl-bullet, hl-link, hl-addition and hl-deletion, plus hl-strong and hl-emphasis for a bold or italic run that takes no colour of its own — and the classes are painted from the twenty-one --ubc-hl-* custom properties (hl-addition and hl-deletion each take a -fg and a -bg one). Redeclare those in an extra_css file to recolour the site, and ubc build pdf picks up the same change through its matching [build.pdf.theme.colors] roles. Both are part of the stable surface. [build.html.highlight] enabled = false draws every listing in one colour, as this builder did before it could highlight.

Note

Markdown and Jinja listings are coloured more sparsely than they were. The bundled grammars scope those two less finely than the browser highlighter did, so a .. code-block:: md sample shows fewer coloured words than it used to. Shell listings gained colour in the same move, and reStructuredText listings now gain it: they are coloured by a grammar written for ubCode.

Redirecting moved pages

Added in version 0.33.0.

When a page moves, every link anyone already has to it breaks. List the move and the build keeps the old URL working:

[build.html.redirects]
"ubc/introduction" = "basics/what_is_ubcode"
"ubc/installation" = "basics/installation"

The build writes a small forwarding page where the old one used to be, so ubc/introduction.html still resolves and carries the reader on to basics/what_is_ubcode.html — keeping the ?query they arrived with, and the #fragment too unless the entry names a section of its own. Nothing else about the site changes: the forwarding pages are ordinary output files, they are pruned when you delete the entry (in a directory the build owns), and a rebuild leaves an unchanged one untouched.

…and moved sections

A key may name a section too, for a heading that moved or was renamed:

[build.html.redirects]
"guide/setup#old-name" = "guide/setup#new-name"

A URL fragment never reaches the server, so this one works in the reader’s browser: the site’s script forwards a reader whose fragment resolves to nothing. A section that is still there always wins, so an entry for an anchor that has not really moved simply never fires. The two layers compose — if the page moved and the section moved, the reader still arrives in one step.

The full grammar, the six warnings a faulty entry raises, and what happens with no JavaScript are under redirects.

Building for production

Added in version 0.31.2.

Declare your project’s quality bar once, in ubproject.toml:

[build.html]
deny = "warning"

and let CI just run the command:

$ ubc build html

deny takes none, info, warning or error, and warning — the default, so the snippet above is what the build already does — fails on any warning-or-worse finding. error lets warnings through and fails only on errors; none never fails the threshold (an explicit --max-warnings budget still applies). It is one bar over both counts together: a broken cross-reference from the index and a degraded page from the render phase are both defects in the site you are about to publish.

The same bar is what ubc check and ubc build index have always applied, so the three commands now agree by default.

The site is still written. The gate is a judgement on the artifact, not a refusal to produce it — the pages, assets and search index are all on disk before the exit code is decided. A pipeline can therefore publish a failing build for inspection; the CI/CD guide has a worked GitHub Actions job that does.

Note

A ubc binary built from source without the embedded client bundles reports build.client_asset_missing — a property of that binary, not of your project (released binaries embed the bundles). When developing against such a build, silence it with ignore = ["build.client_asset_missing"] under [lint], or relax the bar for the run.

When it fires, the run names the knob that set the bar, and lists the findings without a second run:

$ ubc build html
Found 1 build warning(s).
warning[build.need_view_truncated]
  index:16: view directive needlist #0 was truncated: showing the first 2 of 3 needs ...
Site written to: /home/me/project/_build/html
Root page: /home/me/project/_build/html/index.html
Build failed the quality gate: 1 warning(s) found, and any warning-or-worse finding fails this build (build.html.deny = "warning" in ubproject.toml).
  Silence a code with ignore = ["<code>"] under [lint] in ubproject.toml.
  Or relax the bar: --deny error (errors only), --deny none (never fail).
Done with warnings in 0.42s

Overriding for one run. --deny on the command line wins over the configuration key, in both directions:

$ ubc build html --deny none     # I know — just build it
$ ubc build html --deny error    # errors only, without editing tracked config
$ ubc build html --deny warning  # strict, in a project that declared no bar

--max-warnings <N> adds a count budget over the two counts summed — a ratchet for working an existing project down to zero. When both are given the stricter one wins.

Narrowing beats relaxing. A code silenced through lint configuration — by code, by page or by message — never reaches the gate at all, which is usually a better answer than lowering the bar for the whole project. --lint-extend-ignore <CODES> does the same for a single run.

--strict-sources is a different axis and composes with all of this: it fails a build whose source tree moved underneath it, whatever the findings say.

Note

This gate is about the quality of your project’s content; it says nothing about the stability of the emitted markup.

Numbering sections, figures and equations

The site prints the numbers [build.numbering] assigns (see Numbering for every key): section numbers in headings, Fig. / Table / Listing labels on captions, (N) beside equations, and the same numbers in a :numref: or :eq:.

  1. Number the sections. Add :numbered: to the toctree that lists your chapters, as you would for Sphinx:

    .. toctree::
       :numbered:
    
       install
       usage
    

    Or number every chapter without touching the sources: [build.numbering] sections = { from = "all" }.

  2. Number the captions. Figures, tables and code blocks are numbered once their kind is switched on — Sphinx’s numfig, one switch per kind:

    [build.numbering]
    figure = { enabled = true }
    table = { enabled = true }
    code = { enabled = true }
    

    Labelled display equations are numbered already (equation.enabled is on by default).

  3. Refer to them. :numref:`fig-arch` prints Fig. 1.2, :numref:`Section {number} <install>` prints Section 1, and :eq:`energy` prints (1.3) — each a link to the item.

  4. Choose where the site prints numbers with [build.html.numbering] show:

    [build.html.numbering]
    show = { sidebar = true, prev_next = true, title = true }
    

    By default numbers appear in the content — headings, captions, equations, a page’s own toctree lists and the “on this page” contents — and not in the site’s navigation sidebar, its previous / next links or the browser tab’s title. Each switch governs exactly its own surface.

Rebuilds stay incremental. A page is rendered again when a number it prints changes — its own headings, captions and equations, the entries of its toctree lists, or the text of a :numref: / :eq: on it — so inserting a figure early in a chapter re-renders the pages whose later figures move, and the pages that refer to them, and no other. Turning show.sidebar, show.prev_next or show.title on or off re-renders no page at all: the navigation is assembled on every build. Every other numbering setting is part of the configuration, so changing it rebuilds the site.

The editor’s Rendered preview prints the same numbers, from the last completed index pass. While you type, a number is shown only where the page’s structure is still the one that pass saw:

  • after an unsaved edit that changes the sections — one added, removed, moved to another level, or newly marked unnumbered — no heading, caption, equation or toctree-list entry on the page shows a number until the next pass;

  • after one that adds, removes or moves a figure (a table, a code block, an equation), or moves one into another section, no item of that kind shows a number until the next pass;

  • sections reordered at the same level keep their numbers, renumbered for their new places, and an edit that changes no structure (prose, a caption’s wording) keeps every number;

  • the exception to the first two rules: an edit that only removes content at the end of the page keeps the numbers before it, since a number depends only on what comes before its item.

Without a license the preview shows only the first part of a page; the headings, captions and equations it shows carry the same numbers as on the site.

An unsaved change to the entries a toctree lists is not seen until the next pass: the numbers its documents take, and the headings after it, can be a pass behind.

Reference

What the site writes, against what sphinx-build writes:

  • A heading: <h2><span class="section-number">1.2. </span>Title, as Sphinx. The text after the number is [build.html.numbering] suffix (default ". ", Sphinx’s html_secnumber_suffix). On a page that also has .. sectnum::, the project number comes first and the sectnum number after it, as in Sphinx.

  • A toctree list and the sidebar put the number in a <span class="section-number"> inside the link, where Sphinx writes it as plain link text — so a stylesheet can style or hide it. The previous / next links do the same, as Sphinx does there. The page title is 1.2. Title, as Sphinx’s.

  • A caption: <span class="caption-number">Fig. 1.2 </span><span class="caption-text">…</span>, as Sphinx (the space is inside the first span). An unnumbered caption carries no caption-text span, where Sphinx writes one on every caption.

  • An equation: <span class="eqno">(1.2)<a class="headerlink" …>¶</a></span>, as Sphinx, with the equation’s permalink moved inside it. A numbered equation shows exactly one number: a multi-part block is typeset as aligned rather than align, and an explicit \tag{…} you wrote inside a numbered block is hidden (the block carries the class ub-eq-numbered). With show.equations = false it shows none.

  • An unnumbered block — unlabelled, unless [build.numbering.equation] all = true — shows no number, as with Sphinx’s default MathJax: KaTeX’s own automatic numbers are hidden on every block, for Sphinx’s align wrapping of a multi-part block and for an environment you wrote (\begin{equation}, \begin{align}) alike. An explicit \tag{…} you wrote is still drawn, as MathJax draws it. The site’s stylesheet does the hiding (div.math .katex .eqn-num, div.math .katex .mml-eqn-num), so a theme that replaces it should keep those two rules.

build.numbering_mismatch

A page rendered a different sequence of sections, figures, tables, code blocks or equations from the one its numbers were assigned over. Numbers are matched to items by position (an unnamed figure is numbered too), so the items that could not be matched print no number rather than a wrong one, and the page says how many. It should not happen; please report it.

Configuring the site

Everything about how the site looks and what it contains is configured under [build.html] in ubproject.toml:

[build.html]
title = "My Project Docs"
logo = "_static/logo.svg"
extra_css = ["_static/theme.css"]
extra_static = ["_static"]

extra_css names your stylesheets; extra_static publishes the files those stylesheets reference — background images, fonts, icons — copying a listed directory’s contents into the site’s _static/ directory with their structure preserved. Listing the whole _static directory above means a source file at _static/img/bg.png is published at _static/img/bg.png, so url(img/bg.png) in theme.css resolves. Each file is still published only once: the stylesheet and the logo are named by their own keys as well, and naming a file twice does not copy it twice.

See the HTML site configuration page for every option, and in particular:

Where it differs from Sphinx

A site built by ubc is not yet a drop-in replacement for the output of a full sphinx-build. The user-visible differences are registered, one entry each, on Differences from a Sphinx build — read that page before switching a published site over.