Skip to main content

Shared partials

A partial is a fragment of a template that another template includes by name: a letterhead, an address block, a terms paragraph. Formfeed has two kinds.

Template partialsShared partials
Belongs toOne template versionThe organisation, usable from every workspace
Created withA project's partials/ folder and formfeed templates push (Partials)The Partials page in the app, formfeed partials push or PUT /partials/{name}
When it changesWith the next template version you pushLive, from the next render of every template that includes it
Good forStructure inside one templateWhat every document of the organisation must show the same way

Include either kind with the syntax of the template language:

Jinja2LiquidHandlebars
{% include "letterhead" %}{% include "letterhead" %}{{> letterhead}}

Shared partials see the same data as the template, including brand, and may include other shared partials (up to 8 levels deep). In Liquid, use include: a partial pulled in with {% render "letterhead" %} sees only brand and the values you pass to it ({% render "letterhead", customer: customer %}).

Formfeed finds the shared partials a template needs by reading the names it includes, so write the name literally:

  • Jinja2: include, import, from and extends with a quoted name ({% extends "base-letter" %} works too).
  • Liquid: include and render with a quoted name.
  • Handlebars: {{> letterhead}} and the partial block {{#> letterhead}}…{{/letterhead}}, with the name unquoted.

A name that comes from a variable ({% include partial_name %}) or, in Handlebars, a quoted name ({{> "letterhead"}}) never reaches a shared partial: the render fails with a template error as if the partial did not exist.

Create one in the app​

Open Partials in the organisation navigation and choose New partial. A partial has:

  • a name: lowercase letters, digits, dashes and underscores, starting and ending with a letter or digit, at most 64 characters, unique in the organisation;
  • an engine: Jinja2, Liquid or Handlebars. A shared partial is only found by templates of the same engine;
  • an optional description (up to 300 characters) and the source (up to 256 KB), edited in the same code editor as templates.

Every row shows the partial's include line; click it to copy the line to your clipboard (the edit dialog has the same button). The table also shows each partial's version, engine, the templates it is used by (and how many of them are published) and when it was last updated. A template counts as using a partial when its published version or its newest draft includes the name and has no template partial of the same name; shared partials that include it are listed too. Filter by engine to see what templates of one language can include, or search by name.

Owners, admins and editors create, edit, rename and delete shared partials in the app (Edit or rename in a row's menu); viewers see them. Through the API, any key with template:write may write them. An organisation holds up to 200, on every plan.

Changes are live​

When you save a change to a partial that templates use, the dialog first lists those templates, marked Published or Draft, and asks you to confirm: the next render of each of them uses the new version, published templates included, without a new template version.

  • Renaming a partial that is in use breaks the includes that use the old name; those templates fail to render until you change the include.
  • Deleting a partial that is in use names the templates that would fail, and asks you to type the partial's name to confirm.
  • API renders read shared partials through a cache of up to 60 seconds, so a change made in the app reaches them within a minute. A change through the API or the CLI clears that cache, so it usually applies at once.
  • A partial's version goes up when its source or engine changes; a new name or description keeps the version. Every render records the versions of the shared partials it used as shared_partials ({ "letterhead": 4 }) in its reproducibility record, which the render's details on the Renders page of the app show under Reproducibility (the API's render object does not carry it), and a render is only deduplicated when those versions are unchanged.

How a name is resolved​

For each include, Formfeed looks in this order:

  1. the template partials stored with the template version, by exact name;
  2. the shared partials of the template's engine, matched case-insensitively and with one file extension ignored (.html, .htm, .j2, .njk, .jinja, .liquid, .hbs, .handlebars). {% include "Letterhead.html" %} therefore finds the shared partial letterhead.

A template partial with the same name overrides the shared one. That is useful for a deliberate exception, and a trap when it is an old copy: the editor shows an information message in the Problems list: The partial "letterhead" stored with this template hides the shared partial "letterhead". To use the shared one, run formfeed partials pull letterhead in the project (which records the name as shared) and push the template again: the new version no longer carries its own copy.

Only the shared partials a template actually includes (directly or through other partials, in the body, header or footer) travel with a render; together they may be up to 2 MB, beyond which the render answers 400 invalid_request naming the partials. Word and PowerPoint templates get the brand kit but no shared partials. When the partials cannot be read, the request fails with 500 worker_exhausted before a render is recorded, and costs nothing (see How renders use them).

Work with shared partials in a project​

Shared partials live in the same partials/ folder as template partials, as <name>.html. The CLI tells them apart by .formfeed/state.json, where formfeed partials pull and push record each shared partial with its engine and remote version.

npx formfeed partials list # name, engine, version, local state (behind, modified)
npx formfeed partials pull # all shared partials, or name some: pull letterhead
# edit partials/letterhead.html, preview with formfeed dev
npx formfeed partials push letterhead # creates version 4
  • New partials: formfeed partials push <name> creates the partial from partials/<name>.html. The engine is the project's engine unless you pass --engine jinja2|liquid|handlebars. Without names, push sends every recorded shared partial that changed; --dry-run shows what would be sent.
  • Conflicts: push sends the version you last pulled as base_version. If someone changed the partial since, the push stops with exit code 2 (… changed remotely since v3; run formfeed partials pull letterhead and merge, or push with --force). Pull, merge your change and push again, or use --force to overwrite. Pushing a name that already exists remotely but was never pulled is refused the same way.
  • Template pushes: formfeed templates push leaves partials recorded as shared out of the template version, because they resolve live, and does not report them as missing. An include whose file is neither a template partial nor a recorded shared partial still stops the push.
  • Local rendering: formfeed dev, render, test and validate resolve includes from the partials/ folder, so pull first to preview with the current shared partials.

In CI, run formfeed partials push before formfeed templates push --publish when a change touches both, so templates never render against a partial that does not exist yet.

From code​

The Brand and partials API lists, reads, writes and deletes shared partials with template:read and template:write; the TypeScript and Python SDKs wrap it as partials.list(), get, put and delete. A partial is addressed by its name, which the API stores in lowercase:

curl -X PUT https://api-eu.formfeed.dev/v1/partials/letterhead \
-H "Authorization: Bearer $FORMFEED_API_KEY" -H "Content-Type: application/json" \
-d '{ "engine": "jinja2", "source": "<header>{{ brand.name }}</header>", "base_version": 3 }'

engine and source are required, description is optional, and base_version makes the write conditional: when the partial changed since that version, the API answers 409 conflict and writes nothing (see Conflicts). Renaming is done in the app only.