Skip to main content

Migrate from PDFMonkey

PDFMonkey code templates are HTML with Liquid, a CSS or SCSS stylesheet, sample data and page settings. Formfeed runs Liquid too, so an import converts the few places where PDFMonkey's Ruby Liquid and its own filters differ, and reports everything it cannot convert with the line.

Import templates​

In the app: Templates → Import → PDFMonkey.

  1. Enter a secret API key from PDFMonkey (My account → API). It is sent once to read the templates and not stored.
  2. Enter your workspace ID and choose List templates, or paste template IDs (one per line).
  3. If your templates load_snippets, paste the snippets (see Snippets).
  4. Analyse converts every selected template and shows one report per template: errors that stop the render, things to check, and what the importer changed.
  5. Create drafts makes a Liquid template per report with the mapped settings and the sample data as its default data set. Its first version is a draft; publish it in the editor.

The app imports the published version of each PDFMonkey template, or its draft when it was never published.

With the CLI, in a template project (the key comes from --key or PDFMONKEY_API_KEY):

export PDFMONKEY_API_KEY=sk_…
formfeed import pdfmonkey --workspace-id 6f1c… # every code template of the workspace
formfeed import pdfmonkey --template 3b9e… 71aa… # or selected templates
formfeed import pdfmonkey --template 3b9e… --snippet address=snippets/address.liquid
formfeed import pdfmonkey --template 3b9e… --draft # the unpublished draft instead

Each template becomes a folder under templates/, and the CLI prints the same report as the app.

Only code templates can be imported. Builder templates (PDFMonkey's visual builder) cannot, because their format is not published; --workspace-id skips them, and a builder template named with --template is an error in the report. Recreate those in the editor.

Dates​

PDFMonkey formats dates with Ruby's strftime (%d.%m.%Y), Formfeed's date helper with date-fns tokens (dd.MM.yyyy). The importer rewrites every format, including literal words (%d de %B becomes dd' de 'MMMM), and "today" | date becomes "now" | date. An in_time_zone directly before date becomes the timezone argument of date (first line before, second after):

{{ order.created_at | in_time_zone: "Europe/Berlin" | date: "%H:%M" }}
{{ order.created_at | date: "HH:mm", "Europe/Berlin" }}

An in_time_zone anywhere else is an error in the report. The report also lists strftime directives without a date-fns equivalent (for example %j or %s); they are kept literally.

Numbers​

with_delimiter becomes number with the locale whose separators match: delimiter . and separator , render with de-DE, a space and , with fr-FR, ' and . with de-CH, the default (, and .) with en-US. Any other pair is kept and reported, and fails the render. precision: 2 becomes two decimals. strip_insignificant_zeros has no equivalent and is reported; number then shows up to three decimals.

PDFMonkey's Ruby Liquid divides two integers without remainder (7 | divided_by: 2 is 3); Formfeed returns 3.5. The importer does not change the arithmetic but lists every integer division with its line, so you can add | floor where the template relies on it.

Filters​

PDFMonkeyFormfeed
slice_by: 3chunk: 3 (renamed by the importer)
sum, push, json, where_expthe same
with_delimiter, in_time_zonesee Numbers and Dates
entitiesnot needed, removed: headers and footers render UTF-8 directly
to_sentenceerror; use join: ", ", or array_to_sentence_string for "a, b, and c"
format (printf)error; use number: 2 or round: 2 (printf padding has no equivalent)
ensure_protocol, parse_jsonerror; store full URLs (or prepend: "https:") and send JSON as objects

Liquid prints values as they are, in Formfeed as in PDFMonkey: it does not escape HTML in the output. Where a template prints text your users typed, keep or add | escape ({{ customer.name | escape }}), so that a < in a name cannot change the document.

Filters without an equivalent are errors in the report: Formfeed's Liquid runs with strict filters, so an unknown filter stops the render instead of printing nothing. Ruby also accepts a comma after the filter name (| with_delimiter, precision: 2); the importer writes the colon LiquidJS needs.

Snippets​

PDFMonkey's API does not return snippet code. Paste each snippet the templates load in the Snippets field, a line --- name followed by its code:

--- address
<address>{{ who.name }}<br>{{ who.street }}</address>
--- legal-footer
<small>Fennlor Studio GmbH · Musterstraße 1</small>

{% include 'address', who: customer %} and inline {% partial %} blocks are inlined into the template, with the parameters as assigns. An include whose snippet was not provided is an error.

Styles​

The stylesheet may be SCSS. The app compiles it with Sass; the CLI does when sass is installed in the project (npm install -D sass). Without a compiler, top-level $variables are substituted and // comments removed, and anything that needs Sass (@mixin and @include, @extend, @use and @forward, @if, @each and the other control rules, functions) is reported. Nested rules work either way: Chromium renders CSS nesting.

Settings​

Paper format (A0 to A6, Letter, custom sizes in millimetres), orientation and margins are mapped to the template's settings; any other format becomes A4 with a warning, and empty PDFMonkey margins are 10 mm. The simple header and footer (left, center, right with [page] and [topage]) become an HTML header with page numbers in 10px type; the advanced HTML header and footer move to the Header and Footer tabs with the same Liquid conversions as the body. Image templates become image templates at 1200 × 630 (PDFMonkey sets the size per request), with a transparent background where the template had one; change the size under Page setup in the editor.

If the template loaded Tailwind with a <script> or stylesheet tag, the tag is removed and Tailwind mode switched on: Formfeed compiles the classes the template uses.

JavaScript​

PDFMonkey can expose the payload to scripts as $docPayload. Formfeed does not; render the values into the markup, or write them into a script tag:

<script>const order = {{ order | json }};</script>

For QR codes, the qrcode helper replaces JavaScript libraries: <img src="{{ payment.url | qrcode }}" alt="QR code: {{ payment.url }}">. The report points at a QR library it finds in the template.

Paged.js​

Formfeed paginates with Chromium and shows a Paged.js preview in the editor. Templates that loaded Paged.js themselves keep the script; check running headers and margin boxes in the true render.

Forms​

PDF form field markers ([% data_form_field … %]) are not supported and stay as text.

Sample data​

PDFMonkey's sample data becomes the template's default data set; completion, the preview and the report's check for missing variables use it. Your API requests keep sending the payload as before, now as the data of a render.

Switch the API​

Replace the call to PDFMonkey's documents endpoint with a render. The import creates drafts, and the API renders the published version, so publish each template first. The request names the template by its slug (PDFMonkey's template identifier in lowercase with dashes):

curl https://api-eu.formfeed.dev/v1/renders \
-H "Authorization: Bearer ff_live_…" \
-H "Content-Type: application/json" \
-d '{ "template": "order-confirmation", "data": { "order": { "id": "A-1" } } }'

mode: "async" plus a webhook replaces PDFMonkey's pending documents and webhook events; see Async and batches.