---
description: Default styles, icons, and page layout
output-file: theme.html
title: theme

---



<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

`octavo.theme` supplies the HTML layout, styles, icons, and browser interactions used by `octavo.site`. Normal site builds use it automatically. To customize a site's appearance, add stylesheets with `css` in `octavo.yml`; you do not need to edit the bundled theme.

When rendering an article directly, use [`code_wrap`](theme.md#code_wrap) to add code-copy buttons and [`page_body`](theme.md#page_body) to place the rendered content in the page layout:

```python
from octavo.core import md2doc
from octavo.theme import code_wrap, page_body

article = md2doc('# Welcome\n\nMy project documentation.').to_html(code_wrap=code_wrap)
body = page_body(header='', navigation='', content=str(article), toc=article.toc)
```

page_body returns the HTML to put inside a page’s `<body>`. It does not create a complete HTML document or load stylesheets and scripts. [`Site.build`](site.md#site.build) handles that assembly; you normally don’t need to call these helpers yourself.

Octavo's own CSS and JavaScript are bundled locally, but the default typography stylesheet is loaded from a CDN.

## Stylesheets and scripts

The site build writes Octavo's shared styles and browser interactions once, rather than embedding them in every page. `ASSETS` maps the output paths `_octavo/octavo.css` and `_octavo/octavo.js` to their text. The original files live beside the Python module.

Use these output paths when linking pages to the bundled assets. [`head_tags`](theme.md#head_tags), shown below, makes the links relative to each page.

## Icons

`ICONS` maps bundled icon names to SVG markup. For example, `ICONS['github']` returns SVG you can insert into HTML. The icons use `currentColor`, so they inherit the surrounding text colour.

To use these icons as header links, configure `tools` in `octavo.yml` as described in [octavo.site](site.md).

The brand marks (`github`, `x`, `discord`, `markdown`) come from [Simple Icons](https://simpleicons.org), released under CC0. The `rss` and `search` icons come from [Lucide](https://lucide.dev). Its ISC licence is reproduced in Octavo's `LICENSE`.

Available icon names:

```python
test_eq(sorted(ICONS), ['discord', 'github', 'markdown', 'rss', 'search', 'x'])
assert all(v.startswith('<svg') and 'currentColor' in v for v in ICONS.values())
sorted(ICONS)
```

```output
['discord', 'github', 'markdown', 'rss', 'search', 'x']
```

## Code-copy buttons

Normal site builds add Copy buttons to code blocks automatically. When rendering markdown directly, pass `code_wrap=code_wrap` to [`Doc.to_html`](core.md#doc.to_html) to use the same buttons. The generated page must load `octavo.js` for copying to work.

[`code_wrap`](theme.md#code_wrap) receives the highlighted HTML and wraps it with a button. Mermaid blocks take a different path: they use mdhtml's diagram renderer, without a Copy button.

---

### code_wrap

```python
def code_wrap(
    html:str, # The highlighted block
    lang:str, # Its language, or None for a bare fence
    text:str, # Its source, unescaped
):
```

*Add a copy button to code blocks, preserving Mermaid rendering*

Wrapping an ordinary code block adds the button markup; wrapping a Mermaid block leaves diagram rendering to mdhtml.

```python
wrapped = code_wrap('<pre><code>x = 1</code></pre>', 'python', 'x = 1')
assert 'vm-copy' in wrapped
assert 'vm-copy' not in code_wrap('<pre><code>graph TD</code></pre>', 'mermaid', 'graph TD')
wrapped
```

```output
'<div class="vm-code"><pre><code>x = 1</code></pre><button class="vm-copy" type="button">Copy</button></div>'
```

## Tabsets

Use a `panel-tabset` fenced div to present alternatives as tabs. For example, show two ways to set a site's title:

````markdown
::: {.panel-tabset}
### Python

```python
site = Site.from_dir('docs', title='My project')
```

### octavo.yml

```yaml
title: My project
```
:::
````

Rendered with the theme, this becomes:

### Python

```python
site = Site.from_dir('docs', title='My project')
```

### octavo.yml

```yaml
title: My project
```

Each heading at the first heading's level starts a tab. Its content continues until the next heading at that level; lower-level headings remain inside the panel. Tabsets can also be nested.

The first tab opens initially. The theme's JavaScript switches panels when a tab is selected. Without JavaScript, all panels remain readable.

## Page layout

[`page_body`](theme.md#page_body) assembles already-rendered HTML for the header, navigation, article, table of contents, and footer. It does not render markdown or generate navigation links; the caller supplies those parts.

Standard pages place site navigation and a table of contents beside the article. Empty navigation and an empty table of contents are omitted. Both sidebars are collapsible, starting open in the HTML and collapsing on narrow screens through the theme's JavaScript.

Pass `custom=True` for a full-width article without either sidebar or the default `prose` typography class. In a site build, page frontmatter `page-layout: custom` selects this option. The supplied header, footer, and search dialog remain outside the article grid.

---

### page_body

```python
def page_body(
    header:str, # Site brand, tools, and search trigger
    navigation:str, # Site navigation as a nav element
    content:str, # Article HTML, including page actions
    toc:str='', # Generated table of contents
    footer:str='', # Site footer HTML
    custom:bool=False, # Use the full-width custom layout
):
```

*Assemble the page body with collapsible navigation and table of contents*

## Page styles and scripts

[`head_tags`](theme.md#head_tags) returns the stylesheet, script, and optional favicon tags to put in a page's `<head>`. Pass the output page path as `dest`, and a function such as [`octavo.site.relhref`](site.md#relhref) to calculate relative links. Custom stylesheet, script and favicon paths are relative to the site root, not the page's directory.

Stylesheets load in this order: Typrose typography, Octavo, Pagefind search, then custom CSS. Later rules can override earlier ones of equal specificity, subject to the usual CSS cascade.

Custom scripts load as `<script type="module">`, after Octavo's own `octavo.js`. Like `defer` scripts, modules run in page order once the page has been read. A module can use `import` and top-level `await`, runs in strict mode, and keeps its top-level names to itself: to share a value with other scripts, assign it to a property of `window`.

Typrose is loaded from a CDN; its stylesheet is not copied into the site. When calling [`head_tags`](theme.md#head_tags) directly, pass `typrose=False` to omit that link, and supply local typography styles if needed. This is a [`head_tags`](theme.md#head_tags) argument, not an `octavo.yml` setting.

The tags reference files; they do not write them. [`Site.build`](site.md#site.build) writes the Octavo assets and generates the Pagefind search files.

---

### head_tags

```python
def head_tags(
    dest:str, # Output page path relative to the site root
    rel, # `relhref`, or anything that makes one address relative to another
    css:NoneType=None, # The site's own stylesheets, after octavo's
    js:NoneType=None, # The site's own module scripts, after octavo's
    favicon:NoneType=None, # The site's favicon icon path (relative to site root)
    typrose:bool=True, # Link the typography stylesheet?
):
```

*Return stylesheet, script, and favicon tags for a page*

For `api/config.html`, a stylesheet supplied as `styles.css` becomes `../styles.css`. Scripts, the favicon and bundled assets follow the same rule, so nested pages load the same files as the homepage.

```python
tags = head_tags('api/config.html', relhref, css='styles.css', js='site.js', favicon='favicon.png')
order = [i for i,t in enumerate(tags) if 'octavo.css' in t or 'pagefind-component-ui.css' in t or 'styles.css' in t]
test_eq(order, sorted(order))
assert '../_octavo/octavo.css' in tags[1]
assert '../pagefind/pagefind-component-ui.css' in tags[2]
assert any('new URL("../pagefind/pagefind-component-ui.js", document.baseURI)' in t for t in tags)
assert '<link rel="icon" href="../favicon.png">' in tags
assert tags.index('<script type="module" src="../site.js"></script>') > next(i for i,t in enumerate(tags) if 'octavo.js' in t)
L(tags)
```

```output
['<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/typrose@0.2.2/typrose.css">', '<link rel="stylesheet" href="../_octavo/octavo.css">', '<link rel="stylesheet" href="../pagefind/pagefind-component-ui.css">', '<link rel="stylesheet" href="../styles.css">', '<link rel="icon" href="../favicon.png">', '<script type="module">\nconst scriptUrl = new URL("../pagefind/pagefind-component-ui.js", document.baseURI);\nconst bundleUrl = new URL(".", scriptUrl).href;\nconst config = document.createElement("pagefind-config");\nconfig.setAttribute("bundle-path", bundleUrl);\ndocument.body.prepend(config);\nawait import(scriptUrl.href);\n</script>', '<script defer src="../_octavo/octavo.js"></script>', '<script type="module" src="../site.js"></script>']
```
