Style Guide

Outline

Internal reference for this site’s custom shortcodes and partials, plus short how-tos for the built-in features that are easiest to get wrong (equations, video embeds, code blocks) — not linked from the main navigation, listed only in the footer. Documents parameters and defaults, with live rendered examples so behaviour is visible, not just described. English only.

img

Figure with image, caption, and credit. layouts/shortcodes/img.html → layouts/partials/img.html.

Parameters

ParamDefaultDescription
src—Image URL (absolute or site-relative). Required unless key is set.
key—Lookup into data/images.yaml — supplies src/alt/caption/credit/width as fallbacks. Required unless src is set.
caption""Caption text.
credit"" → italic “Undefined”Credit text. Any existing (c)/© is stripped; output is always prefixed with a single ©.
alt""Alt text.
width70%Image width, as a % of the text column. Bare numbers are treated as percent (width=80 == "80%").
alignleftHorizontal position of the image+caption block: left | center | right.
width_captionmirrors widthCaption box width, same % rules as width. Pass width_caption=100 for the full text column regardless of image width.
align_captionleftCaption text-align: left (ragged-right) | center | right.

Default

{{< img src="https://photos.possantti.net/covers/canela-1.jpg" caption="Caracol Falls — Canela's iconic landmark." >}}
Caracol Falls — Canela's iconic landmark. — © Undefined

width=40 — caption mirrors image width by default

{{< img src="..." width=40 caption="Narrower image." >}}
Narrower image — the caption box shrinks to match the 40% image width, which is the default. — © Undefined

width_caption=100 — caption ignores image width

{{< img src="..." width=40 width_caption=100 caption="..." >}}
Same 40% image, but width_caption=100 forces the caption to span the full text column instead of mirroring the image. — © Undefined

align="center" and align="right" — block position

{{< img src="..." width=40 align="center" >}}
{{< img src="..." width=40 align="right" >}}
align="center" — © Undefined
align="right" — © Undefined

align_caption="center" — caption text only, block stays put

{{< img src="..." width=40 align_caption="center" >}}
align_caption="center" only changes how the caption text wraps inside its box — the block itself is still left-positioned by default. — © Undefined

Credit normalization

An empty/missing credit falls back to italic “Undefined”; an existing (c) or © in the string is stripped before the single © prefix is added.

{{< img src="..." caption="No credit supplied" credit="" >}}
{{< img src="..." caption="Credit with a redundant (c)" credit="(c) Jane Doe" >}}
No credit supplied — © Undefined
Credit with a redundant (c) — © Jane Doe

key — lookup from data/images.yaml

{{< img key="basin_map" >}}
Watershed map
Hydrological basin used in the study — © Source: ANA

before_after

Drag-to-reveal comparison slider (e.g. draft vs. final, before vs. after treatment). Pure CSS clip-path reveal + a small JS drag/keyboard handler — no external library. layouts/shortcodes/before_after.html → layouts/partials/before-after.html.

Parameters

ParamDefaultDescription
before—Image for the “before” side — a data/images.yaml key, or a direct URL/path. Required.
after—Image for the “after” side — same resolution rules. Required.
before_labelBeforeTag text on the before side.
after_labelAfterTag text on the after side.
rulerafterWhich image’s aspect ratio sizes the box: before | after. The other image is cropped (object-fit: cover) to fill it.
directionhorizontalDrag axis: horizontal | vertical.
position50Initial reveal position, 0–100.
caption""Caption text. Same behavior as img.
credit"" → italic “Undefined”Credit text. Same normalization as img: any existing (c)/© is stripped; output is always prefixed with a single ©.
width100%Slider width. Same as img: bare numbers are treated as percent (width=80 == "80%"); any other CSS unit (e.g. "500px") passes through unchanged.
alignleftSame as img: horizontal position of the slider+caption block — left | center | right.
width_captionmirrors widthSame as img: caption box width. Pass width_caption=100 for the full text column regardless of slider width.
align_captionleftSame as img: caption text-align — left (ragged-right) | center | right.

Default

{{< before_after before="..." after="..." caption="Drag the handle to compare." >}}
Before After
Default: drag the handle to compare. — © Undefined

ruler="before" — the other image's aspect ratio sizes the box

{{< before_after before="..." after="..." ruler="before" >}}
Before After
ruler="before" — box sized by the before image instead of the default after image. — © Undefined

direction="vertical"

{{< before_after before="..." after="..." direction="vertical" >}}
Before After
direction="vertical" — drag up/down instead of left/right. — © Undefined

position and custom labels

{{< before_after before="..." after="..." position="25" before_label="Draft" after_label="Final" >}}
Draft Final
position="25" starts the reveal near the left edge; labels renamed via before_label/after_label. — © Undefined

width=40 and align="center" — same sizing/position rules as img

{{< before_after before="..." after="..." width=40 align="center" >}}
Before After
width=40 align="center" — narrower slider, centered in the column. — © Undefined

width_caption=100 — caption ignores slider width

{{< before_after before="..." after="..." width=40 width_caption=100 >}}
Before After
Same 40% slider, but width_caption=100 forces the caption to span the full text column instead of mirroring the slider. — © Undefined

Credit normalization — identical to img

An empty/missing credit falls back to italic “Undefined”; an existing (c) or © in the string is stripped before the single © prefix is added.

{{< before_after before="..." after="..." credit="(c) Jane Doe" >}}
Before After
Credit with a redundant (c) — © Jane Doe

Equations

Math is rendered by KaTeX 0.16.9, loaded from a CDN by layouts/partials/helpers/katex.html and switched on for every page by math = true under [params] in hugo.toml. Nothing needs enabling per page. Rendering happens in the browser on load, so the page source keeps the LaTeX verbatim.

Delimiters

SyntaxModeRenders as
$ ... $inlineflows inside the sentence
$$ ... $$displayits own centred block

These two are the only delimiters wired up. \( ... \) and \[ ... \] are not configured and would print literally. The KaTeX delimiter list and the Goldmark passthrough delimiters in hugo.toml have to agree, so adding a pair means editing both.

Inline

The index $HAND_i = z_i - z_d$ gives the height of cell $i$ above its
nearest drainage $d$.

The index $HAND_i = z_i - z_d$ gives the height of cell $i$ above its nearest drainage $d$.

Display

$$
Q = \frac{1}{n} A R_h^{2/3} S^{1/2}
$$
$$ Q = \frac{1}{n} A R_h^{2/3} S^{1/2} $$

Multi-line, aligned

The \\ row separator works, but only because [markup.goldmark.extensions.passthrough] is enabled in hugo.toml. Without it, Goldmark parses the markdown inside the delimiters before KaTeX ever sees it and turns \\ into a <br>, which makes KaTeX throw a parse error instead of rendering.

$$
\begin{aligned}
P &= E + Q + \Delta S \\
Q &= Q_s + Q_b
\end{aligned}
$$
$$ \begin{aligned} P &= E + Q + \Delta S \\ Q &= Q_s + Q_b \end{aligned} $$

Gotchas

  • Two bare $ in one paragraph become math. KaTeX scans text nodes for delimiter pairs, so prose like “costs $5 and $10” would be read as one math span if those amounts were not in backticks, swallowing the words between them. A backslash does not help: Goldmark strips it and leaves a bare $ behind. Backticks do work — KaTeX skips code and pre elements, which is also why the source blocks on this page render as text rather than as equations.
  • Subscripts never needed escaping. $x_a + y_b$ is safe even without passthrough, because CommonMark does not apply _ emphasis inside a word.
  • * inside math does need passthrough. Unspaced $a*b*c$ would otherwise come out as $a<em>b</em>c$.

YouTube videos

Hugo’s own built-in shortcode — there is no project override. It is documented here because the parameter names are easy to forget. The video id is the v= value from the watch URL.

It gives a bare responsive iframe and nothing else. For a video that is a figure in a post — with a caption, credit and a width — use the ytvideo shortcode below instead.

Parameters

ParamDefaultDescription
id—Video id. Required. May be passed positionally as the first argument.
titleYouTube videotitle attribute on the iframe. Worth setting for accessibility.
start0Start offset in seconds.
end0Stop offset in seconds; 0 plays to the end.
autoplay01 autoplays. Browsers block this unless mute=1.
mute01 starts muted.
loop01 loops.
controls10 hides the player controls.
loadingeagerPass lazy for videos far down a long page.
class—Class for the wrapper div. See the gotcha below.

Default — positional id

{{< youtube dC8I9PD8V5A >}}

Named parameters

{{< youtube id="dC8I9PD8V5A" start="30" title="Beven on reading papers" loading="lazy" >}}

Gotcha: class drops the responsive wrapper

By default the shortcode wraps the iframe in a div carrying an inline padding-bottom: 56.25% style, which is what keeps the video 16:9 and full-width on mobile. Passing class replaces that inline style rather than adding to it, so the video collapses to zero height unless the class supplies its own aspect ratio. Leave class alone unless you are also writing the CSS for it.

ytvideo

YouTube embed wrapped in the same figure block as img — caption, credit, width, alignment. Use it whenever a video is a figure in the flow of a post; the built-in youtube shortcode above gives a bare iframe with none of that. layouts/shortcodes/ytvideo.html → layouts/partials/ytvideo.html.

Parameters

ParamDefaultDescription
id—Video id, or any URL it can be read from: watch?v=ID, youtu.be/ID, /embed/ID. Required.
titlethe caption, else YouTube videotitle on the iframe, which is what screen readers announce.
start—Start offset in seconds.
ratio16/9Aspect ratio as W/H. 4/3 for older footage, 9/16 for a vertical clip.
loadinglazylazy | eager. Defaults to lazy, unlike the built-in shortcode, since these sit in long posts.
caption""Caption text. Same behaviour as img.
credit"" → italic “Undefined”Same normalization as img: any existing (c)/© is stripped; output is always prefixed with a single ©.
width100%Player width. Bare numbers are percent (width=80 == "80%"); other CSS units pass through. Note img defaults to 70% — pass width=70 to match it. Desktop only: at ≤768px the figure always spans the full column.
alignleftSame as img: left | center | right.
width_captionmirrors widthSame as img. Pass 100 for the full text column regardless of player width.
align_captionleftSame as img: left (ragged-right) | center | right.

Default

{{< ytvideo id="dC8I9PD8V5A" caption="..." credit="..." >}}
Keith Beven and Jeffrey McDonnell in conversation. — © The Global Institute for Water Security

Accepts a full URL, not just an id

All three of these resolve to the same video, so a link pasted straight from the browser works:

{{< ytvideo id="dC8I9PD8V5A" >}}
{{< ytvideo id="https://www.youtube.com/watch?v=dC8I9PD8V5A&ab_channel=X" >}}
{{< ytvideo id="https://youtu.be/dC8I9PD8V5A" >}}

width=40 and align="center"

{{< ytvideo id="..." width=40 align="center" caption="..." >}}
width=40 align="center" — the caption box mirrors the player width by default. — © Undefined

width_caption=100 — caption ignores player width

{{< ytvideo id="..." width=40 width_caption=100 caption="..." >}}
Same 40% player, but width_caption=100 forces the caption to span the full text column instead of mirroring it. — © Undefined

ratio and start

{{< ytvideo id="..." ratio="4/3" start="30" caption="..." >}}
ratio="4/3" boxes the player for older footage; start="30" opens 30 seconds in. — © Undefined

Credit normalization — identical to img

An empty/missing credit falls back to italic “Undefined”; an existing (c) or © in the string is stripped before the single © prefix is added.

{{< ytvideo id="..." caption="Credit with a redundant (c)" credit="(c) Jane Doe" >}}
Credit with a redundant (c) — © Jane Doe

Code blocks

Fenced blocks are highlighted by Hugo’s built-in Chroma. hugo.toml sets markup.highlight.noClasses = false, so Chroma emits CSS classes instead of inline colours, and the palette lives in assets/code-highlight.css — catppuccin-latte for light, catppuccin-frappe for dark. That file’s header comment carries the hugo gen chromastyles commands to regenerate it.

Basic — the language tag sets the lexer

Put the language right after the opening fence. Omit it and there is no highlighting at all — just a plain <pre> with no chroma class.

```python
def hand(dem, drainage):
    return dem - drainage
```
def hand(dem, drainage):
    """Height above nearest drainage."""
    return dem - drainage

Options

Options go in braces after the language, comma-separated.

OptionExampleEffect
linenoslinenos=trueLine numbers in a side table (table behaves the same as true).
linenos=inlinelinenos=inlineLine numbers inline, so they are selected along with the code.
hl_lineshl_lines=[2,"4-6"]Highlight lines. Single numbers and "from-to" ranges both work.
linenostartlinenostart=10Number the first line as 10 — useful when quoting an excerpt.

linenos with hl_lines

```python {linenos=true, hl_lines=["2-3"]}
def exposure_curve(hand, addresses):
    levels = classify(hand)
    counts = tally(addresses, levels)
    return counts.cumsum()
```
1
2
3
4
def exposure_curve(hand, addresses):
    levels = classify(hand)
    counts = tally(addresses, levels)
    return counts.cumsum()

linenostart — an excerpt keeping its real line numbers

```python {linenos=true, linenostart=142}
for cell in basin:
    yield cell.elevation - cell.drainage
```
142
143
for cell in basin:
    yield cell.elevation - cell.drainage

Nesting fences

Each source example above is wrapped in a four-backtick fence, because the block it shows uses three. A three-backtick wrapper would be closed by the inner fence instead of its own, silently shifting the rest of the page.

Showing shortcode source without running it

Every block on this page that displays shortcode source uses the comment form of the delimiters, {{< and >}}, which Hugo prints instead of executing. Without it the example would render the shortcode a second time.