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:
arrayofstring(optional, default:[])Stylesheets to copy into the site and link on every page.
Each path is resolved relative to the
ubproject.tomlthat 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_failedand 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_cssat 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].namefor 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-logobelow to change that).A file that cannot be read is reported as the build warning
build.logo_read_failedand skipped. Unset — or the empty string — means no logo.- logo_dark
Type:
string(optional, default:"")A dark-theme variant of
logo.Shown instead of
logowhile 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 useslogo. Resolved and copied exactly likelogo(an unreadable file is the samebuild.logo_read_failedwarning), and meant to be set together with it (mirroring furo’slight_logo/dark_logopair); set alone, it behaves likelogoand is used everywhere, including print. Unset — or the empty string — meanslogo(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 warningbuild.favicon_read_failedand 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 asCopyright © 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
falseremoves the sidebar search box, thesearch.htmlresults 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
copyrightline (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:"
Search¶
Every site is built with full-text search unless you turn it off
(search.enabled above).
It is generated entirely at build time and needs no server:
open the site from a local folder — a file:// URL — and search still works,
which is what makes a built site usable on an air-gapped machine
or straight out of a ZIP file.
What you get¶
A search box at the top of the sidebar, on every page. Results appear as you type, grouped by page, with the matching words highlighted in a short excerpt.
Section-level results. A hit points at the section it was found in, not just at the page, so following it lands you at the right heading.
Need results, shown with a badge carrying the need’s type and status. A need’s id, type, title, status, tags and body text are all searchable, and typing a need id exactly (
REQ_001) puts that need at the top. An id that does not exist simply returns nothing for that part of the query — it never quietly falls back to some other need.A results page at
search.html, reached by pressing Enter or following the “more results” link. It accepts a?q=query string, so a search can be bookmarked or shared, and it offers filter chips for need type and status (choosing several values of one filter widens the results, choosing values of both narrows them).
How much is shown: the dropdown lists the first few results per page, for the first few pages, and links to the rest; the results page counts every match and renders up to 200 of them, saying so in its count line when there are more. The filter chips apply to the whole result set, not only to the part shown.
Query syntax¶
Query |
Meaning |
|---|---|
|
Both words must appear (in any field, in any order). |
|
Exclude anything containing |
|
Both words must appear; quoting is a convenient way to group them (word ORDER is not yet significant). |
|
The need with that id, first — plus anything else mentioning it. |
Words are matched by their stem, so requirement also finds
requirements and required.
Identifiers are matched whole and in parts,
so REQ_001 is findable as REQ_001, as req and as 001,
and PascalCase is findable as pascal or case.
A word you are still typing matches by prefix.
That includes id prefixes that would otherwise be too short to search:
T_ finds everything continuing T_ (such as the T_* need ids),
even though a bare T is below the two-character minimum.
Some words are not indexed at all:
very common ones (the, how, to …),
anything shorter than two characters,
and runs longer than 64 characters.
A query word like that is ignored rather than making the query fail,
so how to install searches for install,
python 3 for python,
and C++ parser for parser.
A word with nothing indexable in it at all — punctuation only, such as
!!! or a lone em dash — is a different matter:
there is no word there to look for,
so the query returns nothing rather than quietly searching for less
than you typed.
Excluding such a word (-!!!) is harmless:
an exclusion that matches nothing removes nothing.
Keyboard¶
/focuses the search box, unless you are already typing in a field. Where the box is off screen — a collapsed sidebar on a narrow window — the keystroke is left alone rather than swallowed, so it still reaches the browser and the page.Down/Upmove through the results, wrapping around;Upfrom nothing selected goes to the last result.Enteropens the highlighted result, or — with nothing highlighted — opens the full results page.Escapecloses the results, then leaves the box.
Files and size¶
Search adds three files to the site:
File |
Purpose |
|---|---|
|
The index itself. Loaded when a reader first engages with the search
box — or on |
|
The search interface: the engine and the UI, linked from every page. |
|
The results page. |
The index is one file, which is the right shape for most projects
but has a practical ceiling.
Above roughly 30,000 indexed items — or 8 MiB —
the build reports the warning build.search_index_large,
naming the actual counts:
the site still works, but the first search on a page will feel slow,
because the browser loads the whole index at once.
Until index splitting arrives, the way out is search.enabled = false.
Note
If your project already has a document called search,
your page wins: the generated results page is not written,
and the build reports build.search_page_conflict.
The search box still works; only its “more results” link
lands on your page instead.
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), andthe
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 |
|---|---|---|
|
fixed |
Body font stack. |
|
fixed |
Monospace font stack (code, literals). |
|
theme-aware |
Primary text colour. |
|
theme-aware |
Secondary text: captions, navigation entries, metadata. |
|
theme-aware |
Stronger border colour, for elements that need to read as edged (table cells, blockquote rules). |
|
theme-aware |
Page background. |
|
theme-aware |
Recessed background: the sidebar, the mobile header, table headers. |
|
theme-aware |
Hover background for interactive elements. |
|
theme-aware |
Default border/rule colour. |
|
theme-aware |
The accent colour: the current page in the sidebar, focus rings, and (by default) the active entry in “On this page”. |
|
theme-aware |
Link text. |
|
theme-aware |
Link text on hover. |
|
theme-aware |
Visited link text. |
|
follows |
The “On this page” entry for the section currently being read.
Defined as |
|
theme-aware |
Background of inline code and code blocks. |
|
theme-aware |
Foreground of inline code and code blocks. |
|
theme-aware |
Admonition body background. |
|
fixed |
Accent for |
|
fixed |
Accent for |
|
fixed |
Accent for |
|
fixed |
Accent for |
|
fixed |
Accent for |
|
fixed |
Accent for |
|
fixed |
Reserved accent for |
|
fixed |
Maximum width of the content column (default |
|
fixed |
Default corner radius. |
|
fixed |
Clearance height of the narrow-viewport header (default |
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 |
|
Brand |
|
Site navigation |
|
On this page |
|
Footer |
|
Narrow-viewport drawers |
|
Theme toggle |
|
Back to top |
|
Search |
|
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.