Custom Blocks
Braised supports Pandoc-style fenced divs for structured content components. Blocks are rendered through Go templates and can use Alpine.js for interactivity.
Syntax
A block opens with ::: followed by {.block-name} and closes with :::. The block name is the first CSS class and determines which template is used.
:::{.block-name}
Content goes here.
:::
Additional attributes are passed as props:
:::{.callout type="warning" title="Watch out"}
This will cause problems.
:::
Structural blocks (those that contain child blocks) use a named close instead of a bare ::::
:::{.tabs}
:::{.tab label="First"}
Content of first tab.
:::{/tab}
:::{.tab label="Second"}
Content of second tab.
:::{/tab}
:::{/tabs}
Structural built-in blocks: tabs, tab, code-group, pillstrip, when, dl, item, table, row, cell, spotlight. All require their named close (:::{/name}). Leaf blocks (callout, details, synopsis, option, rawhtml, pull-quote) use a bare ::: close.
Built-in blocks
callout
Highlights important information. The type prop controls the semantic category.
Props
| Prop | Values | Default |
|---|---|---|
type |
note, tip, warning, caution |
note |
title |
Any string | (none) |
Example
:::{.callout}
A plain note with no title.
:::
A plain note with no title.
:::{.callout type="tip" title="Pro tip"}
Use `nav_title` in frontmatter to shorten long page titles in the sidebar.
:::
Use nav_title in frontmatter to shorten long page titles in the sidebar.
:::{.callout type="warning" title="Draft pages are skipped"}
Pages with `draft: true` in their frontmatter are not written to `dist/`.
They do not appear in the build output at all.
:::
Pages with draft: true in their frontmatter are not written to dist/.
They do not appear in the build output at all.
:::{.callout type="caution" title="Destructive operation"}
Running `braised build` will overwrite everything in the `dist/` directory.
:::
Running braised build will overwrite everything in the dist/ directory.
details
A collapsible section backed by the native <details> element. No JavaScript required.
Props
| Prop | Values | Default |
|---|---|---|
summary |
Any string | Details |
Example
:::{.details summary="Show the full error message"}
rendering "docs/guide.md": executing template: template: page.html:14:3: executing "content" at <.Page.Content>: ...
:::
Show the full error message
rendering "docs/guide.md": executing template: template: page.html:14:3:
executing "content" at <.Page.Content>: ...
pull-quote
Typographic emphasis for a text excerpt. Renders a centered, italic block with optional attribution. Excluded from the RAG content manifest by default โ the content is typically repeated from surrounding body prose. Set rag="include" to index it (useful for epigraphs or standalone quotations that are not pulled from body text).
Props
| Prop | Values | Default |
|---|---|---|
attribution |
Any string | (none) |
rag |
"include" |
(excluded from manifest) |
Example
:::{.pull-quote}
Good documentation doesn't explain what the code does. It explains why.
:::
Good documentation doesn't explain what the code does. It explains why.
:::{.pull-quote attribution="Jane Smith, *Designing for Readers*"}
A reader who has to re-read a sentence has already lost the thread.
:::
A reader who has to re-read a sentence has already lost the thread.
spotlight
A breakout container that visually elevates any content โ prose, images, callouts, or a nested pull-quote โ beyond the normal column padding. The level prop controls how far the block expands and how much visual decoration is applied.
spotlight is a structural block: it may contain other blocks as direct children and must be closed with :::{/spotlight}.
Props
| Prop | Values | Default |
|---|---|---|
level |
"1", "2", "3" |
"2" |
Example
:::{.spotlight}

:::{/spotlight}
:::{.spotlight level="3"}
This paragraph receives the strongest visual lift โ widest breakout, deepest shadow.
:::{/spotlight}
Composing with pull-quote
spotlight (structural) wraps pull-quote (leaf) cleanly. The inner ::: closes pull-quote; :::{/spotlight} closes the outer block:
:::{.spotlight level="2"}
:::{.pull-quote attribution="Internal design review"}
Every API should be obvious to someone who has never used it.
:::
:::{/spotlight}
Every API should be obvious to someone who has never used it.
The pull-quote's RAG exclusion applies regardless of what wraps it. To index the quoted text, set rag="include" on the pull-quote, not on spotlight.
tabs
A tabbed panel group. Each direct child should be a tab block. Alpine.js handles the active state.
Child block: tab
| Prop | Values | Default |
|---|---|---|
label |
Any string | Tab N (1-indexed, e.g. Tab 1, Tab 2) |
Example
:::{.tabs}
:::{.tab label="Go"}
```go
package main
import "fmt"
func main() {
fmt.Println("hello")
}
```
:::{/tab}
:::{.tab label="Python"}
```python
def main():
print("hello")
if __name__ == "__main__":
main()
```
:::{/tab}
:::{/tabs}
package main
import "fmt"
func main() {
fmt.Println("hello")
}
def main():
print("hello")
if __name__ == "__main__":
main()
dl
A definition list rendered as <dl>/<dt>/<dd>. Use item child blocks for each entry. Particularly useful for configuration references, glossaries, and command flag documentation. Both dl and item are structural โ close them with :::{/dl} and :::{/item}. Item bodies may contain nested blocks such as callouts.
Child block: item
| Prop | Values | Default |
|---|---|---|
term |
Any string | (empty) |
Example
:::{.dl}
:::{.item term="--port"}
TCP port to listen on. Defaults to `4321`.
:::{/item}
:::{.item term="--host"}
Address to bind to. Defaults to `localhost`.
:::{/item}
:::{/dl}
- --port
TCP port to listen on. Defaults to
4321.- --host
Address to bind to. Defaults to
localhost.
synopsis
A styled block for command syntax, BNF grammar, or function signatures. Write the content as a fenced code block inside the block. Italic text (*param*) renders as a replaceable parameter.
Props
None.
Example
:::{.synopsis}
```
braised build [--drafts] [site-dir]
braised serve [--port PORT] [site-dir]
```
:::
braised build [--drafts] [site-dir]
braised serve [--port PORT] [site-dir]
code-group
A variant of tabs intended specifically for code blocks. Uses the same Alpine.js component (braisedTabs) but with styling hooks that assume each panel contains exactly one fenced code block.
Props
Same as tabs. Child labels default to Tab 1, Tab 2, ... if not set.
Example
:::{.code-group}
:::{.tab label="braised.yaml"}
```yaml
site:
title: My Docs
url: https://docs.example.com
output: ./dist
```
:::{/tab}
:::{.tab label="nav.yaml"}
```yaml
- Introduction: index.md
- Guide:
- Getting Started: guide/start.md
```
:::{/tab}
:::{/code-group}
site:
title: My Docs
url: https://docs.example.com
output: ./dist
- Introduction: index.md
- Guide:
- Getting Started: guide/start.md
table
A semantic HTML table with support for header rows, multiline cell content, and merged cells via colspan/rowspan. Use row and cell child blocks to define structure.
Child block: row
| Prop | Values | Default |
|---|---|---|
header |
"true" |
(none โ body row) |
Child block: cell
| Prop | Values | Default |
|---|---|---|
colspan |
Positive integer | (none) |
rowspan |
Positive integer | (none) |
table props
| Prop | Values | Default |
|---|---|---|
caption |
Any string | (none) |
Indentation: Row and cell fence lines may be indented with spaces or tabs for readability โ braised strips leading whitespace before classifying fence lines. Keep cell content at the natural document depth; 4+ spaces on content lines (not fence lines) would make them CommonMark code blocks.
Cell content: Use separate open, content, and close lines for each cell. Inline syntax :::{.cell}content:::{/cell} is not supported. All three levels (table, row, cell) are structural and require named closes (:::{/table}, :::{/row}, :::{/cell}). Cell bodies may contain nested blocks such as callouts.
Example
:::{.table caption="PostgreSQL numeric types"}
:::{.row header="true"}
:::{.cell}
Name
:::{/cell}
:::{.cell}
Size
:::{/cell}
:::{.cell}
Range
:::{/cell}
:::{/row}
:::{.row}
:::{.cell}
smallint
:::{/cell}
:::{.cell}
2 bytes
:::{/cell}
:::{.cell}
โ32768 to 32767
:::{/cell}
:::{/row}
:::{.row}
:::{.cell}
integer
:::{/cell}
:::{.cell}
4 bytes
:::{/cell}
:::{.cell}
โ2147483648 to 2147483647
:::{/cell}
:::{/row}
:::{/table}
Multiline cell content works naturally โ add as many lines as needed between the open and close fences:
:::{.cell}
First paragraph of content.
Second paragraph of content.
:::{/cell}
Merged cells use colspan and rowspan on the cell block:
:::{.cell colspan="2"}
Spans two columns
:::{/cell}
rawhtml
Passes content through to the page as raw HTML, bypassing Braised's Markdown processing and goldmark's HTML sanitiser. Use this for third-party embeds: form widgets, script-injected components, iframes, or any HTML that cannot be expressed as Markdown or a block.
Props
None.
Example
:::{.rawhtml}
<div id="typeform-embed">
<!-- third-party widget bootstrapped by script below -->
</div>
<script src="https://embed.typeform.com/next/embed.js"></script>
:::
Content inside rawhtml is emitted verbatim. Block-level HTML elements (<div>, <script>, <iframe>, etc.) pass through unchanged. Braised blocks are not supported inside rawhtml.
For inline <script> tags in <head> (e.g. analytics), use the layout extra_head override instead.
pillstrip + when
A page-level content filter. The pillstrip block renders a row of pill buttons; the companion when block wraps content that should show or hide based on the active pill. Selecting a pill updates a global page-filter state โ all when blocks across the page respond simultaneously.
pillstrip child block: option
| Prop | Values | Default |
|---|---|---|
label |
Display text for the pill | (required) |
key |
Filter key matched by when blocks |
value of label |
when props
| Prop | Values | Default |
|---|---|---|
key |
Filter key that activates this block | (required) |
Example
:::{.pillstrip}
:::{.option label="Linux" key="linux"}
:::
:::{.option label="macOS" key="macos"}
:::
:::{.option label="Windows" key="windows"}
:::
:::{/pillstrip}
:::{.when key="linux"}
Install with `apt install braised`.
:::{/when}
:::{.when key="macos"}
Install with `brew install braised`.
:::{/when}
:::{.when key="windows"}
Download the binary from the releases page.
:::{/when}
The first option is active by default. When key is omitted from an option block, the label value is used as the key โ useful when label and key are the same.
Content not wrapped in a when block is always visible regardless of the active pill.
Indentation
Braised strips leading whitespace from fence lines before classification, so any indentation depth works. Two spaces per level is conventional.
:::{.table}
:::{.row}
:::{.cell}
Content here.
:::{/cell}
:::{/row}
:::{/table}
Rules:
- Any number of leading spaces or tabs on a fence line is stripped before classification. Tabs and spaces both work at any depth โ use whichever makes the source most readable.
- 4+ spaces on a content line (not a fence line) still triggers CommonMark's indented code block rule (ยง4.4) inside a block. Only fence lines (
:::{...}and:::{/...}) are immune to this โ they are stripped by braised before goldmark processes them. Keep cell and item content at the natural indentation of the document; only indent the fences. - 3-space indentation on fence lines partially works (the fence opens) but produces unreliable close detection. In practice, use 0, 2, or 4+ spaces โ not 3.
For nested blocks like table > row > cell, a common style is to indent child fences one level (2 spaces or one tab) from their parent โ this is purely visual organisation and has no effect on parsing:
Blank line before outer close fences (recommended style): When an inner structural block closes immediately before an outer close fence, add a blank line between them:
:::{.row}
:::{.cell}
Content.
:::{/cell}
:::{/row}
Without the blank line, the Markdown parser can absorb the outer fence (:::{/row}) as literal text in the preceding paragraph. Braised silently strips these spurious fence lines as a safety net, but the blank line is the recommended style โ it is unambiguous and produces identical output at any nesting depth.
Custom blocks
Create a blocks/ directory in your site root and add an HTML file named after the block.
my-site/
โโโ braised.yaml
โโโ blocks/
โ โโโ badge.html โ renders :::{.badge}
โโโ docs/
โโโ index.md
The template receives a BlockContext value:
| Field | Type | Description |
|---|---|---|
.Children |
template.HTML |
Rendered HTML of all child content |
.ChildBlocks |
[]ChildContext |
Direct child blocks with their rendered output |
.Props |
map[string]string |
Attributes from the opening fence |
.Page |
PageContext |
Title, URL, headings for the current page |
.Site |
SiteContext |
Title and URL for the site |
Each entry in .ChildBlocks has:
| Field | Type | Description |
|---|---|---|
.Name |
string |
Block name (first CSS class) of the child |
.Props |
map[string]string |
Child's opening fence attributes |
.Rendered |
template.HTML |
Output of the child's template |
Use the default function to provide fallback values for optional props:
{{$level := index .Props "level" | default "info"}}
<span class="badge badge-{{$level}}">{{.Children}}</span>
Parent/child rendering: Child templates are data providers โ their .Rendered output is what the child's template emits. Parent templates own all structural markup. If your block type is always a structural child of a specific parent (like tab inside tabs), keep its template minimal:
{{.Children}}
Let the parent template read .ChildBlocks[i].Props directly and build the wrapper elements (tab buttons, <tr> rows, etc.) itself.
Local templates in blocks/ take precedence over built-in templates of the same name, so you can override any built-in block.
Cross-reference registration
Any block โ built-in or custom โ can accept an id prop. When present, it registers that block as a named cross-reference target, reachable from any page via braised:ref/{id}:
:::{.callout type="caution" id="disk-wipe-warning" title="Data loss risk"}
Running this command will erase the disk.
:::
From another page:
See the [disk-wipe warning](braised:ref/disk-wipe-warning) before proceeding.
The id value must be unique across the entire site. Custom block templates can also register refs programmatically using {{call .RegisterRef id title anchorID}} โ see Block Templates for details.
Leaf vs structural blocks
Leaf blocks use a bare ::: to close. All custom blocks default to leaf.
Structural blocks pass ::: through to inner child blocks and use a named close :::{/name}. The built-in structural blocks are tabs, tab, code-group, pillstrip, when, dl, item, table, row, cell, and spotlight.
To make a custom block structural, declare it in braised.yaml:
blocks:
structural: [my-tabs, my-container]
Once declared structural, the block must be closed with :::{/name} and may contain nested leaf blocks (callouts, synopsis, etc.) without ambiguity:
:::{.my-container}
Some intro text.
:::{.callout type="note"}
A note nested inside your custom block.
:::
:::{/my-container}
Without the structural declaration, the outer block would grab the inner ::: and leave the callout unclosed.
Rich content inside blocks
All standard Markdown features work inside blocks, including GFM extensions:
:::{.callout type="note" title="Supported inside blocks"}
| Feature | Works? |
|---|---|
| Tables | โ |
| ~~Strikethrough~~ | โ |
| `code spans` | โ |
| Fenced code blocks | โ |
| Task lists | โ |
| Footnotes | โ |
:::
| Feature | Works? |
|---|---|
| Tables | โ |
| โ | |
code spans |
โ |
| Fenced code blocks | โ |
| Task lists | โ |
| Footnotes | โ |