Jinja2Bild
Kennzahlen-Karte mit Diagramm, 1200×675
Eine Karte im Format 16:9 für Slack, LinkedIn oder X aus dreißig Tagen Bestellungen: Summen, die Veränderung zum Vormonat, der stärkste Tag über sortBy und first und ein Chart.js-Balkendiagramm für dunklen Hintergrund, alles in der Vorlage berechnet.
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
- Dreißig Zeilen mit Tagesbestellungen gehen hinein; die Vorlage summiert sie zu
2,524Bestellungen und£214,791.00und teilt für den durchschnittlichen Auftragswert,£85.10. - Die Veränderung zu August wird in der Vorlage berechnet und gerundet,
▲ 13.1%, und wird mit Pfeil nach unten rot, wenn der Monat schwächer war. days | sortBy('orders', 'desc') | firstfindet den stärksten Tag,Sun 27 Sepmit 126 Bestellungen.- Das Diagramm ist Chart.js, dessen
optionsdurchgereicht werden, Farben für Achsenbeschriftung und Raster auf der dunklen Karte, undrange(1, (days | length) + 1)nummeriert die Tage. access: "public"liefert einen Link, den Slack für einen Bildblock im Team-Kanal abrufen kann.
Verwendete Helfer
Vorlagenordner
{% set orders = days | sum('orders') %}
{% set revenue = days | sum('revenue') %}
{% set change = ((orders - previous.orders) / previous.orders * 100) | round(1) %}
{% set busiest = days | sortBy('orders', 'desc') | first %}
<main class="card">
<header>
<p class="brand">{{ shop.name }}</p>
<h1>{{ period | date('MMMM yyyy') }} in numbers</h1>
</header>
<section class="kpis">
<div>
<span>Orders</span>
<strong>{{ orders | number(0) }}</strong>
<em class="{{ 'up' if change >= 0 else 'down' }}">{{ '▲' if change >= 0 else '▼' }} {{ change if change >= 0 else -change }}% on {{ previous.label }}</em>
</div>
<div>
<span>Revenue</span>
<strong>{{ revenue | money }}</strong>
<em>{{ (revenue / orders) | money }} per order</em>
</div>
<div>
<span>Busiest day</span>
<strong>{{ busiest.date | date('EEE d MMM') }}</strong>
<em>{{ busiest.orders }} orders</em>
</div>
</section>
{{ chart({
type: 'bar',
labels: range(1, (days | length) + 1),
values: days | pluck('orders'),
color: '#818cf8',
options: {
scales: {
x: { ticks: { color: '#94a3b8', font: { size: 13 } }, grid: { display: false } },
y: { ticks: { color: '#94a3b8', font: { size: 13 } }, grid: { color: 'rgba(148, 163, 184, 0.18)' } }
}
}
}, { width: 1040, height: 290 }) }}
</main>body { margin: 0; }
.card {
box-sizing: border-box;
width: 1200px;
height: 675px;
padding: 52px 80px 40px;
background: #0f172a;
font-family: 'Inter', sans-serif;
color: #f8fafc;
}
header { display: flex; justify-content: space-between; align-items: baseline; }
.brand { margin: 0; font-size: 20px; font-weight: 600; letter-spacing: 0.2em; text-transform: uppercase; color: #a5b4fc; }
h1 { margin: 0; font-size: 36px; font-weight: 700; }
.kpis { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; margin: 34px 0 30px; }
.kpis div { padding: 20px 24px; border-radius: 16px; background: #1e293b; }
.kpis span { display: block; font-size: 17px; color: #94a3b8; }
.kpis strong { display: block; margin: 6px 0 4px; font-size: 40px; font-weight: 700; }
.kpis em { font-size: 17px; font-style: normal; color: #94a3b8; }
.kpis em.up { color: #4ade80; }
.kpis em.down { color: #f87171; }
.ff-chart { display: block; }<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap">{
"image": {
"width": 1200,
"height": 675,
"deviceScaleFactor": 1,
"format": "png"
},
"locale": "en-GB",
"timezone": "Europe/London",
"currency": "GBP"
}{
"shop": {
"name": "Fennlor Studio shop"
},
"period": "2026-09-01",
"previous": {
"label": "August",
"orders": 2231
},
"days": [
{
"date": "2026-09-01",
"orders": 64,
"revenue": 5376
},
{
"date": "2026-09-02",
"orders": 71,
"revenue": 5989
},
{
"date": "2026-09-03",
"orders": 69,
"revenue": 5846
},
{
"date": "2026-09-04",
"orders": 83,
"revenue": 7047
},
{
"date": "2026-09-05",
"orders": 104,
"revenue": 8836
},
{
"date": "2026-09-06",
"orders": 112,
"revenue": 9533
},
{
"date": "2026-09-07",
"orders": 58,
"revenue": 5022
},
{
"date": "2026-09-08",
"orders": 66,
"revenue": 5719
},
{
"date": "2026-09-09",
"orders": 73,
"revenue": 6332
},
{
"date": "2026-09-10",
"orders": 70,
"revenue": 5880
},
{
"date": "2026-09-11",
"orders": 88,
"revenue": 7417
},
{
"date": "2026-09-12",
"orders": 109,
"revenue": 9206
},
{
"date": "2026-09-13",
"orders": 117,
"revenue": 9903
},
{
"date": "2026-09-14",
"orders": 61,
"revenue": 5224
},
{
"date": "2026-09-15",
"orders": 68,
"revenue": 5837
},
{
"date": "2026-09-16",
"orders": 75,
"revenue": 6450
},
{
"date": "2026-09-17",
"orders": 72,
"revenue": 6223
},
{
"date": "2026-09-18",
"orders": 91,
"revenue": 7844
},
{
"date": "2026-09-19",
"orders": 113,
"revenue": 9492
},
{
"date": "2026-09-20",
"orders": 121,
"revenue": 10189
},
{
"date": "2026-09-21",
"orders": 63,
"revenue": 5342
},
{
"date": "2026-09-22",
"orders": 70,
"revenue": 5955
},
{
"date": "2026-09-23",
"orders": 77,
"revenue": 6568
},
{
"date": "2026-09-24",
"orders": 74,
"revenue": 6341
},
{
"date": "2026-09-25",
"orders": 94,
"revenue": 8046
},
{
"date": "2026-09-26",
"orders": 118,
"revenue": 10087
},
{
"date": "2026-09-27",
"orders": 126,
"revenue": 10784
},
{
"date": "2026-09-28",
"orders": 66,
"revenue": 5544
},
{
"date": "2026-09-29",
"orders": 72,
"revenue": 6073
},
{
"date": "2026-09-30",
"orders": 79,
"revenue": 6686
}
]
}Ü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: "metrics-card", data: ., output: "png", access: "public"}' 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: 'metrics-card',
data,
output: 'png',
access: 'public',
});
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="metrics-card",
data=data,
output="png",
access="public",
)
print(render.status, render.download_url)<?php
$body = [
'template' => 'metrics-card',
'data' => json_decode(file_get_contents('data.json'), true),
'output' => 'png',
'access' => 'public',
];
$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": "metrics-card",
"data": json.RawMessage(data),
"output": "png",
"access": "public",
})
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 = "metrics-card",
data = JsonDocument.Parse(File.ReadAllText("data.json")).RootElement,
output = "png",
access = "public",
};
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": "metrics-card",
"output": "png",
"access": "public",
"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: "metrics-card",
data: JSON.parse(File.read("data.json")),
output: "png",
access: "public",
}
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.
