Server rendering
Render a .wf template with JSON on a server — an email, an invoice, a report, an HTML fragment — from the CLI, Rust or Node.
A WebFluent site is static files. When HTML has to be made on a server — an email, an invoice PDF per order, a fragment for an existing page — the same language renders a template with JSON data, from the command line, from Rust or from Node.
A template and its data
A .wf file can be rendered with JSON data — an invoice, an email, a report — from the CLI or from Rust and Node. The data's top-level keys are in scope by name; the static subset of the language applies (no state, handlers, stores, resource, motion).
wf
page Invoice(path: "/", title: "Invoice", description: "An invoice.") {
Heading("Invoice #{number}").h1
Text("Bill to: {customer.name}")
Table {
Table.Head { Table.Row { Table.Cell("Item") Table.Cell("Qty") Table.Cell("Amount") } }
Table.Body {
for item in items {
Table.Row {
Table.Cell(item.name)
Table.Cell("{item.qty}")
Table.Cell(format(item.price * item.qty, .currency))
}
}
}
}
if paid { Badge("Paid").success } else { Badge("Due").warning }
Text("Total: {format(total, .currency)}").bold.lg
}
json
{
"number": "INV-001",
"customer": { "name": "Acme Corp" },
"items": [{ "name": "Widget", "qty": 5, "price": 9.99 }],
"total": 49.95,
"paid": false,
"locale": "en"
}
From the command line
bash
--format is html (a whole document with its CSS), html-fragment (the body only), pdf or slides; -o file writes instead of printing; --theme Name picks one of several theme declarations; --token NAME=VALUE sets a design token over the theme's; --lang ar sets the document's language; the data is read from stdin when --data is omitted. The template may be a directory — every .wf and .wfx under it as one, components shared — with --page Name choosing the page to render. A template that names a component nothing declares, or a flag a component does not take, is refused, as a build would refuse it.
From Rust
The template engine is the webfluent crate. Without the wf command's own dependencies (its argument parser and dev server):
toml
[dependencies]
webfluent = { version = "5", default-features = false }
serde = { version = "1", features = ["derive"] }
Load the templates once, at start-up; render on every request:
rust
use serde::Serialize;
use webfluent::{PdfConfig, Template};
#[derive(Serialize)]
struct Customer { name: String }
#[derive(Serialize)]
struct Item { name: String, qty: u32, price: f64 }
#[derive(Serialize)]
struct Invoice { number: u32, customer: Customer, items: Vec<Item>, total: f64, paid: bool }
// Every .wf and .wfx under the directory, as one template: components,
// themes, types and constants shared; each `page` a document.
let templates = Template::from_dir("templates")?;
let invoice: Invoice = load_invoice();
let html = templates.page("Invoice")?.render_html(&invoice)?; // a whole document
let body = templates.page("Invoice")?.render_html_fragment(&invoice)?; // the markup alone
let pdf: Vec<u8> = templates
.page("Invoice")?
.with_pdf(PdfConfig { page_size: "Letter".into(), ..PdfConfig::default() })
.render_pdf(&invoice)?;
- Loading.
Template::from_str(source),from_file(path)(a.wfxfile is read as its indented layout),from_files(&[..]),from_dir(dir), andfrom_sources(&[(name, source), ..])for templates embedded in the binary withinclude_str!. Each parses and checks once; an error names the file and line.pages()lists the pages,page(name)picks one — without it, every page of the template renders. - Data is anything
serdeserializes: your own#[derive(Serialize)]structs, or aserde_json::Value. Its top-level fields are the names the template reads; aconstand adatafile are in scope too. - Output.
render_htmlis a whole document:<html lang>, a<title>from the page's (its{…}filled from the data), the CSS in a<style>block.render_html_partshands the CSS and the markup back separately, for a server that links its stylesheet and keeps a strict Content-Security-Policy.render_html_fragmentis the markup alone;render_pdfandrender_slidesare PDF bytes, laid out by the paged engine of chapter 33;render_pdf_reportandrender_slides_reportreturn aPdfReport— the bytes, the page count, each page's text in reading order (for a test that holds a document to what it says) and the notes (a font taken from the machine, a character no font had). A template loaded from files reads its fonts fromfonts/beside it, and its pictures from there too. - Settings.
with_theme(name),with_tokens(&[(name, value)]),with_lang("ar")(an RTL language also setsdir="rtl"),with_pdf(PdfConfig { .. })for page size, margins and fonts, andwith_slides(SlidesConfig { .. }). - In a server. A
TemplateisSend + Syncand cheap to clone — the parsed program is shared — so keep it in your framework's state (anArc, or a clone per handler) and render from any thread. A render takes tens of microseconds. - Untrusted data. Every value the data supplies is escaped as text, and a URL from the data that a browser would run —
javascript:,data:— is dropped fromhrefandsrc, as the browser build drops it.
cargo run --example invoice in the repository renders a directory of templates to HTML, a fragment and a PDF.
From Node
npm install webfluent — a small wrapper around wf render. It needs wf itself installed: it looks on PATH, in ~/.webfluent/bin and ~/.cargo/bin, or at WF_BIN. Its version is the compiler's.
js
const { Template } = require("webfluent");
const templates = Template.fromDir("templates"); // or fromFile, fromString
const invoice = templates.page("Invoice");
app.get("/invoices/:id", async (req, res) => {
const data = await loadInvoice(req.params.id);
res.send(await invoice.renderHtmlAsync(data));
});
app.get("/invoices/:id.pdf", async (req, res) => {
res.type("application/pdf").send(await invoice.renderPdfAsync(await loadInvoice(req.params.id)));
});
Every render has a synchronous form (renderHtml, renderHtmlFragment, renderPdf, renderSlides) and an …Async one that does not block the event loop — use those in a server. withTheme, withTokens and withLang set what the Rust API sets. A render runs wf once, a few milliseconds; the data goes over stdin, so nothing is written to disk.
A template may declare types and a theme like any file; data files are read relative to the template. format and ago speak the data's locale when it has one, else English.
What a template may use
The static subset of the language: every layout, typography and data-display element, for loops, if/else, string interpolation, format and ago, style { } blocks, flags, themes, types, consts and components. A state is its first value and a derived value what that works out to — a page may work out its totals once, in order, and show them anywhere — and a value the data hands over wins over either. Not: effect, handlers, navigation, stores, animations, resource — there is no browser to run them in.
A template may read private env names (Environments): it runs on your server, where a secret stays secret.
Uses
- Email: render with
--format html; inline the styles with your mailer if it needs them. - Invoices and reports:
--format pdf, one per record, from a job queue. - Fragments:
--format html-fragmentfor a piece of an existing page — an HTMX response, a CMS block.
Checked by the test suite
Every code block in the guide is parsed, checked and type-checked on each release.