A template that renders invoices is code. It has branches, it has edge cases, and when it breaks, a customer receives something wrong on paper. Yet template editors usually sit outside every practice we apply to the rest of a codebase: no review, no tests, no history you can bisect.
Formfeed keeps the browser editor, because the person who writes the wording should not need a checkout. It also lets the same template be a folder of files.
The folder
templates/invoice-de/
template.json name, kind, engine
template.html the body
style.css
header.html, footer.html
settings.json paper, margins, locale, currency
schema.json what the data must contain
data/
default.json sample data sets
many-lines.json
formfeed templates pull writes it, push sends it back. The files map one to one to the tabs in the
web editor, so a designer can work in the browser while a developer works in the repository, and
neither of them wins by accident: push sends the checksum it started from, and the API answers with
a conflict if someone saved a newer version in between, in the editor or with another push.
The loop
formfeed dev invoice-de # hot-reload preview on localhost, no account needed
formfeed validate # diagnostics, exit code 1 on errors
formfeed render invoice-de --data data/many-lines.json --out out.pdf
The preview runs the same engine as the render servers, so what paginates locally paginates in production. Only the final Chromium render happens in the cloud, and with a test key it is free.
The tests
import { loadTemplate } from '@formfeed/testing';
const tpl = await loadTemplate('templates/invoice-de');
test('renders every data set without errors', async () => {
await expect(tpl).toRenderWithoutErrors(await tpl.data('many-lines'));
await expect(tpl).toUseOnlyKnownVariables(await tpl.data('many-lines'));
});
toUseOnlyKnownVariables is the matcher that earns its keep. A renamed field in an upstream service
does not throw, it renders an empty space where the customer number used to be, and nobody notices
until a customer calls. The matcher fails the build instead.
In CI
Validation and tests run offline and previews on a test key, so a pull request costs nothing. The CI guide has the workflow, with or without our GitHub Action. The publish step runs on the default branch with a live key and a message that carries the commit. The result is a template history where every published version has an author, a diff and a reason.
None of this is required. Plenty of teams will only ever use the editor, and that is a complete product. But when a document ends up in an audit, being able to show which version produced it, who approved that version and what changed is worth the folder.