Skip to content

Updated

PDF templates belong in your repository

Documents are code that produces legal artefacts. Here is how the Formfeed dev kit lets a template be reviewed, tested and released like the rest of your software.

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.

Philipp Staudt

Start rendering today

The free plan includes 100 units a month and free test renders. Change plans when you need more.