theme
Default styles, icons, and page layout
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 to add code-copy buttons and page_body to place the rendered content in the page layout:
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 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, 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.
The brand marks (github, x, discord, markdown) come from Simple Icons, released under CC0. The rss and search icons come from Lucide. Its ISC licence is reproduced in Octavo's LICENSE.
Available icon names:
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)
['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 to use the same buttons. The generated page must load octavo.js for copying to work.
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
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.
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
'<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:
::: {.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
site = Site.from_dir('docs', title='My project')
octavo.yml
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 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
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 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 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 directly, pass typrose=False to omit that link, and supply local typography styles if needed. This is a head_tags argument, not an octavo.yml setting.
The tags reference files; they do not write them. Site.build writes the Octavo assets and generates the Pagefind search files.
head_tags
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.
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)
['<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>']