BiblioViva Documentazione
Documentazione tecnica

Print templates: authoring guide

Cards, labels and slips are rendered from template folders (PLAN.md §3.12). A folder is copied, renamed and edited; no code is involved. This page covers the parts common to every kind, the card templates, the label templates and the slips in detail.

Where templates live

KindBuilt-in (shipped with the app)Yours (configuration volume)
Cardsconfig/cards/templates/<name>/CONFIG_DIR/cards/templates/<name>/
Labelsconfig/labels/templates/<name>/CONFIG_DIR/labels/templates/<name>/
Slipsconfig/slips/templates/<name>/CONFIG_DIR/slips/templates/<name>/

A folder in CONFIG_DIR with the same name as a built-in replaces it. The built-ins are copied into CONFIG_DIR at the first start, so editing CONFIG_DIR/cards/templates/default/ changes the default card; copying that folder to CONFIG_DIR/cards/templates/mia-scuola/ and pointing cards.defaultTemplate (or cards.templateByPatronType) at mia-scuola keeps the original intact. Names: letters, digits, - and _.

A copy that nobody edited follows the application: at every start, a folder of CONFIG_DIR whose every file is a version the application shipped (spaces and line breaks of HTML, CSS and JSON do not count) is replaced by the current built-in, so improvements and fixes of the built-ins reach every installation. Once any file of the copy is changed (by hand, in the designer, or with an asset uploaded) the copy is yours and is never replaced. After changing a built-in template, run bun run templates:shipped: it records the shipped versions (src/lib/server/templates/shipped.json) from the git history, and a unit test fails until it is run.

The same operations are available in Administration → Configuration → Print templates (/admin/config/templates, administrators): "Customise" copies a built-in folder under its own name, "Duplicate" copies any folder under a new name (lowercase letters, digits, -, _), "Rename" moves a folder of CONFIG_DIR and updates the settings that name it, "Delete" removes it (refused while a setting names it, unless a built-in with the same name takes its place again — "Back to the original"), and the files of assets/ are uploaded or removed there (PNG, JPEG, WebP, GIF, SVG without scripts, WOFF2, WOFF, TTF; 5 MB at most; a file mentioned by the HTML or CSS cannot be removed). Each operation is one change of the configuration history. Every change of the configuration is also refused when it would leave a setting naming a template that does not exist.

Editing from the admin UI: drawing and code

A template of CONFIG_DIR opens in Configuration → Print templates → Edit (/admin/config/templates/<kind>/<name>; a built-in is customised first). Cards and labels open in Design, slips in Code. Both tabs edit the same files: switching from the drawing to the code writes the drawing into the files, switching back reads the files into the drawing, so a change made in one is what the other shows and nothing is dropped. There is no layout file beside the faces (a design.json in a folder is not read, and the next save removes it).

How the drawing reads the files

Large background images are better set in style.css (embedded once per print job) than on the face's root, where the drawing puts them (embedded once per card).

The preview renders the draft from a temporary copy of the folder exactly as printing does (sample patron, sample copy, sample overdue groups); a draft that does not load or render — an unknown field, a broken template.json — shows the problem and cannot be saved. Each save is one change of the configuration history.

What a folder contains

FilePurpose
template.jsonmetadata (see the kinds below); validated at load time
*.htmlMustache templates of each face or cell
style.cssCSS in millimetres; the app scopes it to this template so it cannot affect anything else
assets/images and fonts referenced with {{#asset}}file.png{{/asset}} (allowed: png, jpg, webp, svg, gif, woff, woff2, ttf)

{{#asset}}…{{/asset}} works in the HTML and in style.css; the file is embedded as a data URL, so previews and printouts never depend on the network. Paths with folders are refused.

Mustache in three lines: {{field}} prints a value (HTML-escaped), {{{field}}} prints it as is (only for fields declared raw below), {{#flag}}…{{/flag}} shows the part when the flag is true, {{^flag}}…{{/flag}} when it is false. Unknown fields are an error (the template is refused with a message naming the field), so typos never print an empty box silently.

Cards

Each card is a unit of two faces side by side, front on the left and back on the right, that is cut out and folded on the shared short edge (CR80 faces, 85.6 × 53.98 mm; cards.cardMm in the configuration). The sheet (A4 portrait, cards.sheet.rows units per page, margins, gaps, cut lines, fold and crop marks) is drawn by the app, not by the template: the template only draws the two faces.

template.json:

{
    "name": "default",
    "description": "Tessera come quella attuale",
    "patronTypes": ["student", "teacher"],
    "bleedMm": 0,
    "barcode": { "widthMm": 40, "heightMm": 7, "text": false }
}

Files: front.html, back.html, style.css, assets/logo.png and the background picture. The root element of each face must carry the class face plus front or back; the app sizes .face to the card format, positions inside it are absolute, in mm. The built-in fronts place each element in its own style attribute, as the drawing does, and each face carries its background (colour and picture) on its root element, where the drawing reads it; style.css keeps the rules common to the faces (font, colour).

Three card templates are shipped (step 11.17): default (the library's card: logo, library name, photo, name, class and barcode on the front, a full-page illustration on the back), fronte-retro (text on both sides: the back names the library and the school year and asks whoever finds the card to bring it back) and verticale (a portrait card, cardMm 53.98 × 85.6: a sheet takes the rows that fit the page). Copy one to change it.

Backgrounds. config/cards/backgrounds/ holds pictures ready for the cards (backgrounds.json lists each with its orientation); the designer offers those of the face's orientation ("Choose a background") and copies the chosen one into the template's assets/ as background-<name>.webp, or takes a picture uploaded from the computer ("Upload a picture"). Each shipped picture is the card plus 3 mm of bleed on every side, so the face writes it with background-size: calc(100% + 6mm) calc(100% + 6mm); background-position: center (the trim of the picture on the card, the extra part cut away); any other picture is written with background-size: cover.

Fields

FieldContent
{{library.name}}library.name from the configuration
{{school_year}}e.g. 2026/2027 (from the print date and library.schoolYearStart), or 2026 where library.schoolYearLabel is start
{{issued_on}}print date, YYYY-MM-DD
{{valid_until}}print date + cards.validityMonths (empty when null)
{{photo_url}}the patron photo as a data URL, or a neutral silhouette
{{{barcode_svg}}}the card barcode as inline SVG (raw, triple braces)
{{patron.first_name}}, {{patron.last_name}}names
{{patron.card_number}}the 10-digit number
{{patron.class_label}}the class as school.classLabel writes it: 3 A (a teacher: the name of their group, Docenti)
{{patron.level_label}}, {{patron.level_name}}the year: 3, Terza primaria (empty for teachers)
{{patron.group}}the name of the class or group: A, Docenti
{{school.group_word}}school.groupWord, the word before the class name (Sezione)
{{patron.class_line}}the class line of the card, school.cardClass filled in: Sezione A by default, 1年2組, Klasse 7b (a teacher: the name of their group)
{{words.teacher}}the word for a teacher in the installation's language (Docente, Teacher); the shipped templates print every fixed word from words.*, so they follow the language
{{patron.patron_type}}student or teacher
{{#patron.is_student}}…{{/patron.is_student}}section shown for students only
{{#patron.is_teacher}}…{{/patron.is_teacher}}section shown for teachers only
{{#patron.has_photo}}…{{/patron.has_photo}}section shown when a photo exists ({{^patron.has_photo}} otherwise)

Everything is HTML-escaped except barcode_svg; a surname such as D'Angelo <b> prints literally.

Checking a template

Labels

Book labels (step 3.8) use the same folder convention, but their geometry comes from the adhesive stock, not from the template: labels/stocks/<id>.json describes each sheet product (label width and height in mm, columns, rows, top and left margins, horizontal and vertical gaps, corner radius, paper) and the renderer places the cells absolutely on an A4 page with @page { margin: 0 }. A template only draws the inside of one cell (or of each cell of a unit); it is sized to the stock's labelMm and scoped like a card template.

Three kinds ship, each with a built-in template and its own defaults in labels.<kind> (template and stock):

KindTemplateUnitWhat it is
pair (default)pair-default2 cells stacked vertically: top.html (spine band) + bottom.html (barcode label)the library's current printout: 70 × 96 mm per book on the a4-70x48 sheet, 9 books per page
barcodebarcode-default1 cell, label.htmlthe internal barcode label alone (reprints)
spinespine-default1 cell, label.htmlthe spine band alone (reclassifications); a compact colour-band layout on small stickers

template.json of pair-default:

{
    "name": "pair-default",
    "description": "Coppia impilata: fascetta dorso sopra, etichetta barcode sotto",
    "kind": "pair",
    "unit": { "cells": 2, "stack": "vertical", "files": ["top.html", "bottom.html"] },
    "stocks": ["a4-70x48", "a4-70x24"],
    "variants": {
        "full": { "minCellHeightMm": 40 },
        "compact": { "maxCellHeightMm": 39.9, "files": ["top-compact.html", "bottom-compact.html"] }
    },
    "barcode": { "widthMm": 50, "heightMm": 15, "text": false }
}

Fields

FieldContent
{{item.barcode}}the copy's barcode (2026-00427, or a legacy number)
{{item.call_number}}the copy's call number (segnatura), or the suggested one when empty
{{item.item_type}}the item type code (libro, dvd, …)
{{call.head}}, {{call.rest}}the call number split at its last token: B 4 + DEL, 590 + ROS, B + DEL
{{biblio.title_short}}the title cut to labels.titleMaxChars characters (30)
{{biblio.author_short}}the part of the author before the comma (Rossi)
{{biblio.publication_year}}the year, or empty
{{biblio.series}}the series (Harry Potter), or empty
{{biblio.series_number}}the number in the series (3), or empty
{{classification.kind}}, {{#classification.is_fiction}}…, {{#classification.is_nonfiction}}…fiction / nonfiction and the two sections
{{classification.code}}, {{classification.label}}genre letter or class code, and its name (Animali)
{{classification.image_url}}the pictogram as a data URL (a child class inherits its parent's; a generated disc with the code when no file exists)
{{classification.color}}the node's colour (#e53935) or empty
{{classification.reading_level}}the reading level of the record (its code, PLAN.md Phase 12.6), or of the copy when classification.classPerItem is on
{{library.name}}the library name
{{#labels.monochrome}}…{{/labels.monochrome}}section shown when labels.monochrome is on (black-and-white printer: the compact spine band hatches its colour stripe)
{{{barcode_svg}}}the barcode as inline SVG (raw), sized by the template CSS

Measuring a sheet and checking the output

  1. Write the stock: measure the first label's distance from the top and the left edge of the sheet (marginMm), one label (labelMm), and the space between two labels in each direction (gapMm); count columns and rows. Save it as CONFIG_DIR/labels/stocks/<id>.json (copy an existing file), or enter it in Configuration → Labels → Adhesive label sheets, where a drawing of the A4 page follows the numbers and a changed code is followed by the label jobs and by the stocks lists of the label templates in CONFIG_DIR.
  2. Print the calibration page of the stock on plain paper (Administration → Configuration → Print templates → Labels, or /print/labels/calibration?stock=<id>&kind=pair): every cell is outlined with its index, the units of the kind in a heavier line, plus a 10 cm ruler. Hold it against a real sheet of labels; adjust the margins until the outlines sit on the labels. Print at scale 100 %, never "fit to page".
  3. Preview the template on that stock in the same page (real size or 200 %, fiction or non-fiction sample) and print the sample sheet (/print/labels/<kind>?sample=1&template=<name>&stock=<id>).
  4. Print for real from a print list, the catalogue selection, a record page or the dashboard tile (labels still to print): the dialog asks the kind, the stock and the first free position on a partially used sheet (counted in units: in pairs for the pair kind), opens the print page in a new tab and then offers "Mark as printed", which records barcode_label_printed_at / spine_label_printed_at (a pair sets both).

Pictograms: the library's existing images, named by code (B.gif, 500.gif, MMM.gif, …), are imported with bun run classification:import-images <folder> into CONFIG_DIR/classification/images/ (GIF and other rasters converted to PNG, SVG copied); the command lists the codes still without an image.

Slips

Slips are the printed lists of the circulation desk (step 4.6). Three built-ins ship with the app:

FolderkindPrinted fromContent
overdue-by-classoverdueLoans → Overdue notices → "Print notices" (/print/overdues)the library's current overdue notice: one block per group (class and section, students without a class, Docente), heading <group> - Solleciti biblioteca del <date>, table Nome · Titolo · Giorni ritardo
overdue-patronoverdue-patron"Print a reminder" on the patron page, the check-in desk and the checkout message about overdue books (/print/overdues?patron=<id>)one patron's overdue books
checkout-receiptreceipt"Print receipt" on the checkout desk, shown when circulation.receipts is on (/print/receipt?loans=…)the books of one checkout with the date to return them

template.json:

{ "name": "overdue-by-class", "description": "…", "kind": "overdue", "page": "A4 portrait", "marginMm": 15 }

The file is slip.html, rendered once per block (once per group for the notices). The notices template is chosen by circulation.overdueNotice.template; the other two are replaced by a folder with the same name in CONFIG_DIR/slips/templates/. The shipped slips take their fixed words from the installation's dictionary ({{words.overdues_on}}, {{words.name}}, …), so they print in its language; a copy may keep them or write its own words.

Fields

FieldContent
{{date}}the print date, dd/MM/yyyy
{{library.name}}the library name
{{group.label}}, {{group.kind}}notices only: 3 A, Docente…, and class / noclass / teacher
{{patron.full_name}}, {{patron.first_name}}, {{patron.last_name}}, {{patron.class_label}}the patron (inside rows for the notices, at the top for the reminder and the receipt); class_label is 3 A, empty for teachers
{{#rows}}…{{/rows}}one entry per loan, sorted by surname, first name and due date
{{biblio.title}}, {{biblio.author_short}}inside rows: the title and the part of the author before the comma
{{item.barcode}}inside rows: the copy's barcode
{{due_date}}inside rows: the due date, dd/MM/yyyy
{{days_overdue}}inside rows: calendar days after the due date
{{words.title}}, {{words.code}}, {{words.due_by}}, {{words.due}}, {{words.name}}, {{words.days_overdue}}, {{words.loan_on}}, {{words.overdues_on}}, {{words.to_return}}, {{words.enjoy}}the fixed words of the shipped slips in the installation's language (printing.words.* of the dictionary): Titolo, Codice, Da restituire entro il, Scadenza, Nome, Giorni ritardo, Prestito del 25/09/2026 a, Solleciti biblioteca del 25/09/2026, Libri da restituire alla biblioteca, Buona lettura!; a copy may keep them or write its own words

Preview a notices template without real data with /print/overdues?sample=1&template=<name> (made-up groups; add &date=2026-09-19 for a fixed date).

Indice A–Z