HTML site

The [build.html] section configures the static HTML site produced by ubc build html: which extra stylesheets to load, what the site is called, which logo to show, the footer’s copyright line, and whether to build the site’s search index.

Warning

ubc build html is experimental. The command is hidden from ubc build --help, and both the emitted markup and the theming surface described here may change before it is announced as stable.

Minimal example

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

Options

extra_css

Type: array of string (optional, default: [])

Stylesheets to copy into the site and link on every page.

Each path is resolved relative to the ubproject.toml that declares it (or may be absolute). Every file is copied into the site’s _static/ directory and linked after the built-in stylesheet and after any feature bundle (syntax highlighting, copy buttons), in the order listed — so a later file overrides an earlier one, and any of them overrides the built-in defaults.

Two files with the same name are both copied, under distinct names (custom.css, custom1.css, …), and a file listed twice is copied and linked once.

A file that cannot be read is reported as the build warning build.extra_css_read_failed and skipped; the rest of the site still builds, and the pages still link it — so the failure is visible in the browser rather than silently dropped.

Note

If the stylesheet lives inside your source directory, exclude it from source discovery (see source) so ubCode does not try to parse it as a document.

An extending configuration’s list is replaced whole, not merged — a child that sets extra_css at all sets the complete list.

title

Type: string (optional, default: "")

The site title.

It is shown as the sidebar heading and appended to each page’s browser title as page title site title. A page whose own title already equals the site title (typically the root document) is left unsuffixed.

When set, it overrides [project].name as the sidebar heading. Unset — or the empty string, which means the same thing — falls back to [project].name for the heading and leaves page titles unsuffixed.

logo

Type: string (optional, default: "")

An image file to show above the sidebar heading.

The path is resolved relative to the declaring ubproject.toml (or may be absolute), and the file is copied into the site’s _static/ directory. It is scaled to fit within the sidebar column (see .ub-brand-logo below to change that).

A file that cannot be read is reported as the build warning build.logo_read_failed and skipped. Unset — or the empty string — means no logo.

logo_dark

Type: string (optional, default: "")

A dark-theme variant of logo.

Shown instead of logo while the site renders in its dark theme — whether the reader chose it with the theme toggle or their system prefers it. When both images are set, printing uses logo. Resolved and copied exactly like logo (an unreadable file is the same build.logo_read_failed warning), and meant to be set together with it (mirroring furo’s light_logo / dark_logo pair); set alone, it behaves like logo and is used everywhere, including print. Unset — or the empty string — means logo (when set) is used in both themes.

favicon

Type: string (optional, default: "")

The site favicon — the icon browsers show in the tab (.ico, .svg, .png, …).

The path is resolved relative to the declaring ubproject.toml (or may be absolute); the file is copied into the site’s _static/ directory and linked as <link rel="icon"> from every page. A file that cannot be read is reported as the build warning build.favicon_read_failed and skipped. Unset — or the empty string — means no favicon.

hide_title

Type: boolean (optional, default: false)

Declare that the logo artwork already contains the site name, so the title text is not repeated beside it — logo-only branding wherever the shell brands the site (today: the sidebar heading; furo’s sidebar_hide_name).

Browser titles and the narrow-viewport header keep the title, and assistive technology still hears the site name (it becomes the logo image’s alternative text). With no logo configured either, the sidebar shows no heading at all — including its link back to the root page.

copyright

Type: string (optional, default: "")

The site copyright statement.

It is shown in each page’s footer as Copyright © {value} — for example, the value "2026, team useblocks" renders as Copyright © 2026, team useblocks. The footer only exists on pages with the site shell (a sidebar and/or an “On this page” column), so a page without either shows no copyright line. Unset — or the empty string — means no copyright line; the footer’s “Built with ubc” attribution is shown either way.

search.enabled

Type: boolean (optional, default: true)

Whether to build the site’s search index and ship its search interface — see Search for what that gives you.

[build.html.search]
enabled = false

Setting it to false removes the sidebar search box, the search.html results page and the index file from the site, and skips the indexing work during the build. Rebuilding an existing site after disabling search also deletes the files an earlier build wrote, so nothing stale is left behind.

Turn it off for a very large project whose index file has grown impractical (see the size warning below), or for a site that is only ever read through a host with its own search.

Built-in page behaviour

Beyond the options above, every page with the site shell also carries:

  • a footer at the end of the content column: the previous/next links, then a divider with the copyright line (when configured) and a muted “Built with ubc” attribution;

  • a back-to-top button — a small pill that appears at the top centre whenever you scroll back up, anywhere beyond the very top of the page (and drops below the narrow-viewport header where that is shown);

  • a skip-to-content link for keyboard users: the first tab stop on the page, hidden until focused, jumping straight to the main content;

  • print styling that hides the navigation chrome (the drawers, header, previous/next links and floating buttons) while keeping the content and the copyright line.

The responsive breakpoints follow furo’s ladder: the “On this page” column folds into an overlay drawer below 82em, the sidebar (and, for most pages, the narrow-viewport header) below 63em, and on very wide screens (97em and up) the whole page scales up ~10%. A page with no sidebar shows the header from 82em already, where its “On this page” column folds away.

Unhandled directives

Added in version 0.31.0.

ubCode renders a known set of directives with their real semantics and every other one as a readable generic placeholder — a labelled frame that still shows the directive’s name, options and rendered body. A directive ubCode does not recognise at all is reported as block.directive_unknown, but a directive it does recognise and simply has no renderer for — only, tabularcolumns, acks, and friends — used to render as a placeholder with no signal at all.

The build now reports those, aggregated per directive name for the whole build:

$ ubc build html -w
Found 2 build warning(s).
  [build.directive_unhandled] only: 47 sites rendered as placeholders
    — no handler for this directive yet (first api/core:118, api/core:204, guide:31)
  [build.directive_unhandled] tabularcolumns: 12 sites rendered as placeholders
    — no handler for this directive yet (first guide:52, guide:96, tables:14)

One line per directive name, most-used first, with the total number of placeholder sites and the first few distinct locations of them — so the output stays a handful of lines however many times a large project uses such a directive.

The count is what carries the multiplicity; the locations only tell you where to start looking, so a repeated one is listed once. Sites pulled in through include are counted and attributed to the page that includes them, but named without a line number (the line would refer to the included file, not the page), which is why several of them can collapse to a single bare page name:

[build.directive_unhandled] raw: 17 sites rendered as placeholders
  — no handler for this directive yet (at configuration/workflow)

Like every other build.* warning it is non-fatal (the site is complete, and the placeholders are the same honest output as before) and its detail is shown by --show-warnings / -w.

Two ways to read it:

  • as a coverage signal — if a directive you rely on appears here, its own rendering is not implemented yet, and the frame is what readers see;

  • as a regression guard — a name appearing (or a count jumping) after an upgrade or a content change is worth a look.

To silence it, use the ordinary lint configuration. The whole code:

[lint]
ignore = ["build.directive_unhandled"]

or just one directive, matched on the message (which always begins with the directive name):

[[lint.message-ignores]]
codes = ["build.directive_unhandled"]
contains = "only:"

Theming with extra_css

The built-in stylesheet is written in terms of CSS custom properties, so most re-branding is a matter of setting a handful of values rather than overriding rules.

There are two stable surfaces:

  • the --ubc-* custom properties (the token table below), and

  • the ub-* class names on the emitted markup (the vocabulary below).

Everything else — the specific rules, their order, their selectors — is an implementation detail and may change.

Note

The syntax-highlighting bundle owns a separate --ubc-hl-* namespace, which is not part of this contract.

Worked example

A complete re-brand: one accent colour per theme, plus an underline-style link treatment.

# ubproject.toml
[build.html]
extra_css = ["_static/theme.css"]
/* _static/theme.css */

/* Light (the default) — set the tokens on :root. */
:root {
  --ubc-brand: #583eff;
  --ubc-link: #1a1c1e;
  --ubc-link-hover: #1a1c1e;
  --ubc-link-visited: #1a1c1e;
}

/* Dark. Two selectors, because the site has two ways of being dark:
   the reader picked dark with the theme toggle, or the reader's system
   prefers dark and they have not overridden it. */
@media not print {
  :root[data-theme="dark"] {
    --ubc-brand: #e4ff3e;
    --ubc-link: #d6d9dd;
    --ubc-link-hover: #d6d9dd;
    --ubc-link-visited: #d6d9dd;
  }

  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
      --ubc-brand: #e4ff3e;
      --ubc-link: #d6d9dd;
      --ubc-link-hover: #d6d9dd;
      --ubc-link-visited: #d6d9dd;
    }
  }
}

/* Rules, for anything the tokens do not cover. */
.ub-content a {
  text-decoration-color: var(--ubc-brand);
  text-decoration-thickness: 0.1em;
}

The theme is applied by setting data-theme="dark" or data-theme="light" on the <html> element; with the attribute absent, the reader’s system preference decides. Match that in your own rules and your theme will follow the toggle.

Important

Set every theme-aware token (see the table below) in all three blocks, exactly as above — even a value that would otherwise be identical in both themes.

The built-in stylesheet declares its dark values with the two selectors :root[data-theme="dark"] and :root:not([data-theme="light"]), both of which are more specific than a plain :root. A token you set only on :root therefore wins in light mode and is silently overridden back to the built-in value in dark mode.

Fixed tokens have no dark declaration to lose to, so for those :root alone is enough.

Custom properties

All tokens are declared on :root. The theme-aware ones are redefined for dark mode by the built-in stylesheet, so a theme that changes them must set both (as in the example above); the fixed ones have a single value in both themes.

Property

Kind

Purpose

--ubc-font

fixed

Body font stack.

--ubc-font-mono

fixed

Monospace font stack (code, literals).

--ubc-fg

theme-aware

Primary text colour.

--ubc-fg-muted

theme-aware

Secondary text: captions, navigation entries, metadata.

--ubc-fg-border

theme-aware

Stronger border colour, for elements that need to read as edged (table cells, blockquote rules).

--ubc-bg

theme-aware

Page background.

--ubc-bg-soft

theme-aware

Recessed background: the sidebar, the mobile header, table headers.

--ubc-bg-hover

theme-aware

Hover background for interactive elements.

--ubc-border

theme-aware

Default border/rule colour.

--ubc-brand

theme-aware

The accent colour: the current page in the sidebar, focus rings, and (by default) the active entry in “On this page”.

--ubc-link

theme-aware

Link text.

--ubc-link-hover

theme-aware

Link text on hover.

--ubc-link-visited

theme-aware

Visited link text.

--ubc-toc-active

follows --ubc-brand

The “On this page” entry for the section currently being read. Defined as var(--ubc-brand), so recolouring the brand recolours this too; set it to decouple the two.

--ubc-code-bg

theme-aware

Background of inline code and code blocks.

--ubc-code-fg

theme-aware

Foreground of inline code and code blocks.

--ubc-admonition-bg

theme-aware

Admonition body background.

--ubc-note

fixed

Accent for note admonitions.

--ubc-tip

fixed

Accent for tip / hint admonitions.

--ubc-important

fixed

Accent for important admonitions.

--ubc-warning

fixed

Accent for warning / caution / attention admonitions.

--ubc-danger

fixed

Accent for danger / error admonitions, and for error notices.

--ubc-seealso

fixed

Accent for seealso admonitions.

--ubc-todo

fixed

Reserved accent for todo admonitions. Declared but not yet referenced by any built-in rule.

--ubc-content-width

fixed

Maximum width of the content column (default 46em).

--ubc-radius

fixed

Default corner radius.

--ubc-header-height

fixed

Clearance height of the narrow-viewport header (default 3rem): how far jumped-to anchors and the back-to-top button stay below it. It does not size the header itself.

Class vocabulary

The class names the site shell emits. Directive-level classes (ub-need*, ub-video*, ub-needflow*, …) come from the shared HTML writer and are documented with those directives.

Area

Classes

Layout

ub-page (the column container), ub-sidebar-drawer / ub-sidebar, ub-main-column, ub-toc-drawer / ub-toc, ub-content (the <main>), ub-skip-to-content (the keyboard skip link). The drawers are two-layer: the outer *-drawer aside absorbs the width beyond the centred ensemble (and is the off-canvas panel at narrow widths), the fixed-width inner container is the sticky scroll column.

Brand

ub-brand (the block), ub-brand-link (the link to the root document), ub-brand-logo (the logo image), with ub-logo-light / ub-logo-dark variant classes when a distinct logo_dark pairs with logo (the stylesheet displays exactly one per theme).

Site navigation

ub-nav (the <nav>), ub-nav-caption, ub-nav-list, ub-nav-item, ub-nav-link, ub-nav-external, ub-current and ub-current-ancestor (the active trail), ub-has-children, ub-nav-toggle / ub-nav-toggle-label / ub-nav-children (the collapse control).

On this page

ub-page-toc (the <nav>), ub-page-toc-title, ub-toc-list, and ub-active on the entry for the section being read (also marked aria-current="location"). ub-scrollspy-end is a zero-height, aria-hidden marker the script appends at the end of the document so it can tell when the reader has reached the bottom; it renders nothing.

Footer

ub-footer (the <footer>, on every page with the shell), containing the previous/next ub-relations nav — ub-rel with ub-rel-prev / ub-rel-next, ub-rel-dir, ub-rel-title — and the bottom row ub-footer-bottom / ub-footer-details with ub-copyright (only when configured) and the ub-built-with attribution.

Narrow-viewport drawers

ub-drawer-toggle (the hidden checkboxes ub-drawer-nav / ub-drawer-toc), ub-mobile-header with the ub-mh-nav / ub-mh-toc markers and its three slots ub-mh-left / ub-mh-center / ub-mh-right (ub-scrolled is added by the JavaScript once the page scrolls, for the header’s shadow), ub-mobile-btn / ub-mobile-nav-open / ub-mobile-toc-open / ub-mobile-icon, ub-mobile-title, ub-content-icons / ub-toc-content-open (the in-content on-this-page opener, shown where the sidebar column hides the header), ub-scrim with ub-scrim-nav / ub-scrim-toc.

Theme toggle

ub-theme-toggle on both instances, plus ub-theme-toggle-column (in the “On this page” column) or ub-theme-toggle-float (the fixed fallback, marked ub-theme-toggle-paired when a column instance also exists), and ub-theme-toggle-btn on the button itself.

Back to top

ub-back-to-top on the JavaScript-injected pill, revealed by the ub-show-back-to-top class the script toggles on <html>.

Search

ub-search-form (the injected box, with ub-search-sidebar or ub-search-page-form), ub-search-input, ub-search-dropdown (the listbox, hidden when closed) with ub-search-group-label, ub-search-option (ub-search-active on the highlighted one), ub-search-empty and ub-search-all; one result is ub-search-hit / ub-search-hit-head / ub-search-title / ub-search-crumb / ub-search-excerpt with ub-search-mark on the matched words, plus ub-search-badge and ub-search-id on a need result. The results page adds ub-search-count, ub-search-facets / ub-search-facet / ub-search-chip (ub-search-chip-on when active), ub-search-group / ub-search-group-title / ub-search-list / ub-search-item / ub-search-more, ub-search-noscript, and the visually-hidden ub-search-announce live region.

Note

The theme toggle, the back-to-top button, the header’s ub-scrolled shadow, the ub-active marking, and the whole search interface are added by the site’s JavaScript, not present in the served HTML — so a page viewed with JavaScript disabled carries none of them.