Add the Bora theme to a new or existing Hugo project.
Requirements
Hugo v0.141.0 or later (standard build, extended is not required)
Dart Sass on the build machine (the theme compiles SCSS with the dartsass transpiler)
Tip
Install Dart Sass once per machine/CI image: download a release from
sass-lang.com/dart-sass and put sass on the PATH.
Node.js is not required. All JavaScript is pre-bundled in the theme.
Option 1: Hugo Module (recommended)
If your site is not already a Hugo module, initialise it first:
The theme reads two files from your site root (next to hugo.toml):
file
purpose
site.json
titles, logo, themes, menus, footer. The theme’s configuration (reference)
static/versions.json
minimal version manifest for local development. In production the release pipeline generates it from git tags (versioning)
Your hugo.toml also needs a few output-format blocks. The Hugo configuration page has the complete copy-paste setup.
Important
Without site.json the build stops with a clear error. The fastest start is
copying the exampleSite/ directory from the theme repository, which is the
site you are reading right now.
From empty site to running documentation in five steps.
Every snippet below is copy-paste ready. The exampleSite/ in the theme repository is this exact setup, fully assembled.
1. Configure Hugo
Copy the output-format blocks from Hugo configuration into your hugo.toml and set your baseURL. Deploying under a sub-path (GitHub/GitLab Pages) only requires changing baseURL.
You always write under one fixed directory called the working-docs token, with a language directory below it: docs/<token>/<language>/…. Versions are created later from git tags (versioning).
The token is yours to choose. This site uses __vcurrent__, the snippets below use __mydocs__. Pick a string that will never occur naturally in your content, because releasing literally string-replaces it in links. If it contains capitals, set disablePathToLower = true in hugo.toml.
site.json and static/versions.json are not watched by the dev server.
Restart hugo server after editing them.
You now have: a themed homepage at /, your docs under docs/__mydocs__/en/, working search (Ctrl/⌘ K), light/dark switching and a styled 404. Releasing versions from tags is covered in versioning. Next up: writing pages.
Configuration
site.json
Complete reference for every site.json key, the theme's configuration file.
site.json lives in your site root, next to hugo.toml. It is validated at build time: a missing file or an empty themes list fails the build with a clear message. Everything else is optional with sensible defaults.
Note
The dev server does not watch site.json. Restart hugo server after changes.
site name used as the topbar fallback logo, <title>, and social cards
themes
available color themes. Each id maps to static/docs/css/theme-<id>.css. default: true marks the eager-loaded default of each family (light/dark). See Themes
Branding & navigation
key
example
meaning
logo
"/docs/media/svg/logo.svg"
topbar/homepage logo. Falls back to title text if the file doesn’t exist
favicon
"/docs/media/svg/favicon.svg"
favicon path
defaultLanguage
"en"
UI language for pages without a path language (homepage, 404) and the x-default hreflang
footer content. :YEAR: becomes the current year. Icons: github, linkedin
Sub-path deployments (GitHub/GitLab Pages)
Write internal URL values (menu.links, landing.ctas) without a leading slash, e.g. "changelog/" rather than "/changelog/". Hugo resolves slash-less values against your baseURL, so the same site.json works at the domain root and under https://host/project/. The paths inside versions.json are navigated to verbatim, so on a sub-path deployment they must include the sub-path. The release pipeline’s generated manifest handles that automatically.
SEO & social
key
example
meaning
social.cover
"/docs/media/banner.png"
default Open Graph/Twitter banner (per-page override: cover front matter)
dismissible bar above the topbar. Markdown text. A new id re-shows it to everyone
Homepage
json
"landing":{"tagline":"Versioned, multilingual product documentation.","ctas":[{"label":"Get started","url":"/docs/v1_0_0/en/getting-started/","primary":true},{"label":"Changelog","url":"/changelog/"}],"features":[{"title":"Instant search","text":"Client-side, no server required."}]}
All three sub-keys are optional. Without the block the homepage renders the title, description and a “Get started” button.
hugo.toml
The complete Hugo configuration the theme needs, copy-paste ready.
This is the full hugo.toml for a Bora site. Copy it, change baseURL and the module path style to your setup, and you are done:
hugo.tomltoml
baseURL="https://docs.example.com/"languageCode="en-us"title="My Docs"disableKinds=["taxonomy","term"]# no tags/categories in a docs site[markup.highlight]noClasses=falsestyle="github"# ── Output formats used by the theme ────────────────────────────────[outputFormats.SearchIndex]# section-chunked search indexmediaType="application/json"baseName="searchindex"isPlainText=truenotAlternative=true[outputFormats.LLMS]# llms.txt for AI assistantsmediaType="text/plain"baseName="llms"isPlainText=truenotAlternative=true[mediaTypes."text/markdown"]suffixes=["md"][outputFormats.Markdown]# raw markdown per page (linked from llms.txt)mediaType="text/markdown"baseName="index"isPlainText=truenotAlternative=true[outputs]home=["HTML","SearchIndex","LLMS"]page=["HTML","Markdown"][module][[module.imports]]path="gitlab.com/natitec_public/bora"
What each piece does
Setting
Why
markup.highlight.noClasses = false
Hugo emits CSS classes so theme-syntax.css can color code per theme
outputFormats.SearchIndex + home output
emits /searchindex.json. Without it, search shows “Search is unavailable”
Themes
How color themes work, and how to add your own using Material Theme Builder.
The example site ships four themes: Light and Dark (the defaults, a cool blue that matches the Bora logo), plus Green Light and Green Dark. Switch the active family with the toggle in the topbar, and pick any specific theme from the settings drawer. Your choice persists across visits and carries from the homepage into the docs.
How it works
Each theme is one CSS file at static/docs/css/theme-<id>.css. It holds the Material color variables, scoped under [data-theme="<id>"].
site.json registers each theme. Switching themes only changes the data-theme attribute on the <html> element, and the matching file supplies the colors.
Only the default theme in each family loads up front (one light, one dark). Other variants load the first time someone picks them. A returning visitor’s saved choice is applied before the first paint, so there is no flash and nothing extra is downloaded.
The dark field groups a theme into the light or dark family. The topbar toggle switches between the two families’ defaults. The settings drawer lists every registered theme.
Important
A dark theme’s id must start with dark. The dark code-block palette and the
dark treatments of UI elements (admonitions, badges, version banners) apply to
any [data-theme^="dark"], so name dark themes like dark, dark-ocean or
dark-green. Light themes have no prefix requirement. The dark: true flag and
the dark id prefix do two different jobs: the flag groups the theme for the
toggle, and the prefix switches the code and component styling to their dark form.
Adding your own theme
Generate the palette with Material Theme Builder. Pick your brand color and export as Web (CSS). The export gives you a light block and a dark block, each wrapped in a class such as .light or .dark.
Re-scope each block to the attribute the theme reads. Replace the exported class selector with [data-theme="<id>"], and give the dark block an id that starts with dark. The variables themselves do not change.
static/docs/css/theme-ocean.csscss
[data-theme="ocean"]{--md-sys-color-primary:rgb(0101142);--md-sys-color-on-primary:rgb(255255255);/* the rest of the exported variables, unchanged */}
Save each block as its own file at static/docs/css/theme-<id>.css, for example theme-ocean.css and theme-dark-ocean.css. Your site’s static/ folder merges with the theme’s, so you do not need to fork anything.
Each theme then appears in the settings drawer and loads the first time it is picked. To make one the family default, set "default": true on it and remove that flag from the theme it replaces.
Tip
High-contrast and brand variants work the same way. Material Theme Builder can
export them all at once. Each one is a single file plus a single line in the registry.
Related knobs
themeColor in site.json tints the browser UI per scheme (reference).
Code block colors come from theme-syntax.css (Chroma classes). Override it from your site’s static/ the same way.
Warning accents (admonitions, banners) default to amber. Override with --nti-color-warning inside your theme file.
Languages
Add a documentation language: content tree, UI strings, changelog.
Languages are plain directories. The theme does not use Hugo’s multilingual mode. Adding German takes three steps:
1. Content tree
text
content/docs/__mydocs__/ ← your working-docs token
├── en/ …
└── de/ ← new
├── _index.md ← title: "Deutsch"
└── …
From the next tag onward, the release pipeline detects the new directory and lists it in the generated manifest automatically.
2. Register in the local-dev manifest
So the pickers also show it while writing locally, add it to the minimal static/versions.json:
The language picker, search scope filter and hreflang links pick it up automatically. Pages link their translations only where the same page actually exists.
3. Translate the UI strings
The chrome (“Search…”, “On this page”, badge labels, error messages) reads from data/i18n/<lang>.toml. Copy the English file from the theme (data/i18n/en.toml) into your site as data/i18n/de.toml and translate the values:
data/i18n/de.tomltoml
search_placeholder="Dokumentation durchsuchen…"back_to_top="Nach oben"on_this_page="Auf dieser Seite"# … keep every key. Missing keys fall back to English.
The <lang> filename must match the language directory name. Site data files override the theme’s, so you can also tweak English wording the same way.
Note
The page’s language comes from its path (docs/<version>/<lang>/…), so the
right strings apply per page automatically, even when versions mix languages.
Changelog languages
The changelog is also per-language: add content/changelog/de/ with the same _index.md pattern as English.
Features
Every Bora feature is documented here. Each page explains what it does and includes working examples you can copy.
Front matter reference and how headings drive navigation, search and deep links.
Every page in Bora is a Markdown file. Hugo converts it to HTML and the theme wires it into the sidebar, table of contents and search index automatically.
Front matter
Every page needs at least a title. Everything else is optional:
yaml
---title:"Page Title"description:"One-line summary for cards, search results and link previews."weight:10cover:"/docs/media/special-banner.png"draft:false---
Field
Description
title
Page heading, sidebar label and search result title.
description
Card subtitle on section pages, search snippet, <meta name="description"> and social card text.
weight
Sidebar and prev/next ordering. Lower values appear first.
cover
Per-page Open Graph banner, overriding social.cover from site.json.
draft
Drafts build only with hugo server -D and get a badge on section cards.
Headings
Use ## (h2) and ### (h3) to structure pages. The theme turns them into:
the “On this page” column on wide screens, with the current section highlighted while scrolling
search results that deep-link to the exact section, shown as “Page > Heading”
anchor links: hover any heading and click the link icon to copy the URL
#### and deeper headings render normally but stay out of the table of contents and search chunks. That is a signal to keep page structure flat.
Section index pages
A directory needs an _index.md to appear in the sidebar and get a card grid on wide screens. The minimal version:
yaml
---title:"Section Title"description:"What this section covers."weight:10---
You can add body content below the front matter and it appears above the card grid.
Everything you can put on a page
Admonitions: note, tip, important, warning and caution callouts
Shortcodes: diagrams, PDF embeds, OpenAPI, steps, cards and more
Standard Markdown works too: tables, blockquotes, task lists and \``mermaid` diagrams all render out of the box.
Admonitions
Callout boxes with GitHub-compatible syntax: note, tip, important, warning, caution.
Admonitions use GitHub’s alert syntax, so the same Markdown renders as callouts here and on GitHub and GitLab:
markdown
> [!NOTE]
> Useful background information.
The five types
Note
Background information the reader should notice in passing.
Tip
A best practice or shortcut.
Important
Must-read information for the task to succeed.
Warning
Something that can go wrong.
Caution
Destructive consequences ahead.
Custom titles
Add the title on the marker line:
markdown
> [!TIP] Name your versions consistently
> The label line replaces the default "Tip".
Name your versions consistently
The label line replaces the default “Tip”. Icon and colors stay.
Rich content inside
Any Markdown works inside the body: multiple paragraphs, lists, links and code blocks.
markdown
> [!WARNING]
> First paragraph with **bold** and `inline code`.
>
> ```bash
> echo "code blocks too"
> ```
>
> - and lists
> - work fine
Warning
First paragraph with bold and inline code.
bash
echo"code blocks too"
and lists
work fine
Plain blockquotes (no [!…] marker) keep their own separate quote styling:
A regular quote, unaffected by admonition styles.
Notes
Default labels translate per language via data/i18n. See languages.
All five types map to distinct icon and color combinations that follow the active theme.
Code Blocks
Syntax highlighting, filename titles, highlighted lines, line numbers, wrap toggle and Mermaid diagrams.
Highlighting happens at build time via Hugo’s Chroma engine, so no JavaScript is needed for colors. The palette follows the active theme. Every block gets a copy button and a line-wrap toggle in its header bar automatically.
Language badge
markdown
```go
packagemainfuncmain(){println("hello")}```
go
packagemainfuncmain(){println("hello")}
Filename title
markdown
```go {title="main.go"}
package main // the bar shows main.go next to the language badge
```
main.gogo
packagemain// the bar shows main.go next to the language badge
Highlighted lines
markdown
```yaml {title="site.json themes" hl_lines=[2,3]}
themes:
- id: light # these two lines
default: true # are highlighted
- id: dark
```
site.json themesyaml
themes:- id:light # these two linesdefault:true# are highlighted- id:dark
Line numbers
markdown
```bash {linenos=true}
hugo mod get gitlab.com/natitec_public/bora
hugo server
```
bash
1
2
hugo mod get gitlab.com/natitec_public/bora
hugo server
Wrap toggle
Click the wrap button (left of copy) on this block. The long line soft-wraps instead of scrolling:
text
This deliberately long single line would normally force a horizontal scrollbar inside the code block unless the wrap toggle is switched on, in which case it wraps within the block borders.
Mermaid diagrams
A ```mermaid block renders as a diagram. The renderer loads only on pages that use it and re-renders when the theme switches:
flowchart LR
A[Markdown] --> B(Chroma)
B --> C{Theme CSS}
C -->|light| D[GitHub palette]
C -->|dark| E[Dark palette]
Combining options
All options combine freely on one block:
markdown
```yaml {title="example.yaml" hl_lines=[2] linenos=true}
key: value
highlighted: true
another: value
```
example.yamlyaml
1
2
3
key:valuehighlighted:trueanother:value
Images and Links
Figures with captions, click-to-zoom lightbox, remote image fetching and link validation.
Plain images and figures
Standard Markdown. Add a title to get a centered figure with a caption:
markdown


Local SVGs are inlined at build time and follow the theme colors. Raster images get width and height attributes at build time so the page does not shift while loading.
Lightbox
Every content image is click-to-zoom. Press Esc or click anywhere to close. Try it:
Click the image to zoom. This caption comes from the title attribute.
Images wrapped in a link keep their link behavior and are excluded from the lightbox.
Remote images
Remote URLs are downloaded at build time and served from your site. A dead URL never breaks the build. The theme logs a warning and falls back to a plain <img> the browser loads at view time. Severity is configurable:
Set error to make broken internal links fail the CI pipeline.
External links open in a new tab and get a small external icon automatically: Hugo documentation.
Same-page anchors work as expected: back to top. The target heading flashes on arrival so readers see where they landed.
Badge
Inline pill labels for annotating features, status and version information.
The badge shortcode renders a small inline pill label. Use it in prose, headings and table cells to annotate feature status, API stability, version requirements and similar metadata. All parameters are named.
Basic usage
text
Released in {{< badge text="v0.8.0" >}} and later.
Released in v0.8.0 and later.
Variants
The variant parameter controls the color. All variants derive their color from the theme palette and adapt automatically in light and dark mode.
Responsive grid of linked or static cards for section overviews and feature grids.
The cards shortcode renders a responsive grid of cards. Each inner card shortcode defines one card with a title, optional body text, optional icon and an optional link that makes the whole card clickable.
Basic usage
text
{{< cards >}}
{{< card title="Getting started" >}}
Install and configure Bora in under five minutes.
{{< /card >}}
{{< card title="Shortcodes" >}}
Embed diagrams, math, file trees, and more.
{{< /card >}}
{{< card title="Theming" >}}
Switch between light and dark, or add your own palette.
{{< /card >}}
{{< /cards >}}
Getting started
Install and configure Bora in under five minutes.
Shortcodes
Embed diagrams, math, file trees, and more.
Theming
Switch between light and dark, or add your own palette.
With icons
Pass any emoji or Unicode character as icon:
text
{{< cards >}}
{{< card title="Getting started" icon="🚀" href="/docs/v1_0_0/en/getting-started/" >}}
Up and running in minutes.
{{< /card >}}
{{< card title="Features" icon="⚙️" href="/docs/v1_0_0/en/features/" >}}
Diagrams, math, file trees, steps, and more.
{{< /card >}}
{{< card title="Search" icon="🔍" >}}
Client-side full-text search, no server required.
{{< /card >}}
{{< /cards >}}
The grid uses auto-fill with a 220 px minimum column width, so it adapts from one column on narrow screens to three or four on wide ones.
Cards without href are <div> elements. Cards with href are <a> elements with full keyboard and screen-reader semantics.
Inner content is rendered as Markdown, so you can include inline code, links and emphasis.
Columns
Place two or three blocks of content side by side in a responsive grid.
The columns shortcode arranges content in a horizontal grid. Separate each column with <---> on its own line. On narrow screens the columns stack vertically automatically.
Two columns
text
{{< columns >}}
**Option A**
Works well for small datasets with simple queries.
<--->
**Option B**
Better for high-volume writes and concurrent connections.
{{< /columns >}}
Option A
Works well for small datasets with simple queries.
Option B
Better for high-volume writes and concurrent connections.
Three columns
text
{{< columns >}}
Install the CLI:
```shell
npm install -g mybundle
```
<--->
Configure your project:
```shell
mybundle init
```
<--->
Run the build:
```shell
mybundle build
```
{{< /columns >}}
Install the CLI:
shell
npm install -g mybundle
Configure your project:
shell
mybundle init
Run the build:
shell
mybundle build
Custom ratio
Use ratio to control relative widths. The value maps directly to CSS fr units, so "2:1" gives the left column twice the space of the right.
text
{{< columns ratio="2:1" >}}
A wider left column for the main explanation or code sample.
<--->
A narrower right column for a note, tip, or screenshot.
{{< /columns >}}
A wider left column for the main explanation or code sample.
A narrower right column for a note, tip, or screenshot.
Parameters
Parameter
Default
Description
ratio
(none)
Column width ratio. Use colon-separated integers such as "2:1" or "1:2:1".
Notes
Any number of columns is supported, though two or three work best on most screen widths.
Each column renders full Markdown including code blocks, lists, badges and other shortcodes.
Columns collapse to a single vertical stack on screens narrower than 768 px.
Details
Collapsible sections using the native HTML details element. No JavaScript required.
The details shortcode wraps content in a native HTML <details>/<summary> element. No JavaScript is needed. The browser handles expand and collapse.
Usage
Pass the summary title as the first argument:
text
{{< details "What is Bora?" >}}
Bora is a Hugo theme for technical documentation.
{{< /details >}}
What is Bora?
Bora is a Hugo theme for technical documentation.
Open by default
Add open=true to render the section expanded on load:
text
{{< details title="Already open" open=true >}}
This section is expanded by default.
{{< /details >}}
Already open
This section is expanded by default.
Markdown inside
The inner content is rendered as full Markdown, so you can include headings, lists, code blocks and other shortcodes:
text
{{< details "Configuration options" >}}
| Option | Default | Description |
|--------|---------|-------------|
| `baseURL` | (none) | The root URL of your site. |
| `title` | (none) | Site title shown in the browser tab. |
{{< /details >}}
Configuration options
Option
Default
Description
baseURL
(none)
The root URL of your site.
title
(none)
Site title shown in the browser tab.
Parameters
Parameter
Type
Default
Description
First positional / title
string
"Details"
Text shown in the summary bar.
open
bool
false
Start the section expanded. When used, pass title as a named parameter too.
draw.io
Embed and render interactive draw.io diagrams with zoom and pan.
The drawio shortcode embeds a draw.io diagram directly in the page. Readers can zoom, pan and navigate layers without leaving the documentation. The draw.io viewer script loads only on pages that contain at least one diagram.
File location
Where you place the .drawio file determines how it is loaded.
Path style
Where the file lives
How it is loaded
diagram.drawio (no leading /)
Next to the page’s index.md as a page bundle resource
Read at build time, embedded inline, works on private sites
/path/to/diagram.drawio (leading /)
In assets/ at that path
Read at build time, embedded inline, works on private sites
Fallback (file not found in bundle or assets)
In static/ at that path
Fetched at runtime, public sites only
Page bundle resources sit in the same directory as the page’s index.md. For a page at content/docs/en/guides/setup/index.md, a file at content/docs/en/guides/setup/diagram.drawio is referenced simply as diagram.drawio.
Build-time embedding means the diagram XML is written directly into the HTML. No HTTP request is made when the reader opens the page, so the embed works even on private GitLab Pages or any other auth-gated host.
Usage
text
{{< drawio "diagram.drawio" >}}
Or with an absolute path from assets/:
text
{{< drawio "/docs/files/architecture.drawio" >}}
Live example
The demo file lives at exampleSite/assets/docs/files/example.drawio in the theme repository and is embedded inline at build time.
text
{{< drawio "/docs/files/example.drawio" >}}
Use the toolbar to zoom in, zoom out, and fit the diagram to the container. You can also pan by clicking and dragging.
Parameters
Parameter
Description
1st positional
Path to the .drawio file. Relative paths resolve as page bundle resources. Absolute paths (leading /) resolve from assets/.
Notes
The viewer script (drawio-viewer-static.min.js) is bundled with the theme and works fully offline.
Diagrams load after page JavaScript runs, so a brief loading state is normal.
Interactive embeds are replaced with a static note in the printable handbook.
File tree
Styled directory tree for showing project or repository structure.
The filetree shortcode renders a styled directory tree. Write the tree using standard box-drawing characters. Folder names (ending with /) are highlighted in the primary color and tree connector characters are rendered in a muted tone.
Use the tree command to generate the input on Linux and macOS:
shell
tree -F --noreport content/
On Windows (PowerShell):
powershell
tree/Fcontent
Copy the output directly into the shortcode body.
Common box-drawing characters
Character
Unicode
Meaning
├──
U+251C U+2500 U+2500
Branch (more items follow)
└──
U+2514 U+2500 U+2500
Last branch
│
U+2502
Vertical connector
Most terminals and editors insert these automatically when using tree.
Notes
Lines ending with / are treated as folders and styled with the primary color.
All other non-blank lines are treated as files.
Horizontal scrolling is enabled so long paths do not break the layout.
Math
Render LaTeX math expressions in your docs using KaTeX.
Bora renders LaTeX math via KaTeX (v0.17.0). Both display blocks and inline expressions are supported. KaTeX loads only on pages that use it, so pages without math carry no extra weight.
Display math
Use a fenced code block with the math language identifier. KaTeX renders it as a centered block equation:
For inline math, add math: true to the page front matter and enable the Goldmark passthrough extension in hugo.toml. Then write $...$ for inline expressions and $$...$$ for display expressions directly in prose.
With passthrough enabled and math: true in front matter, you can write:
markdown
The quadratic formula is $x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$.
Which renders as: The quadratic formula is $x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$.
Tip
Use math: true in front matter only on pages that contain inline $...$ math. Pages that only use ```math ``` blocks do not need it. The render hook sets the flag automatically.
How KaTeX loads
KaTeX (JS, CSS and fonts) is loaded only on pages where math is detected. The render hook for ```math ``` blocks sets the flag automatically. For inline math, the math: true front matter triggers the load. Neither the JS nor the CSS appears on pages without math.
OpenAPI
Full API reference rendered from an OpenAPI or Swagger spec, offline and theme-aware.
The openapi shortcode renders an OpenAPI 3.x (or Swagger 2) spec as a browsable API reference using a vendored Redoc bundle. There is no CDN dependency, so it works in air-gapped deployments. The bundle loads only on pages that use the shortcode and the reference re-renders when the theme switches.
File location
Where you place the spec file determines how it is loaded.
Path style
Where the file lives
How it is loaded
api.yaml (no leading /)
Next to the page’s index.md as a page bundle resource
Parsed at build time, embedded inline, works on private sites
/path/to/api.yaml (leading /)
In assets/ at that path
Parsed at build time, embedded inline, works on private sites
Fallback (file not found in bundle or assets)
In static/ at that path
Fetched at runtime, public sites only
Build-time embedding parses the YAML or JSON spec and writes it directly into the HTML as JSON. No HTTP request is made when the reader opens the page, so the embed works on private GitLab Pages and any other auth-gated host.
Usage
text
{{< openapi "api.yaml" >}}
Or with an absolute path from assets/:
text
{{< openapi "/docs/specs/api.yaml" >}}
Live demo
The spec file for this demo lives at exampleSite/assets/docs/specs/sample-api.yaml in the theme repository and is embedded inline at build time.
text
{{< openapi "/docs/specs/sample-api.yaml" >}}
Parameters
Parameter
Description
1st positional
Path to the .yaml or .json spec file. Relative paths resolve as page bundle resources. Absolute paths (leading /) resolve from assets/.
Notes
The Redoc bundle is vendored inside the theme. No internet connection is needed at build time or runtime.
The reference respects the active color theme: primary color, text and sidebar colors all follow the theme palette.
Interactive embeds are replaced with a static note in the printable handbook.
PDF embed
Inline PDF viewer with a fullscreen expand button.
The pdf shortcode embeds a PDF document inline on the page using PDFObject. Readers can scroll through the document without leaving the page. An expand button opens the viewer fullscreen. Press Esc to return to the page.
The PDFObject script loads only on pages that contain a PDF embed.
File location
Where you place the .pdf file determines how it is loaded.
Path style
Where the file lives
How it is loaded
report.pdf (no leading /)
Next to the page’s index.md as a page bundle resource
Read at build time, embedded as a data URI, works on private sites
/path/to/report.pdf (leading /)
In assets/ at that path
Read at build time, embedded as a data URI, works on private sites
Fallback (file not found in bundle or assets)
In static/ at that path
Fetched at runtime, public sites only
Build-time embedding converts the PDF to a base64 data URI and writes it directly into the HTML. No HTTP request is made when the reader opens the page, so the embed works on private GitLab Pages and any other auth-gated host. Keep individual PDF files under a few megabytes to avoid large HTML pages.
Usage
text
{{< pdf "report.pdf" >}}
Or with an absolute path from assets/:
text
{{< pdf "/docs/files/report.pdf" >}}
Live example
The demo file lives at exampleSite/assets/docs/files/sample.pdf in the theme repository and is embedded as a data URI at build time.
text
{{< pdf "/docs/files/sample.pdf" >}}
Scroll through the pages inline, or use the expand button to open the viewer fullscreen and press Esc to return.
Parameters
Parameter
Description
1st positional
Path to the PDF file. Relative paths resolve as page bundle resources. Absolute paths (leading /) resolve from assets/.
Notes
The embed height defaults to 800 px and can be resized by dragging the bottom edge.
If the browser does not support inline PDF embedding, PDFObject shows a link to open the file directly.
Looking for the printable handbook and PDF export? See Print and handbook.
Steps
Numbered procedural steps with a visual connecting rail.
The steps shortcode wraps a Markdown ordered list and applies a visual treatment: numbered circles connected by a vertical rail. Use it for installation guides, tutorials and any sequential process where order matters.
Usage
Write a standard Markdown ordered list inside the shortcode:
text
{{< steps >}}
1. Install Hugo (v0.141 or later).
2. Clone your documentation repository.
3. Run `hugo server` to start the local preview.
{{< /steps >}}
Install Hugo (v0.141 or later).
Clone your documentation repository.
Run hugo server to start the local preview.
Rich content per step
Each list item renders as full Markdown, so steps can contain paragraphs, code blocks, callouts and other shortcodes:
text
{{< steps >}}
1. **Create the config file.**
Copy the starter config to your project root:
```toml
baseURL = "https://docs.example.com/"
theme = "bora"
```
2. **Add your content.**
Place Markdown files under `content/docs/`.
3. **Deploy.**
Push to your hosting provider. Bora generates a fully static site.
{{< /steps >}}
Create the config file.
Copy the starter config to your project root:
toml
baseURL="https://docs.example.com/"theme="bora"
Add your content.
Place Markdown files under content/docs/.
Deploy.
Push to your hosting provider. Bora generates a fully static site.
Parameters
This shortcode takes no parameters. It styles the first ordered list inside its body.
Notes
The shortcode targets the outermost <ol> inside the block. Nested lists are unaffected.
Unordered lists (- or *) do not receive the step treatment.
Tabs
Tabbed content with synced groups and a persisted choice, ideal for OS or language variants.
Tabs show alternative versions of the same content side by side, so readers only see what applies to them. Common uses: OS-specific install commands, language-specific code samples and before/after comparisons.
The iframe is sandboxed (allow-scripts allow-same-origin allow-forms allow-popups). Sites that set X-Frame-Options: DENY will not load inside an iframe regardless of this shortcode.
Search
Client-side full-text search scoped to the current version and language.
Try it
Press Ctrl/⌘ K (or click Search… in the topbar) and type manifest. Results deep-link to the exact section, shown as “Page > Heading”.
How it works
At build time the theme emits one searchindex.json for the whole site, chunked per heading. Each h2/h3 section is its own entry with its anchor. Nothing search-related loads with the page. On the first open of the modal the browser fetches the index and the search library in parallel. Pages stay light, and the index is built once per visit. Everything is client-side: queries never leave the browser, no search server exists, and it works in intranet and air-gapped deployments.
Scoping
Results are scoped to the current version and language by default. The modal’s filters switch scope, and the All button searches everything, including the changelog.
Keyboard
Keys
Action
Ctrl/⌘ K
Open or close
Type
Live results
Enter
Open the first result
↑↓
Move through results
Esc
Close
Press ? anywhere for the full shortcut overview.
The 404 page searches too
A missing URL offers a Search the docs button prefilled with the broken link’s slug. Readers landing on dead links from old bookmarks find the moved page in one keystroke.
Important
Search requires the SearchIndex output format in your hugo.toml.
See Configuration. Without it the
modal reports “Search is unavailable”.
Navigation
Sidebar, on-this-page column, breadcrumbs, prev/next and keyboard shortcuts.
Sidebar
The left sidebar lists the current version and language’s sections as collapsible groups. Clicking a group header expands or collapses it without navigating. Ordering follows weight. Sub-sections nest: a directory inside a section renders as an indented nested group, recursively.
The sidebar state (open/closed) persists. The group containing the current page auto-opens, the active page is highlighted, and on mobile the panel closes after choosing a page.
On this page
Wide screens (1280 px and wider) get a sticky right column built from the page’s h2/h3 headings, with the current section highlighted while scrolling. Clicking an entry jumps to the heading and flashes it so the reader sees where they landed. That is also why this page has several headings.
Breadcrumbs
Every article shows its trail (home > version > language > section > page) with the version label rendered as dots (v0.0.1). The same trail is emitted as structured data for search engines. See SEO and AI readability.
Previous / Next
Article pages link their neighbors in sidebar order, strictly within the current version and language. The pager never jumps across versions or languages.
Keyboard and accessibility
A Skip to content link is the first Tab stop on every page.
Heading anchors are keyboard-reachable. The active sidebar link carries aria-current="page".
Press ? for the shortcuts dialog.
Back to top
A floating button appears after scrolling. One click returns smoothly to the top.
Versioning
Tag-based versioning with an automatic pipeline, version pickers and outdated banners.
The model
You only ever write one docs tree, living under a directory named after your working-docs token (the snippets below use __mydocs__, this site uses __vcurrent__):
Releasing means pushing a git tag (v1.0.0). Nothing is copied in the repository. Git remembers what the token directory looked like at every tag.
At build time, the release pipeline assembles the full site:
text
tag v1.0.0 ──┐
tag v1.1.0 ──┼─→ content/docs/v1_0_0/… ← extracted from each tag,
tag v2.0.0 ──┘ v1_1_0/… the token dir renamed to the tag,
v2_0_0/… token links and title replaced
└─→ static/versions.json ← generated: newest tag = latest
One Hugo build then renders everything. Pickers, search scopes, hreflang and the outdated-version banner work across the assembled versions automatically.
Choosing the token
Releasing is a literal string replacement, so the token must be a string that will never occur naturally in your content. Avoid real words like “current”. A sentinel like __mydocs__ or __vnext__ works well. Two hard rules:
It is configured in exactly one place: the last directory of docsPath in ci/config.json. The theme itself never knows it.
Capitals in the token require disablePathToLower = true in hugo.toml, otherwise Hugo lowercases the URLs while your links keep the capitals.
The pipeline replaces the token in three places:
Link paths in every Markdown file: /docs/__mydocs__/… becomes /docs/v1_2_0/…
The token directory’s own _index.md: every bare __mydocs__ becomes the tag name (v1.2.0). That is why its title should be the literal token, so each released version’s card and breadcrumb show its tag.
site.json: links into the token dir (landing CTAs, menu entries) are pointed at the latest version, since the token dir itself is gone after assembly.
No other file’s prose is ever touched.
The pipeline
The theme ships the script (ci/scripts/build-docs.sh), driven by ci/config.json:
Where your working docs live. The last directory is your token.
fromTag / untilTag
Tag range to build. Empty means from the first tag up to the latest.
skipTags
Yanked or broken releases to exclude.
includeCurrent
Also include the working tree as a preview version, named after the token.
CI usage:
.gitlab-ci.ymlyaml
pages:rules:[{if:$CI_COMMIT_TAG }]variables:{GIT_DEPTH:0}script:- sh ci/scripts/build-docs.sh ci/config.json "$PAGES_PATH"- hugo --environment production --baseURL "${CI_PAGES_URL}/"
With no tags the script leaves the working tree untouched. Running hugo server while writing simply shows the token directory.
Writing rules this enables
Internal links always use the fixed prefix docs/__mydocs__/<lang>/…. The pipeline rewrites it per version.
Version names come from tags (v1.2.0 becomes directory v1_2_0, displayed as “v1.2.0”). You never name version directories yourself.
Fixing an old release: branch from its tag, fix, tag again (v1.0.1). Retire the bad tag via skipTags.
The version manifest
/versions.json always lives beside the site. The theme fetches it host-relative. It drives the pickers, search scoping and the outdated banner, and is generated by the pipeline. The minimal copy committed under static/ only serves local development.
The outdated-version banner
Readers of any non-latest version get a banner linking to the same page in the latest version. To preview it locally: hand-edit static/versions.json, add a fake newer version with "latest": true, restart hugo server.
Failure behavior
If versions.json cannot be fetched, the pickers hide, a toast informs the reader and everything else keeps working. Search falls back to unscoped results.
Page extras
Announcement bar, edit links, feedback widget, last-updated date and the changelog.
Everything on this page is opt-in via site.json. Nothing renders until you configure it. This demo site has all of them enabled, so you can see each one live.
Announcement bar
The bar above the topbar. Markdown text with an optional dismiss button. The dismissal remembers the announcement’s id, so a new id re-shows the bar to everyone:
site.jsonjson
"announcement":{"id":"release-2026-06","text":"**v2 is out**: [release notes](/changelog/)","dismissible":true}
Outdated-version banner
Automatic. No configuration is needed beyond the version manifest. Readers on a non-latest version see a banner linking to the same page in the latest version.
The page’s content path is appended automatically. “Last updated” appears when dates exist. Set enableGitInfo = true in hugo.toml to pull dates from git history.
Feedback widget
“Was this page helpful?” with Yes/No at the end of each article (look below). Votes POST as JSON to your endpoint:
No personal data is collected. The endpoint must respond 2xx and allow CORS from the docs origin. A short Cloudflare Worker or Netlify function forwarding to a Slack webhook or spreadsheet is the typical receiver.
Caution
This demo site posts to httpbin.org (a public echo service) purely so the
buttons work. Replace or remove feedback.url before shipping.
Printable handbook
The book icon in the settings drawer opens a handbook: the entire current version and language assembled into one page. It starts with a cover, then every section and page in sidebar order, each chapter starting on a fresh sheet. Open it and print to PDF (Ctrl/⌘ P) to get a single coherent manual.
It is a Hugo output format, enabled on each language root’s _index.md:
docs/<token>/<lang>/_index.mdyaml
---title:"English"outputs:["HTML","Handbook"]---
That produces one handbook per version and language at docs/<version>/<lang>/handbook.html. It reuses the print stylesheet, always prints with the light theme (ink-friendly and readable) and needs no JavaScript.
Because it is plain HTML, you can also feed it to any HTML-to-PDF tool in your CI pipeline to produce a real, page-numbered PDF. See Print and handbook.
Note
Interactive embeds (Mermaid, draw.io, OpenAPI, PDF) render via JavaScript, so
in the static handbook they show a short “view online” note instead. All text,
code, images, admonitions, tables and tabs are included in full.
Changelog
A multilingual release-notes section at /changelog/, with one page per release showing the date, colored badges and full Markdown. Create:
Badge vocabulary (translated and theme-colored): added, changed, fixed, deprecated, removed, breaking, security, performance, docs. Each language gets an RSS feed automatically. Copy the _index.md files from this site’s exampleSite/content/changelog/. The front matter wiring matters.
Print and handbook
One-click printable handbook plus PDF export via any HTML-to-PDF tool in CI.
The printable handbook is a plain, self-contained HTML page at a predictable URL:
text
docs/<version>/<lang>/handbook.html
No app chrome, no JavaScript. Just the cover, a linked table of contents and every page in order, styled for print. That makes it ideal input for any HTML-to-PDF tool run in your CI. The theme ships no PDF toolchain of its own, so you keep full control over the renderer and its output.
Why generate the PDF yourself
The handbook’s in-browser print uses the browser’s print dialog, which cannot compute page numbers. That is why its table of contents has no page numbers. A dedicated HTML-to-PDF engine can: it lays the document out into real pages and back-fills numbers and leader dots. If you want a true page-numbered PDF, render the handbook with one of the tools below.
With WeasyPrint (no browser)
WeasyPrint renders HTML and CSS to PDF with its own engine. No headless browser is required, so it is light in CI (Python plus a few system libraries).
bash
# after hugo has built the site into ./publicweasyprint public/docs/v1_0_0/en/handbook.html handbook-v1.0.0-en.pdf
With Paged.js CLI (headless Chrome)
pagedjs-cli drives headless Chromium, so the PDF matches Chrome exactly. Heavier (Node plus Chromium) but pixel-faithful.
Prince, wkhtmltopdf or your own headless-Chrome script work the same way: point them at handbook.html.
Page numbers and a leader-dot TOC
To get the classic Chapter …………… 12 table of contents, add a small print stylesheet that your engine understands (WeasyPrint and Paged.js both support CSS Generated Content for Paged Media):
pdf.csscss
/* number the pages */@page{@bottom-center{content:counter(page);}}/* fill the handbook TOC leaders with the target page number */.handbook-toc__link::after{content:leader('.')target-counter(attr(href),page);}
Pass it to the tool (e.g. weasyprint -s pdf.css …). Because the handbook’s TOC links already point at each page’s anchor, target-counter resolves the real page numbers for you.
In a CI job
A minimal GitLab CI example that produces a downloadable PDF alongside the site:
.gitlab-ci.ymlyaml
pdf:stage:deployimage:python:3.12-slimrules:[{if:$CI_COMMIT_TAG }]script:- pip install weasyprint- sh ci/scripts/build-docs.sh ci/config.json "$PAGES_PATH"- hugo --source exampleSite --environment production --baseURL "${CI_PAGES_URL}/"- weasyprint exampleSite/public/docs/<version>/en/handbook.html public/handbook.pdfartifacts:{paths:[public] }
The result is a real, page-numbered PDF, generated by the engine you chose, with no PDF dependency baked into the theme.
Tip
Looking for the shortcode that embeds a PDF viewer on a page? See PDF embed.
SEO and AI readability
Open Graph, hreflang, structured data, llms.txt and per-page markdown outputs.
Per-page head, automatically
Every page emits without configuration:
canonical URL (self-referencing, safe with separately-deployed versions)
Open Graph and Twitter card tags: title, description, type and locale
hreflang alternates linking the same page across the version’s languages (only where the page exists), with x-default on the default language
BreadcrumbList JSON-LD matching the visible breadcrumbs
noindex automatically on non-production builds
Social card image
site.jsonjson
"social":{"cover":"/docs/media/banner.png"}
Per-page override via cover: front matter. With an image set, shares render as large cards. Without one, they appear as text cards. Must be a direct image URL, sized approximately 1200x630.
Browser tint
site.jsonjson
"themeColor":{"light":"#fff8f6","dark":"#1a110f"}
llms.txt and Markdown outputs
For AI assistants and “read as Markdown” use cases, the build emits:
/llms.txt: a curated index (llmstxt.org) of the latest version’s default language, linking to the per-page Markdown files
per-page index.md: the raw Markdown of every article, next to its HTML (this page: index.md)
Both come from the LLMS and Markdown output formats in hugo.toml. Remove those lines to disable them.
Build-time link checking
Internal links and anchors are validated while building: