Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SVG Templates

SVG templates define the visual layout of your screens. They use Tera templating syntax to insert data from your Lua scripts.

Template Basics

A Byonk SVG template is a standard SVG file with Tera expressions:

<svg xmlns="http://www.w3.org/2000/svg"
     viewBox="0 0 {{ layout.width }} {{ layout.height }}"
     width="{{ layout.width }}" height="{{ layout.height }}">
  <rect width="{{ layout.width }}" height="{{ layout.height }}" fill="white"/>

  <text x="{{ layout.center_x }}" y="{{ layout.center_y }}" text-anchor="middle" font-size="24">
    {{ message }}
  </text>
</svg>

Key points:

  • Size the viewBox from layout.width and layout.height, never from fixed numbers
  • Always include width and height attributes
  • Use {{ variable }} to insert values from Lua

Display Dimensions

DeviceWidthHeightAspect Ratio
TRMNL OG8004805:3
TRMNL X187214044:3

Build the size from layout.width and layout.height. These are the panel’s own pixels, so a template that uses them is always drawn at scale 1, on every device.

If your SVG is some other size, Byonk scales it to fit rather than failing — and that costs you more than sharpness:

  • Every dimension you chose is displayed at the wrong size. Type set at 10 px in a 400x240 SVG is 20 px on an 800x480 panel. You cannot judge a layout that is not being shown at the size you designed it.
  • Hinting stops helping. Hinted glyph outlines are fitted to the pixel grid of the SVG’s own coordinate system, not the panel’s.
  • Bitmap fonts stop being bitmaps. A strike is drawn for one exact pixel size, so it is only reproduced faithfully at scale 1; at any other scale it is resampled.

Byonk reports this when it happens. byonk render prints it to stderr, and the authoring API returns it in the render log:

[warn] this screen's SVG is 400x240 but the device is 800x480, so the render is
scaled by 2 to fit ... Use layout.width and layout.height rather than hardcoded
dimensions.

A mismatched aspect ratio is scaled by the smaller of the two ratios and centred, so the render is also padded with blank bands.

The remaining examples on this page write 0 0 800 480 literally, to keep them short. In a real screen, use layout.width and layout.height.

Variables

Template Namespaces

Variables in templates are organized into four namespaces:

NamespaceSourceExample
data.*Lua script return valuedata.title, data.items
device.*Device info (battery, signal)device.battery_voltage, device.rssi
params.*Config params from config.yamlparams.station, params.limit
layout.*Pre-computed layout valueslayout.width, layout.grey_count

Device Variables

These are automatically available under device.*:

VariableTypeDescription
device.macstringDevice MAC address (e.g., “AC:15:18:D4:7B:E2”)
device.battery_voltagefloat or nilBattery voltage (e.g., 4.12)
device.rssiinteger or nilWiFi signal strength in dBm (e.g., -65)
device.modelstring or nilDevice model (“og” or “x”)
device.firmware_versionstring or nilFirmware version string
device.widthinteger or nilDisplay width in pixels (800 or 1872)
device.heightinteger or nilDisplay height in pixels (480 or 1404)
<!-- Display battery and signal in header -->
<text class="status" x="780" y="25" text-anchor="end">
  {% if device.battery_voltage %}{{ device.battery_voltage | round(precision=2) }}V{% endif %}
  {% if device.rssi %} · {{ device.rssi }}dBm{% endif %}
</text>

<!-- Responsive layout based on device dimensions -->
{% if device.width == 1872 %}
  <!-- TRMNL X layout (1872x1404) -->
{% else %}
  <!-- TRMNL OG layout (800x480) -->
{% endif %}

Note: Some device variables may be nil if the device doesn’t report them. Always use {% if device.variable %} to check before using.

Layout Variables

Pre-computed layout values are available under layout.*. These mirror the layout table available in Lua scripts:

VariableTypeDescription
layout.widthintegerDisplay width in pixels (default 800)
layout.heightintegerDisplay height in pixels (default 480)
layout.scalefloatScale factor relative to 800×480 base
layout.center_xintegerHorizontal center (width / 2)
layout.center_yintegerVertical center (height / 2)
layout.marginintegerStandard margin (20px × scale)
layout.margin_smintegerSmall margin (10px × scale)
layout.margin_lgintegerLarge margin (40px × scale)
layout.colorsarrayDisplay color palette (hex strings)
layout.color_countintegerNumber of colors in palette (default 4)
layout.grey_countintegerNumber of grey levels in palette (default 4)

This is useful for conditional logic in SVG templates without needing Lua to pass the values through — a template can, for example, choose a denser layout on a small panel by branching on layout.width.

Basic Interpolation

<text>{{ data.title }}</text>
<text>{{ data.user.name }}</text>
<text>{{ data.items[0].label }}</text>

Filters

Apply filters to modify values:

<!-- Truncate long text -->
<text>{{ data.description | truncate(length=50) }}</text>

<!-- Format timestamp (uses UTC) -->
<text>{{ data.updated_at | format_time(format="%H:%M") }}</text>

<!-- Get length -->
<text>{{ data.items | length }} items</text>

Tip: The format_time template filter uses UTC timezone. For local time formatting, use time_format() in your Lua script and pass the pre-formatted string to the template.

Default Values

<text>{{ data.title | default(value="Untitled") }}</text>

Control Flow

Conditionals

{% if data.error %}
  <text fill="red">Error: {{ data.error }}</text>
{% else %}
  <text>All systems operational</text>
{% endif %}

Comparisons

{% if data.count > 0 %}
  <text>{{ data.count }} items</text>
{% elif data.count == 0 %}
  <text>No items</text>
{% endif %}

{% if data.status == "active" %}
  <circle fill="green" r="10"/>
{% endif %}

Boolean Checks

{% if data.is_online %}
  <text fill="green">Online</text>
{% endif %}

{% if not data.items %}
  <text>No data available</text>
{% endif %}

Loops

Basic Loop

{% for item in data.items %}
  <text y="{{ 100 + loop.index0 * 30 }}">{{ item.name }}</text>
{% endfor %}

Loop Variables

VariableDescription
loop.indexCurrent iteration (1-indexed)
loop.index0Current iteration (0-indexed)
loop.firstTrue on first iteration
loop.lastTrue on last iteration

Positioning with Loops

{% for dep in data.departures %}
  <!-- Calculate Y position based on index -->
  <text y="{{ 80 + loop.index0 * 40 }}">
    {{ dep.time }} - {{ dep.destination }}
  </text>
{% endfor %}

Conditional Styling in Loops

{% for item in data.items %}
  <!-- Alternating row backgrounds -->
  {% if loop.index0 is odd %}
    <rect y="{{ 100 + loop.index0 * 40 }}" width="800" height="40" fill="#f5f5f5"/>
  {% endif %}

  <text y="{{ 125 + loop.index0 * 40 }}">{{ item.name }}</text>
{% endfor %}

Empty State

{% if data.items | length > 0 %}
  {% for item in data.items %}
    <text>{{ item.name }}</text>
  {% endfor %}
{% else %}
  <text fill="#999">No items found</text>
{% endif %}

Styling

Inline Styles

<text x="20" y="40"
      font-family="sans-serif"
      font-size="24"
      font-weight="bold"
      fill="black">
  {{ data.title }}
</text>

CSS in Style Block

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 480">
  <style>
    .title { font-family: sans-serif; font-size: 32px; font-weight: bold; }
    .subtitle { font-family: sans-serif; font-size: 18px; fill: #666; }
    .highlight { fill: #333; font-weight: bold; }
  </style>

  <text class="title" x="20" y="40">{{ data.title }}</text>
  <text class="subtitle" x="20" y="70">{{ data.subtitle }}</text>
</svg>

Variable Fonts

Byonk supports variable fonts via CSS font-variation-settings:

<style>
  .light { font-family: Outfit; font-variation-settings: "wght" 300; }
  .regular { font-family: Outfit; font-variation-settings: "wght" 400; }
  .bold { font-family: Outfit; font-variation-settings: "wght" 700; }
</style>

Note: Place custom font files (e.g., Outfit-Variable.ttf) in the fonts/ directory.

Bundled Fonts

Byonk ships these outline families, so they render the same on your machine and on the device. (The device image contains no system fonts at all — anything not bundled simply does not draw.)

FamilyUse
OutfitThe house sans. Variable weight 100–900.
'Source Sans 3'What sans-serif resolves to. Variable weight 200–900.
'Source Serif 4'What serif resolves to. Variable weight and optical size.
'Source Code Pro'What monospace resolves to. Variable weight 200–900.
'Terminus (TTF)'A pixel face, plus 26 X11* bitmap families — see fonts/FONTS.md.

cursive and fantasy resolve to Outfit; byonk bundles nothing decorative.

Quote any family name that ends in a digit or contains parentheses. CSS reads an unquoted font-family as a list of identifiers, and 3, 4 and (TTF) are not identifiers — the whole declaration is discarded and your text silently comes out in some other face. There is no warning.

<!-- Wrong: renders in a fallback face -->
<text font-family="Source Sans 3">…</text>
<text font-family="Terminus (TTF)">…</text>

<!-- Right -->
<text font-family="'Source Sans 3'">…</text>
<text font-family="'Terminus (TTF)'">…</text>

Outfit and Source Code Pro contain neither, so they are safe either way.

You do not need to ask for a weight: text with no font-weight renders at 400, not at whatever the variable font’s own default happens to be. Source Sans 3 and Source Code Pro default internally to 200 (ExtraLight), and that default does not reach your screen.

Colors and Palettes

E-ink displays support a limited color palette. The default 4-grey OG palette is #000000, #555555, #AAAAAA, #FFFFFF, but color displays may have palettes like #000000, #FFFFFF, #FF0000, #FFFF00. The display palette is available in Lua via layout.colors.

The palette follows a priority chain: Lua script colors return > device config colors > firmware Colors header > system default. You can override the palette per-device in config.yaml:

devices:
  "ABCDE-FGHJK":
    screen: examples/swiss-departure-board
    colors: "#000000,#FFFFFF,#FF0000"

Or per-script by returning colors in the Lua result table (see Lua API: colors).

Dithering Mode

Byonk supports two dithering modes via the dither option:

  • graphics (default) — Blue noise ordered dithering, best for UI content
  • photo — Atkinson error diffusion, best for photographs

Set per-device in config.yaml or per-script in the Lua return table (see Lua API: dither and Content Pipeline: Rendering Intents).

Using Palette Colors

For the cleanest output, use colors from the display palette directly. Byonk will dither any color to the nearest palette color, but exact palette matches are preserved without dithering.

-- In your Lua script
local colors = layout.colors  -- e.g., {"#000000", "#555555", "#AAAAAA", "#FFFFFF"}
<!-- Use palette colors for crisp rendering -->
{% for swatch in data.swatches %}
<rect x="{{ swatch.x }}" width="{{ swatch.width }}" height="100" fill="{{ swatch.color }}"/>
{% endfor %}

Marking continuous-tone content

Byonk renders a screen as two kinds of content, and you choose which is which by marking the continuous-tone parts:

<!-- A photograph, or a gradient that sweeps through hues -->
<image data-byonk-tone="continuous" href="photo.jpg" .../>

Everything not marked is treated as structure — text, rules, logos, flat fills, UI chrome. The difference is not cosmetic:

Unmarked (structure) — the defaultMarked continuous
Matched againstOfficial palette (device.colors)Measured palette (device.colors_actual)
Gamut mappingoffon
Exact-match pinningon — an official colour comes out as that one ink, flatoff

For structure this is what you want. #FF0000 is simply red: it matches at distance zero, pins, and renders as one flat ink with no speckle. Black text next to a saturated block stays black instead of picking up diffused colour error.

For a photograph it is not. Nominal matching aims a photo at primaries the panel cannot physically produce, so an unmarked photograph looks markedly worse than a marked one. This is the one mistake that costs you real quality — mark your photographs and your hue gradients.

Two rules that are easy to get wrong:

  • Mark the element that is continuous-tone, never a group around it. A <g> wrapper will swallow neighbouring labels and captions, turning text into continuous-tone content and switching off its pinning.
  • Don’t mark achromatic (grey) gradients. Grey is always in gamut, so mapping it is a no-op — while marking it switches exact-match pinning off across the whole gradient, for no gain. Before marking anything, ask whether the content can even be out of gamut.

Marking rasterizes in document order, so an unmarked element drawn after a marked one paints over it and reverts those pixels to structure. Text over a photograph therefore needs no special handling, as long as it comes later in the document.

Testing Display Colors

The included graytest screen adapts to the device palette and shows all available colors as swatches with gradient and dithering tests.

4-grey palette (TRMNL OG default):

Display color test — 4 grey

6-color palette (color e-ink display):

Display color test — 6 color

The default screen also adapts to the palette:

Default screen — 6 color

Avoid

  • Gradients - Convert to dithered patterns (may look noisy)
  • Subtle color differences - May become indistinguishable on limited palettes
  • Colors not in palette - Will be dithered to nearest match

Font Rendering for E-ink

Byonk hints text for you. There is nothing to put in the template — hinting is chosen per render from the palette the device reports, so the same screen does the right thing on a black-and-white panel and on a greyscale one.

Only override it if you have a reason to. That is done from script.lua with the font_hinting directive, not from CSS: see Font Hinting for the full surface, including variants, which let one family appear twice in a screen with different treatment.

If you are updating an older screen: the -resvg-hinting-* CSS properties no longer exist, and {% include "byonk-base-v1/hinting.svg" %} is now inert. Including it still works and renders identically, so it can simply be deleted. Anything that set -resvg-hinting-* directly must move into the font_hinting directive.

Properties that still matter

PropertyValuesApplies toDescription
shape-renderingauto, crispEdges, geometricPrecisionshapes onlycrispEdges disables anti-aliasing on lines and rectangles. It has no effect inside a text rule — text takes its rasterization from text-rendering.
text-renderingauto, optimizeSpeed, optimizeLegibility, geometricPrecisiontextoptimizeLegibility restores anti-aliasing and keeps hinting. geometricPrecision restores anti-aliasing but disables hinting.

text-rendering is worth knowing about for one specific case: on a black-and-white panel byonk draws the whole document 1-bit, and any text that is not mono-hinted can lose stems. Setting text-rendering="optimizeLegibility" on those elements is the fix. Byonk warns you when a screen sets this up.

Hinting Demo Screen

examples/demo/font/hinting renders a 3×3 grid of font_hinting variants over one family — engine (auto, interpreter, auto_fallback) against target (mono, smooth, and a hinting-off control) at six sizes. It is the worked example for variants, and a useful thing to put on your own panel.

Several cells deliberately coincide, which is the most useful thing the grid teaches: for a font carrying no usable hinting program the target matters and the engine barely does. The image states which coincidences are expected.

Hinting demo screen

Tips

  • Choose font sizes that land on whole pixel boundaries. Fractional pixel heights cause glyphs to snap to the grid differently, producing inconsistent shapes.
  • Quote interpolated font families. font-family="{{ line.family }}" is invalid CSS as soon as the name contains anything but plain identifiers — a family like Terminus (TTF) silently falls back to a serif. Write font-family="'{{ line.family }}'".
  • Don’t set font-family in both a CSS rule and an attribute. A presentation attribute is the lowest-priority source in SVG, so text { font-family: … } in a <style> block silently overrides every font-family="…" attribute on matching elements. The text still renders, in the wrong face. Set it on a class or on the element, not both.
  • Put text on whole-pixel positions, not just whole-pixel sizes. Hinting fits the outline to the pixel grid; a baseline at y="80.667" then slides the fitted glyph back off it. Measured on byonk’s own demo, fractional baselines cost 3–5% of the ink to dropped stems — more than the difference between two hinting engines.
  • Test on your actual display. What reads well depends on the font, the size and the panel.

Bitmap Fonts

Byonk ships with X11 bitmap fonts converted to TTF files. These contain embedded bitmap strikes — pre-rendered glyphs at specific pixel sizes — which produce perfectly crisp text on e-ink displays without any hinting artifacts.

Available Families

Proportional fonts:

FamilyStylesPixel Sizes
X11HelvRegular, Bold, Oblique, BoldOblique8, 10, 11, 12, 14, 17, 18, 20, 24, 25, 34
X11LuSansRegular, Bold, Oblique, BoldOblique8–34 (13 sizes)
X11LuTypeRegular, Bold8–34 (13 sizes)
X11TermRegular, Bold14, 18

Fixed-width fonts (grouped by cell width):

FamilyStylesPixel Sizes
X11Misc5xRegular6, 7, 8
X11Misc6xRegular, Bold, Oblique9, 10, 12, 13
X11Misc7xRegular, Bold, Oblique13, 14
X11Misc8xRegular, Bold, Oblique13, 16
X11Misc9xRegular, Bold15, 18
X11Misc10xRegular20
X11Misc12xRegular24

Usage

Set font-family to the family name and font-size to a pixel size that matches a bitmap strike. The renderer automatically selects the closest strike:

<text font-family="X11Helv" font-size="14">Proportional text</text>
<text font-family="X11Helv" font-size="14" font-weight="700">Bold</text>
<text font-family="X11Misc7x" font-size="13">Fixed width</text>
<text font-family="X11Misc7x" font-size="13" font-style="oblique">Fixed oblique</text>

For sizes without an exact bitmap strike, autotraced scalable outlines are used as fallback — but these won’t look as clean as the native bitmap sizes.

Bitmap Font Demo Screen

The example examples/demo/font/bitmap screen showcases all sizes and styles for a given font family. Assign it to a device and configure it with the font_prefix parameter:

devices:
  "YOUR:MAC:AD:DR:ES:S0":
    screen: examples/demo/font/bitmap
    params:
      font_prefix: X11Helv   # or X11LuSans, X11LuType, X11Term, X11Misc

This renders each available size and style combination as a labeled line, useful for picking the right font and size for your screen.

Bitmap font demo - X11Helv

Bitmap font demo - X11Misc

Layout Patterns

Header + Content

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 480">
  <!-- Header bar -->
  <rect width="800" height="70" fill="black"/>
  <text x="30" y="48" fill="white" font-size="28">{{ data.title }}</text>
  <text x="770" y="48" fill="#aaa" font-size="14" text-anchor="end">{{ data.time }}</text>

  <!-- Content area -->
  <g transform="translate(0, 70)">
    <!-- Your content here, Y coordinates start at 0 -->
  </g>
</svg>

Grid Layout

{% for item in data.items %}
  {% set col = loop.index0 % 3 %}
  {% set row = loop.index0 // 3 %}

  <rect x="{{ col * 266 }}" y="{{ 80 + row * 100 }}"
        width="260" height="90" fill="#f0f0f0" rx="5"/>
  <text x="{{ col * 266 + 130 }}" y="{{ 130 + row * 100 }}"
        text-anchor="middle">{{ item.name }}</text>
{% endfor %}

Two Columns

<!-- Left column -->
<text x="30" y="100">Left content</text>

<!-- Divider -->
<line x1="400" y1="80" x2="400" y2="450" stroke="#ccc"/>

<!-- Right column -->
<text x="430" y="100">Right content</text>

Dynamic Styling

Conditional Colors

{% for item in data.items %}
  <text fill="{% if item.is_urgent %}red{% else %}black{% endif %}">
    {{ item.name }}
  </text>
{% endfor %}

Dynamic Classes

<text class="{% if data.count > 100 %}highlight{% else %}normal{% endif %}">
  {{ data.count }}
</text>

Status Indicators

{% if data.status == "online" %}
  <circle cx="20" cy="20" r="8" fill="green"/>
{% elif data.status == "warning" %}
  <circle cx="20" cy="20" r="8" fill="orange"/>
{% else %}
  <circle cx="20" cy="20" r="8" fill="red"/>
{% endif %}

Common Patterns

Truncating Long Text

<text>
  {% if data.title | length > 30 %}
    {{ data.title | truncate(length=30) }}
  {% else %}
    {{ data.title }}
  {% endif %}
</text>

Formatted Numbers

Use Lua to format numbers before passing to template:

-- In Lua script
return {
  data = {
    temperature = string.format("%.1f°C", temp),
    price = string.format("$%.2f", amount)
  }
}

Time-Based Styling

-- In Lua script
local hour = tonumber(time_format(time_now(), "%H"))
return {
  data = {
    is_night = hour < 6 or hour > 20
  }
}
<rect width="800" height="480" fill="{% if is_night %}#333{% else %}white{% endif %}"/>

Debugging Templates

Show Raw Data

<!-- Temporarily add this to see all data -->
<text x="10" y="460" font-size="10" fill="#999">
  Debug: {{ data.items | length }} items
</text>

Check for Missing Data

{% if not data.title %}
  <text fill="red">ERROR: title is missing!</text>
{% endif %}

Template Errors

If your template has a syntax error, Byonk will display an error screen with the message. Check the server logs for details.

Embedding Images

Byonk supports embedding images in your SVG templates. You can include PNG, JPEG, GIF, WebP, and SVG files.

Asset Directory Structure

Place image assets inside the screen’s own folder, alongside its script.lua, screen.svg, and meta.yaml:

screens/
└── hello/                # screen ref: local/hello
    ├── meta.yaml
    ├── script.lua
    ├── screen.svg
    ├── logo.png
    ├── icon.svg
    └── background.jpg

Method 1: Direct in SVG (Automatic Resolution)

Simply reference images by filename in your SVG template. Byonk automatically resolves relative paths to the screen’s own folder and embeds them as data URIs:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 480">
  <!-- This automatically loads the screen folder's logo.png -->
  <image x="10" y="10" width="64" height="64" href="logo.png"/>

  <text x="100" y="50">{{ data.greeting }}</text>
</svg>

Supported image formats:

  • PNG (.png)
  • JPEG (.jpg, .jpeg)
  • GIF (.gif)
  • WebP (.webp)
  • SVG (.svg)

Notes:

  • Paths are relative to the screen’s asset directory
  • URLs starting with data:, http://, or https:// are left unchanged
  • Missing images log a warning but don’t break rendering

Method 2: Via Lua (For Dynamic Images)

For more control, use read_asset() and base64_encode() in your Lua script:

screens/hello/script.lua:

local icon = read_asset("icon.png")

return {
    data = {
        greeting = "Hello World!",
        icon_src = "data:image/png;base64," .. base64_encode(icon)
    },
    refresh_rate = 3600
}

screens/hello/screen.svg:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 480">
  <image x="10" y="10" width="64" height="64" href="{{ data.icon_src }}"/>
  <text x="100" y="50">{{ data.greeting }}</text>
</svg>

This method is useful when you need to:

  • Conditionally include images
  • Fetch images from external URLs
  • Process or transform image data

Background Images

To use a full-screen background image:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 480" width="800" height="480">
  <!-- Background image -->
  <image x="0" y="0" width="800" height="480" href="background.png" preserveAspectRatio="xMidYMid slice"/>

  <!-- Content on top -->
  <text x="400" y="240" text-anchor="middle" fill="white" font-size="32">
    {{ data.title }}
  </text>
</svg>

Tips for background images:

  • Use preserveAspectRatio="xMidYMid slice" to cover the entire area
  • Consider e-ink limitations: high-contrast images work best
  • Keep file sizes reasonable for fast rendering

Template Reusability

Byonk supports Tera’s template inheritance and includes for reusable components. Shared SVG comes from two places:

  • byonk-base-v1/… — byonk’s built-in standard library of shared layouts and components (the base layout, header.svg, footer.svg, status_bar.svg), versioned by the -vN suffix so a future byonk-base-v2 can change the contract without breaking existing screens.
  • Repo-relative paths — any .svg file inside your own screen package, referenced by its path relative to the package root (e.g. parts/panel.svg). Because a package is one atomic, versioned unit, a screen and the shared SVG it uses can never drift out of sync.

Package Structure

A package is a directory with a byonk-screens.yaml manifest at its root; shared SVG lives alongside the screen folders:

my-screens/                  # a package (its own repo, or the embedded byonk-builtin tree)
├── byonk-screens.yaml       # package manifest
├── parts/
│   └── panel.svg            # shared SVG, referenced as "parts/panel.svg"
└── dashboard/               # a screen folder
    ├── meta.yaml
    ├── script.lua
    └── screen.svg

Template Inheritance (extends)

Use byonk’s base layout, which defines the overall structure with replaceable blocks:

dashboard/screen.svg:

{% extends "byonk-base-v1/base.svg" %}

{% block title %}My Screen{% endblock %}

{% block content %}
<text x="400" y="200" text-anchor="middle" font-size="32">
  {{ data.message }}
</text>
{% endblock %}

{% block footer %}
<text x="400" y="460" text-anchor="middle" fill="#999" font-size="12">
  Updated: {{ data.updated_at }}
</text>
{% endblock %}

Key points:

  • Use {% extends "byonk-base-v1/base.svg" %} at the start of your template (or a repo-relative base of your own, e.g. {% extends "layouts/base.svg" %} where that path is inside your package)
  • Define blocks with {% block name %}...{% endblock %}
  • Child templates override parent blocks
  • Unoverridden blocks use the parent’s default content

Template Includes

Include reusable components in your templates. Reference byonk’s standard components by their byonk-base-v1/… path, or your own shared SVG by a repo-relative path:

dashboard/screen.svg:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 480" width="800" height="480">
  <rect width="800" height="480" fill="white"/>

  <!-- Include a standard-library component -->
  {% include "byonk-base-v1/header.svg" %}

  <!-- Main content -->
  <g transform="translate(0, 60)">
    <text x="400" y="200" text-anchor="middle">{{ data.message }}</text>
  </g>

  <!-- Include your own shared SVG (repo-relative) -->
  {% include "parts/panel.svg" %}
</svg>

Key points:

  • Use {% include "byonk-base-v1/filename.svg" %} for standard-library components, or a repo-relative path for your package’s own shared SVG

  • Included templates have access to all variables in the current context — but note that your script’s data arrives namespaced under data.*, while the standard components read bare names like title. Bridge the two with {% set %} before the include, or the component silently falls back to its default:

    {% set title = data.headline %}
    {% include "byonk-base-v1/header.svg" %}
    
  • Components work well for headers, footers, status bars, and other repeated elements

Standard-Library Components

Byonk’s byonk-base-v1 package ships several ready-to-use components:

ComponentDescription
byonk-base-v1/base.svgBase layout with title/content/footer blocks (for {% extends %})
byonk-base-v1/header.svgBlack title bar across the top 60px
byonk-base-v1/footer.svgFooter with timestamp (updated_at) and optional text
byonk-base-v1/hinting.svgDeprecated and inert. Hinting moved into the server; see Font Hinting. Kept so existing screens keep working.
byonk-base-v1/status_bar.svgWiFi and battery indicators, drawn into the header’s top-right corner

These components are designed to stack without overlapping: header.svg owns the top 60px, status_bar.svg the corner inside it, and footer.svg the bottom 30px.

  • The timestamp belongs to the footer. header.svg does not draw one, because that is where status_bar.svg’s icons go. Set updated_at and include footer.svg.

  • The status icons default to light ink (rgb(200,200,200)), since they sit on the black header bar. To place them anywhere else, set both position and colour:

    {% set status_y = 70 %}
    {% set status_color = "rgb(80,80,80)" %}
    {% include "byonk-base-v1/status_bar.svg" %}
    

Combining Extends and Includes

You can use both in the same template:

{% extends "byonk-base-v1/base.svg" %}

{% block title %}Dashboard{% endblock %}

{% block header_extra %}
{% include "byonk-base-v1/status_bar.svg" %}
{% endblock %}

{% block content %}
<text x="400" y="200" text-anchor="middle">{{ data.message }}</text>
{% endblock %}

Next Steps