Building a static HTML site

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.

Caution

ubc build html is in alpha.

The command works and is under active development, but the emitted markup, the theming surface and the available options may still change between releases, so it is not yet recommended for production builds. Pin your ubc version if you depend on the output, and see Differences from a Sphinx build for what a site built this way does not yet match in a full sphinx-build.

We would love your feedback.

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.

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 — for example -c 'build_tags = ["html"]'.

-v / --verbose additionally names the client-asset bundle the binary carries — the Mermaid, highlight.js and KaTeX versions, and the bundle’s content hash — which is what to quote when the rendered output looks stale or unexpected.

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.

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). The build only prunes a directory it created itself (or one an earlier ubc build html made); pointed at some other non-empty directory it writes the pages and warns instead of deleting anything.

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. 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 build warning(s).
warning[build.directive_unhandled]
  acks: 1 site rendered as a placeholder — no handler for this directive yet (at index:35)
Site written to: /home/me/project/_build/html
Found 1 indexing warning(s).
warning[std.ref]
  --> index.rst:39:10
  undefined label: 'nowhere-at-all'
Root page: /home/me/project/_build/html/index.html
Done in 0.08s

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.

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 and #fragment they arrived with. Nothing else about the site changes: the forwarding pages are ordinary output files, they are pruned when you delete the entry, and a rebuild leaves an unchanged one untouched.

The full grammar, the four 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

ubc build html is still in alpha (see the caution at the top of this page). This gate is about the quality of your project’s content; it says nothing about the stability of the emitted markup.

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.