Anwendungsfall
Open-Graph-Bild-API
Formfeed ist eine API, die aus dem Titel einer Seite ihr Vorschaubild macht: eine Karte pro Artikel, Produkt oder Profil, die ein echter Browser aus einer Vorlage in der Größe zeichnet, die Sie festlegen. Gestalten Sie die Karte auf einer Arbeitsfläche oder schreiben Sie sie in HTML und CSS. Ihr Build oder Ihr Backend sendet Titel und Autor als JSON, wenn die Seite veröffentlicht wird, und die Antwort ist die Adresse des Bildes.
Kostenloser Plan mit 100 Einheiten im Monat, keine Kreditkarte
Arbeitsfläche oder Code
Text, Bilder, Formen und QR-Codes im visuellen Editor (Beta) auf einer Arbeitsfläche platzieren und an Ihre Daten binden, oder HTML und CSS selbst schreiben. Für die API ist beides dieselbe Art von Vorlage.
Titel, die passen
Ein Titel unbekannter Länge bekommt die größte Schriftgröße, mit der er in seine Box passt, bis zu einem Minimum, das Sie festlegen: auf der Arbeitsfläche oder im Code mit data-ff-fit. Auf der Arbeitsfläche prüft ein Klick jeden Datensatz auf Text, der trotzdem abgeschnitten würde.
Die Größe, die Sie festlegen
Eine Bildvorlage rendert in dem Fenster, das ihre Einstellungen nennen, 1200 × 630, solange Sie nichts anderes setzen, und deviceScaleFactor vervielfacht die Pixel der Datei für scharfen Text. PNG, JPG oder WebP.
Ihre Schriften, Ihr Logo
Google Fonts laden über ein link-Tag, eigene Schriftdateien ab dem Starter-Plan, und das Logo kommt aus der Dateibibliothek. Dahinter stehen Noto-Schriften und farbige Emoji; ein Titel in einem anderen Schriftsystem wird also trotzdem gedruckt.
Eine Adresse ohne Signatur
Mit "access": "public" ist die Antwort eine Adresse auf dem CDN, die keine Signatur braucht und so lange funktioniert, wie die Datei aufbewahrt wird; darüber entscheidet Ihr Plan.
Tailwind
Ist tailwind eingeschaltet, wird eine Karte mit Utility-Klassen geschrieben, und beim Rendern werden genau die Klassen kompiliert, die sie verwendet.
Ein Open-Graph-Bild aus JSON erzeugen
Eine Karte ist die Vorlage plus die Daten einer Seite. Speichern Sie die Daten als data.json und senden Sie sie mit dem Namen der Vorlage; die Antwort enthält die Adresse des Bildes, und access: "public" macht daraus eine, die keine Signatur braucht. Die Vorlage hat 1200 × 630 Pixel und ist in Liquid mit Tailwind-Klassen geschrieben. TypeScript und Python nutzen die SDKs, jede andere Sprache schickt dasselbe JSON mit ihrer Standardbibliothek. Ein Schlüssel, der mit ff_test_ beginnt, rendert, ohne dass gezählt wird.
{
"post": {
"category": "Engineering",
"title": "Templates belong in Git: reviewing invoice layouts in pull requests",
"published": "2026-09-10",
"minutes": 6,
"author": {
"name": "Formfeed",
"avatar": "https://formfeed.dev/favicon.svg"
}
}
}jq '{template: "og-image", data: ., 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: 'og-image',
data,
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="og-image",
data=data,
access="public",
)
print(render.status, render.download_url)<?php
$body = [
'template' => 'og-image',
'data' => json_decode(file_get_contents('data.json'), true),
'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": "og-image",
"data": json.RawMessage(data),
"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 = "og-image",
data = JsonDocument.Parse(File.ReadAllText("data.json")).RootElement,
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": "og-image",
"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: "og-image",
data: JSON.parse(File.read("data.json")),
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"]Vollständige Anfrage- und Antwortformate in der Doku. Vollständiges Vorlagenbeispiel →
Das kommt heraus
Ein echtes Rendering der Beispielvorlage für diesen Anwendungsfall, direkt aus dem Render-Worker. Ein Klick öffnet die Datei.
Kostenloser Plan, keine Kreditkarte. Sie erhalten eine eigene Kopie im Editor, veröffentlicht und bereit für die API.
Von der Antwort ins Meta-Tag
Die API ist ein POST mit Ihrem Schlüssel; das Bild entsteht also nicht, wenn ein soziales Netzwerk Ihre Seite abruft: Ein Scraper hat keinen Schlüssel und würde nicht warten. Das Bild entsteht, wenn die Seite veröffentlicht wird. Ihr Build, ein Hook Ihres CMS oder Ihr Backend sendet den Titel, nimmt download_url aus der Antwort und schreibt sie in den Kopf der Seite.
<meta property="og:image" content="https://cdn.formfeed.dev/…">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">Eine identische Anfrage mit einem Live-Schlüssel wird 24 Stunden lang aus dem gespeicherten Bild beantwortet und kostet nichts; ein Build, der zweimal am Tag läuft, rendert jede Karte also einmal. Danach würde eine unveränderte Karte neu gerendert: Bewahren Sie die Adresse bei der Seite auf und fordern Sie ein neues Bild an, wenn sich der Titel ändert. Ein Bild, das mit einem Test-Schlüssel entstanden ist, wird nach spätestens einem Tag gelöscht; das Bild, das eine Seite behält, kommt also von einem Live-Schlüssel.
Größe, Schärfe und Format
Die übliche Größe einer Link-Vorschau ist 1200 × 630 Pixel, und das ist das Fenster, das eine Bildvorlage hat, solange ihre Einstellungen nichts anderes sagen. deviceScaleFactor vervielfacht die Pixel der Datei: Bei 2, dem Standard, ist dieselbe Karte eine Datei mit 2400 × 1260 Pixeln, die auf hochauflösenden Bildschirmen scharf bleibt; das Beispiel setzt 1 für eine kleinere Datei. Quadratische und hochformatige Bilder folgen demselben Prinzip mit anderen Zahlen, 1080 × 1080 für eine Zitatkarte und 1080 × 1920 für eine Story, und eine Anfrage kann sie in settings.image für ein einzelnes Rendering setzen.
{
"image": {
"width": 1200,
"height": 630,
"deviceScaleFactor": 1,
"format": "png"
},
"tailwind": true,
"locale": "en-GB"
}output wählt das Format pro Anfrage: png, jpg oder webp, die letzten beiden mit image.quality. image.transparent lässt den Hintergrund eines PNG oder WebP weg.
Ein Titel beliebiger Länge
Was eine Karte nicht wissen kann, ist die Länge ihres Titels. Es gibt zwei Antworten. truncate schneidet den Text nach einer Zahl von Zeichen ab, wie es das Beispiel bei 70 tut. Oder die Box behält ihre Größe und der Text gibt nach: Kennzeichnen Sie die Box mit data-ff-fit="shrink", und ihr Text bekommt die größte Schriftgröße, mit der er hineinpasst, zwischen seiner CSS-Größe und dem Minimum, das Sie nennen.
<div class="title" data-ff-fit="shrink" data-ff-fit-min="36">
<h1>{{ post.title }}</h1>
</div>Die Box braucht eine feste Breite und Höhe, und eingepasst wird, nachdem die Schriften geladen sind und bevor das Bild aufgenommen wird. Auf der Arbeitsfläche des visuellen Editors ist dasselbe eine Einstellung der Textebene, und „Jeden Datensatz prüfen“ rendert dort jeden Datensatz der Vorlage und nennt jeden Text, der trotzdem nicht in seine Box passt.
Auf einer Arbeitsfläche gestaltet, ohne Code
Eine Karte ist Layoutarbeit, und wer sie macht, schreibt vielleicht kein CSS. Der visuelle Editor (Beta, in jedem Plan) beginnt mit einer Artikelkarte in 1200 × 630, einer Zitatkarte, einer Event-Story oder einer leeren Arbeitsfläche. Eine Textebene enthält Felder Ihrer Daten, eine Bildebene nimmt ihr Bild aus der Dateibibliothek, von einer Webadresse, aus einem Feld der Daten oder vom Markenlogo, und eine Ebene lässt sich nur dann zeigen, wenn ein Feld einen Wert hat. Was die Arbeitsfläche schreibt, ist eine gewöhnliche Vorlage; die Anfrage oben bleibt also dieselbe.
Alle Seiten einer Website auf einmal
Ändert sich das Design der Karte, braucht jede Seite ein neues Bild. Ein Batch nimmt bis zu 1.000 Renderings in einer Anfrage und arbeitet sie im Hintergrund ab. Jeder Eintrag bringt seine eigenen Daten mit, einen filename und in meta die ID der Seite, zu der er gehört; sie kommt mit seinem Ergebnis zurück. render.completed erreicht Ihren Webhook für jedes Bild, oder Ihr Code liest den fertigen Job mit GET /v1/jobs/{id}. Mehr unter Asynchron und Batch.
Was ein Bild kostet
Ein Bild ist eine Einheit, gleich wie groß es ist und wie es gestaltet wurde. Der Free-Plan hat 100 Einheiten im Monat und verlangt keine Kreditkarte. Starter hat 3.000 Einheiten für 19 € im Monat, rund 0,6 Cent pro Bild. Renderings mit einem Test-Schlüssel kosten nichts, solange Sie bauen; im Free-Plan tragen sie ein kleines Wasserzeichen. Ein fehlgeschlagenes Rendering wird nie gezählt. Alle Pläne stehen auf der Preisseite.
Was bei Ihnen bleibt
Formfeed erzeugt das Bild und behält es für die Aufbewahrung Ihres Plans: einen Tag im Free-Plan, 30 Tage bei Starter und 365 Tage bei Pro; Business setzt keine Grenze. Danach wird die Datei gelöscht, und ihre Adresse antwortet mit einem Fehler. Ein Vorschaubild muss so lange antworten, wie die Seite geteilt wird; lassen Sie die einzige Kopie also nicht bei Formfeed: Laden Sie die Datei herunter, wenn sie gerendert ist, und liefern Sie sie von Ihrer eigenen Website aus. Ab dem Pro-Plan geht von jedem Bild eine Kopie in Ihren eigenen S3- oder R2-Bucket, und ist dort eine öffentliche Basis-URL eingetragen, hat jede Kopie eine Adresse auf Ihrer eigenen Domain.
Es gibt keine Adresse, die ein Bild aus den Wörtern in ihrem Query-String zeichnet, und Formfeed liest Ihre Seiten nicht und schreibt nicht deren Meta-Tags. Ein Bild entsteht durch eine Anfrage mit Ihrem Schlüssel und ist von da an eine Datei.
FAQ
- Lässt sich die Karte ohne Code gestalten?
- Ja. Der visuelle Editor (Beta, in jedem Plan) beginnt mit einer Artikelkarte, einer Zitatkarte, einer Event-Story oder einer leeren Arbeitsfläche und schreibt eine gewöhnliche Vorlage; der API-Aufruf bleibt also derselbe.
- Welche Größe sollte ein Open-Graph-Bild haben?
- 1200 × 630 Pixel sind die übliche Größe einer Link-Vorschau und das, was eine Bildvorlage rendert, solange Sie nichts ändern. Mit
deviceScaleFactor2 hat die Datei 2400 × 1260 Pixel und bleibt auf hochauflösenden Bildschirmen scharf; mit 1 ist sie kleiner. - Wie lange funktioniert die Adresse eines Bildes?
- Eine öffentliche Adresse funktioniert so lange, wie die Datei aufbewahrt wird: einen Tag im Free-Plan, 30 Tage bei Starter, 365 Tage bei Pro, ohne Grenze bei Business. Für eine Seite, die länger geteilt wird, speichern Sie das Bild selbst.
- Kostet eine unveränderte Seite bei jedem Build eine Einheit?
- Innerhalb von 24 Stunden nicht: Eine identische Anfrage mit einem Live-Schlüssel wird kostenlos aus dem gespeicherten Bild beantwortet. Danach wird neu gerendert; bewahren Sie die Adresse also bei der Seite auf und rendern Sie, wenn sich der Titel ändert.
- Lässt sich statt einer Vorlage eine Webseite aufnehmen?
- Ja. Mit
urlund"output": "png"wird eine öffentliche Seite in dem Fenster aufgenommen, das Sie setzen; URL zu PDF sagt, was die Seite erfüllen muss. Eine Vorlage gibt Ihnen die Kontrolle über Schriften und Layout. - Funktionieren Emoji und Titel in anderen Schriftsystemen?
- Ja. Hinter den Schriften, die Sie nennen, hat der Renderer die Noto-Schriften für viele Schriftsysteme, auch Chinesisch, Japanisch und Koreanisch, und farbige Emoji. Die Vorschau in Ihrem Browser zeichnet ein Emoji vielleicht anders; ein echtes Rendering im Editor zeigt das Bild so, wie die API es erzeugt.
