LiquidPDF
Händler-Preisliste mit EAN-Barcodes
Ein Produkt-Feed kommt unsortiert an. groupBy bildet einen Abschnitt je Kategorie, sortBy sortiert jeden nach Name, min und max ergeben die Preisspanne in der Überschrift, und jede Zeile trägt ihren EAN-13-Barcode. Eine Kategorie wird nie über eine Seite getrennt.
Kostenloser Plan, keine Kreditkarte. Sie erhalten eine eigene Kopie im Editor, veröffentlicht und bereit für die API.
Gerendertes Ergebnis
Was dieses Beispiel zeigt
products | groupBy: 'category'bildet einen Abschnitt je Kategorie in der Reihenfolge des Feeds, undsortBy: 'name'sortiert jeden Abschnitt.min: 'price'undmax: 'price'schreiben die Preisspanne jeder Kategorie in ihre Überschrift: Schreibtischleuchten kosten£49.50bis£118.00.- Jede Zeile trägt ihren EAN-13-Code als Barcode,
barcode: type: 'ean13', mit den Ziffern in den üblichen Gruppen. Die Beispielcodes beginnen mit 20, einem Präfix, das GS1 für Nummern freihält, die nur innerhalb eines Unternehmens gelten. multiply: product.packergibt neben dem Stückpreis den Packungspreis: zehn Leuchtmittel zu£3.20machen£32.00.- Eine Kategorie hat
break-inside: avoid, und die Fußzeile wiederholt auf jeder Seite das Gültigkeitsdatum aus den Daten.
Verwendete Helfer
Vorlagenordner
<header>
<div>
<p class="brand">{{ company.name }}</p>
<h1>{{ list.title }}</h1>
<p class="muted">{{ list.season }} · {{ products | size }} products</p>
</div>
<p class="contact">{{ company.street }}<br>{{ company.city }}<br>{{ company.email }}</p>
</header>
{% assign categories = products | groupBy: 'category' %}
{% for category in categories %}
{% assign lowest = category.items | min: 'price' %}
{% assign highest = category.items | max: 'price' %}
{% assign sorted = category.items | sortBy: 'name' %}
<section class="category">
<h2>{{ category.key }} <span>{{ category.items | length }} products · {{ lowest | money }} to {{ highest | money }}</span></h2>
<table>
<thead><tr><th>Item</th><th>EAN</th><th class="num">Pack</th><th class="num">Unit</th><th class="num">Per pack</th></tr></thead>
<tbody>
{% for product in sorted %}
<tr>
<td><strong>{{ product.name }}</strong><br><span class="muted">{{ product.sku }}</span></td>
<td><img class="ean" src="{{ product.ean | barcode: type: 'ean13', height: 7 }}" alt="EAN {{ product.ean }}"></td>
<td class="num">{{ product.pack }}</td>
<td class="num">{{ product.price | money }}</td>
<td class="num">{{ product.price | multiply: product.pack | money }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
{% endfor %}
<ul class="terms">
{% for term in list.terms %}
<li>{{ term }}</li>
{% endfor %}
</ul>body { font-family: 'Inter', sans-serif; font-size: 9pt; color: #1e293b; }
header { display: flex; justify-content: space-between; align-items: flex-end; padding-bottom: 6mm; border-bottom: 2px solid #0f766e; }
.brand { margin: 0; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: #0f766e; }
h1 { margin: 1mm 0; font-size: 22pt; color: #0f172a; }
.muted { margin: 0; color: #64748b; }
.contact { margin: 0; text-align: right; font-size: 8pt; line-height: 1.5; color: #475569; }
.category { margin-top: 8mm; break-inside: avoid; }
h2 { margin: 0 0 2mm; font-size: 12pt; color: #0f172a; }
h2 span { margin-left: 2mm; font-size: 8.5pt; font-weight: 400; color: #64748b; }
table { width: 100%; border-collapse: collapse; table-layout: fixed; }
th { padding: 1.5mm; border-bottom: 1.5px solid #0f172a; text-align: left; font-size: 7pt; letter-spacing: 0.05em; text-transform: uppercase; color: #64748b; }
th:nth-child(1) { width: 40%; }
th:nth-child(2) { width: 27%; }
th:nth-child(3) { width: 9%; }
td { padding: 1.6mm 1.5mm; border-bottom: 1px solid #e2e8f0; vertical-align: middle; }
.num { text-align: right !important; white-space: nowrap; }
.ean { display: block; height: 10mm; }
.terms { margin: 8mm 0 0; padding-left: 4mm; font-size: 8pt; line-height: 1.6; color: #475569; }<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap"><div style="width:100%;display:flex;justify-content:space-between;font-size:7.5px;color:#64748b">
<span>Trade prices in GBP excluding VAT, valid until {{ list.valid_until | date: 'd MMMM yyyy' }}</span>
<span>Page <span class="pageNumber"></span> of <span class="totalPages"></span></span>
</div>{
"pdf": {
"metadata": {
"title": "{{ list.title }}"
}
},
"paper": {
"format": "A4"
},
"margin": {
"top": "18mm",
"right": "16mm",
"bottom": "20mm",
"left": "16mm"
},
"locale": "en-GB",
"timezone": "Europe/London",
"currency": "GBP",
"footer": {
"height": "12mm"
}
}{
"company": {
"name": "Fennlor Studio Ltd",
"street": "1 Example Street",
"city": "Exampletown",
"email": "trade@fennlor.example"
},
"list": {
"title": "Trade price list",
"season": "Autumn/Winter 2026",
"valid_until": "2027-03-31",
"terms": [
"Minimum order £250; free delivery within the UK from £750.",
"Lamps ship in packs of two, shades in packs of four and bulbs in packs of ten.",
"Prices exclude VAT and may change with thirty days’ notice."
]
},
"products": [
{
"sku": "FF-1030",
"name": "Task lamp Line, black",
"category": "Desk lamps",
"ean": "2000000010304",
"price": 92,
"pack": 2
},
{
"sku": "FF-1001",
"name": "Desk lamp Arc, black",
"category": "Desk lamps",
"ean": "2000000010014",
"price": 64,
"pack": 2
},
{
"sku": "FF-1020",
"name": "Reading lamp Fold, brass",
"category": "Desk lamps",
"ean": "2000000010205",
"price": 118,
"pack": 2
},
{
"sku": "FF-1002",
"name": "Desk lamp Arc, white",
"category": "Desk lamps",
"ean": "2000000010021",
"price": 64,
"pack": 2
},
{
"sku": "FF-1010",
"name": "Clamp lamp Studio, graphite",
"category": "Desk lamps",
"ean": "2000000010106",
"price": 49.5,
"pack": 2
},
{
"sku": "FF-2010",
"name": "Pendant Cone, smoked glass",
"category": "Pendant lights",
"ean": "2000000020105",
"price": 138,
"pack": 1
},
{
"sku": "FF-2001",
"name": "Pendant Dome 30 cm, sand",
"category": "Pendant lights",
"ean": "2000000020013",
"price": 76,
"pack": 1
},
{
"sku": "FF-2020",
"name": "Cluster pendant, three lights",
"category": "Pendant lights",
"ean": "2000000020204",
"price": 229,
"pack": 1
},
{
"sku": "FF-2002",
"name": "Pendant Dome 45 cm, sand",
"category": "Pendant lights",
"ean": "2000000020020",
"price": 104,
"pack": 1
},
{
"sku": "FF-3002",
"name": "Linen shade 35 cm, natural",
"category": "Shades",
"ean": "2000000030029",
"price": 16.8,
"pack": 4
},
{
"sku": "FF-3020",
"name": "Rattan shade 30 cm",
"category": "Shades",
"ean": "2000000030203",
"price": 21.5,
"pack": 4
},
{
"sku": "FF-3001",
"name": "Linen shade 25 cm, natural",
"category": "Shades",
"ean": "2000000030012",
"price": 12.4,
"pack": 4
},
{
"sku": "FF-3010",
"name": "Paper shade 40 cm, white",
"category": "Shades",
"ean": "2000000030104",
"price": 9.9,
"pack": 4
},
{
"sku": "FF-4010",
"name": "LED filament G95, 470 lm",
"category": "Bulbs",
"ean": "2000000040103",
"price": 5.6,
"pack": 10
},
{
"sku": "FF-4001",
"name": "LED bulb E27, 806 lm, warm white",
"category": "Bulbs",
"ean": "2000000040011",
"price": 3.2,
"pack": 10
},
{
"sku": "FF-4030",
"name": "LED spot GU10, 345 lm",
"category": "Bulbs",
"ean": "2000000040301",
"price": 2.4,
"pack": 10
},
{
"sku": "FF-4002",
"name": "LED bulb E27, 1521 lm, warm white",
"category": "Bulbs",
"ean": "2000000040028",
"price": 4.9,
"pack": 10
},
{
"sku": "FF-4020",
"name": "LED candle E14, 470 lm",
"category": "Bulbs",
"ean": "2000000040202",
"price": 2.9,
"pack": 10
}
]
}Über die API rendern
Speichern Sie die Beispieldaten als data.json und senden Sie sie mit dem Namen der Vorlage. TypeScript und Python nutzen die SDKs; jede andere Sprache schickt dasselbe JSON mit ihrer Standardbibliothek an die REST-API. Renderings mit einem Schlüssel, der mit ff_test_ beginnt, werden nicht gezählt; im Free-Tarif tragen sie ein kleines Wasserzeichen.
jq '{template: "price-list", data: ., filename: "trade-price-list.pdf"}' data.json \
| curl https://api-eu.formfeed.dev/v1/renders \
-H "Authorization: Bearer ff_test_..." \
-H "Content-Type: application/json" \
--data-binary @-import { Formfeed } from '@formfeed/sdk';
import data from './data.json' with { type: 'json' };
const formfeed = new Formfeed({ apiKey: process.env.FORMFEED_API_KEY! });
const render = await formfeed.renders.create({
template: 'price-list',
data,
filename: 'trade-price-list.pdf',
});
console.log(render.status, render.download_url);import json
import os
from formfeed import Formfeed
formfeed = Formfeed(api_key=os.environ["FORMFEED_API_KEY"])
with open("data.json", encoding="utf-8") as file:
data = json.load(file)
render = formfeed.renders.create(
template="price-list",
data=data,
filename="trade-price-list.pdf",
)
print(render.status, render.download_url)<?php
$body = [
'template' => 'price-list',
'data' => json_decode(file_get_contents('data.json'), true),
'filename' => 'trade-price-list.pdf',
];
$curl = curl_init('https://api-eu.formfeed.dev/v1/renders');
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('FORMFEED_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($body),
]);
$render = json_decode(curl_exec($curl), true);
echo $render['status'], ' ', $render['download_url'], PHP_EOL;package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
data, err := os.ReadFile("data.json")
if err != nil {
panic(err)
}
body, _ := json.Marshal(map[string]any{
"template": "price-list",
"data": json.RawMessage(data),
"filename": "trade-price-list.pdf",
})
request, _ := http.NewRequest("POST", "https://api-eu.formfeed.dev/v1/renders", bytes.NewReader(body))
request.Header.Set("Authorization", "Bearer "+os.Getenv("FORMFEED_API_KEY"))
request.Header.Set("Content-Type", "application/json")
response, err := http.DefaultClient.Do(request)
if err != nil {
panic(err)
}
defer response.Body.Close()
var render struct {
Status string `json:"status"`
DownloadURL string `json:"download_url"`
}
json.NewDecoder(response.Body).Decode(&render)
fmt.Println(render.Status, render.DownloadURL)
}using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
var body = new
{
template = "price-list",
data = JsonDocument.Parse(File.ReadAllText("data.json")).RootElement,
filename = "trade-price-list.pdf",
};
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("FORMFEED_API_KEY"));
var response = await http.PostAsJsonAsync("https://api-eu.formfeed.dev/v1/renders", body);
var render = await response.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine($"{render.GetProperty("status")} {render.GetProperty("download_url")}");import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
public class Render {
public static void main(String[] args) throws Exception {
String data = Files.readString(Path.of("data.json"));
String body = """
{
"template": "price-list",
"filename": "trade-price-list.pdf",
"data": %s
}
""".formatted(data);
HttpRequest request = HttpRequest.newBuilder(URI.create("https://api-eu.formfeed.dev/v1/renders"))
.header("Authorization", "Bearer " + System.getenv("FORMFEED_API_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body()); // status, download_url, page_count, units …
}
}require "json"
require "net/http"
body = {
template: "price-list",
data: JSON.parse(File.read("data.json")),
filename: "trade-price-list.pdf",
}
response = Net::HTTP.post(
URI("https://api-eu.formfeed.dev/v1/renders"),
JSON.generate(body),
"Authorization" => "Bearer #{ENV.fetch("FORMFEED_API_KEY")}",
"Content-Type" => "application/json"
)
render = JSON.parse(response.body)
puts render["status"], render["download_url"]Heute mit dem Rendern beginnen
Der kostenlose Plan enthält 100 Einheiten im Monat und kostenlose Testrenderings. Wechseln Sie den Plan, wenn Sie mehr brauchen.

