Juicer
English

Template syntax

Squiggly cheatsheet — the moves you’ll use on every page.

Squiggly is the template language juicer uses. It’s Go-template-like. This page is the practical cheatsheet; the full squiggly reference covers everything else.

Substitution

{{ .name }}              value at .name in the data context
{{ .page.title }}        nested
{{ .args[0] }}           list element by index (in shortcodes)

Built-in functions

Juicer adds a few project-specific functions to squiggly’s defaults. Call them as expressions:

FunctionWhat
{{ relURL '/path' }}Site-relative URL (baseURL.path prefix added if non-trivial)
{{ absURL '/path' }}Absolute URL (baseURL.base + relURL)
{{ relLangURL lang '/path' }}Like relURL, but prefixed with lang‘s URL segment (Hugo’s relLangURL) — see i18n
{{ absLangURL lang '/path' }}Like absURL, but prefixed with lang‘s URL segment
{{ markdownify s }}Render a markdown string to HTML
{{ emojify s }}Replace :smile: etc. with Unicode emoji
{{ i18n lang 'key' }}Look up an i18n string (falls back to default lang then literal key)
{{ ogTags .page }}Emit OpenGraph + Twitter card <meta> tags for a page record
{{ imageDims '/path' }}Read pixel dimensions of an on-disk image; returns {width, height} or empty map (see below)
{{ imageVariants '/path' }}Generate resized + reformatted variants of an image; returns {original, originalWidth, originalHeight, variants} (see below)
{{ srcset '/path' 'fmt' }}One-liner: build the comma-separated srcset body for one variant format (see below)

imageDims

Reads the header of an image on disk and returns a map with width and height (pixel ints). Useful for emitting <img width=... height=...> so the browser can reserve space before the bytes arrive — eliminates cumulative layout shift.

{{ d := imageDims '/img/hero.jpg' }}
{{ if d.width }}
  <img src="/img/hero.jpg" width="{{ d.width }}" height="{{ d.height }}" alt="" />
{{ else }}
  <img src="/img/hero.jpg" alt="" />
{{ end }}

Resolution order: the path is looked up first under the built output directory (so theme + site static/ files and any generated images work), then under the source root. Paths can be site-absolute (/img/x.png) or relative; absolute URLs (http://…) are not fetched.

Returns an empty map when the file is missing, unreadable, or in a format the header parser does not understand. Recognized formats: PNG, JPEG (baseline + progressive), GIF87a/89a, WebP (VP8 / VP8L / VP8X). Pure Scala — no javax.imageio, no FFI, works on every target.

Per-build cache: each unique path is read once even if a shortcode fires across hundreds of pages.

imageVariants and srcset

Opt-in build-time image-variant generation. With an [images] block in site.toml and ImageMagick (magick) on PATH, juicer will resize and reformat source images into a <picture>-ready variant set:

[images]
enabled  = true
widths   = [320, 640, 960, 1280]
formats  = ["webp", "original"]   # most-modern first; original is a passthrough# most-modern first; original is a passthrough
quality  = 80
cacheDir = ".image-cache"

imageVariants returns the full structured shape — useful when a theme emits its own <picture> markup:

{{ v := imageVariants '/img/hero.jpg' }}
{{ if v.variants }}
  <picture>
    {{ for src <- v.variants }}
      {{ if src.mime and src.format != 'original' }}
        <source srcset="{{ src.url }} {{ src.width }}w" type="{{ src.mime }}" />
      {{ end }}
    {{ end }}
    <img src="{{ v.original }}" width="{{ v.originalWidth }}" height="{{ v.originalHeight }}" alt="" />
  </picture>
{{ else }}
  <img src="{{ v.original }}" alt="" />
{{ end }}

Returned map:

KeyTypeWhat
originalstringURL of the passthrough original (site-absolute)
originalWidthintPixel width of the source (0 if unknown)
originalHeightintPixel height of the source (0 if unknown)
variantslistOne {width, format, url, mime} per generated variant

srcset is a one-liner shorthand for the common case where a layout wants to drop the variant list straight into an <img srcset=...> attribute for a single format:

<img src="/img/hero.jpg"
     srcset="{{ srcset '/img/hero.jpg' 'webp' }}"
     sizes="(min-width: 800px) 50vw, 100vw"
     alt="" />

Returns "" when the requested format isn’t in [images].formats, the source can’t be resolved, or variants weren’t generated (feature off, encoder missing, etc.).

Cache & encoder. Every generated filename embeds a 64-bit FNV-1a hash of the source bytes (hero-640w.<hash>.webp), so re-running the build on an unchanged image skips the encoder shell-out and editing the image invalidates every variant in one shot. The encoder backend (magick) is probed once per build; if it’s not on PATH a single advisory prints to stderr and the feature degrades cleanly to a passthrough-only VariantSet (variants is empty). Same behaviour on the Scala Native and Scala.js targets until process-spawn lands in cross_platform.

Widths above the source’s own width are dropped (no upscaling); the source’s exact width is always included so the fallback <img> can point at a same-size variant. The original slot at the source width is a 1:1 byte copy rather than an encoder round-trip — keeps build time sane on photo-heavy sites and preserves source bytes exactly.

Conditionals

{{ if .page.summary }}
  <p class="lead">{{ .page.summary }}</p>
{{ end }}

{{ if .page.draft }}
  <span class="badge">Draft</span>
{{ else }}
  <span class="badge">Published</span>
{{ end }}

if is truthy on non-empty strings, non-zero numbers, non-empty lists/maps, and true. null / empty / false / 0 are falsy.

Logical operators: and, or, prefix not.

{{ if .page.prev or .page.next }}
  <nav>...</nav>
{{ end }}

Loops

{{ for p <- .section.pages }}
  <li><a href="{{ p.url }}">{{ p.title }}</a></li>
{{ end }}

{{ for k, v <- .page }}
  {{ k }}: {{ v }}
{{ end }}

Partials

{{ partial 'topbar' . }}              call partials/topbar.html with the current data
{{ partial 'page-toc' .page }}        with a different data context

Defines & blocks

{{ define main }}
  <article>{{ .content }}</article>
{{ end }}

{{ block main . }}{{ .content }}{{ end }}    fallback content if main isn't defined

define is statement-level (no output). block <name> <data> looks up the named block, calls it with <data> as the context, falls back to its inner template if the block isn’t defined.

Builtin filters

Most squiggly builtins work as either a function or a pipe target:

{{ trim .page.summary }}
{{ .page.summary | trim }}
{{ .name | upper }}
{{ .x | replace 'foo' 'bar' }}

Useful ones:

BuiltinWhat
lenLength of a list / string
headFirst element of a list
tailAll but the first
trimTrim whitespace
upper / lowerCase
replace 'a' 'b'String substitution
split 'sep'String split
join 'sep'List join

Comments

{{ // squiggly's not too noisy in templates }}

Scope inside loops

Inside a {{ for x <- coll }} block, .foo resolves against the current iterated element, not the outer page context. Use $.foo to reach back up to the top-level data root:

{{ for p <- .section.pages }}
  <li>
    <a href="{{ p.url }}">{{ p.title }}</a>
    {{ if eq p.url $.page.url }}<span>(this page)</span>{{ end }}
  </li>
{{ end }}

A common pitfall: writing {{ .site.title }} inside a loop and getting nothing. Use {{ $.site.title }}.site doesn’t exist on the iterated element.

Search

Esc
to navigate to open Esc to close