Zum Inhalt springen

HandlebarsPDF

Kurszertifikat in Handlebars

Ein Zertifikat im Querformat mit QR-Code zur Echtheitsprüfung, einzeln als PNG gerendert oder für einen ganzen Kurs als Batch, der in einem Zip endet. Handlebars nimmt Optionen als Hash-Argumente: {{qrcode verify_url size=96}}.

Kostenloser Plan, keine Kreditkarte. Sie erhalten eine eigene Kopie im Editor, veröffentlicht und bereit für die API.

Gerendertes Ergebnis

Gerendertes Ergebnis: Kurszertifikat in Handlebars
PDF · 1 Seite · 49 KB ↓ herunterladen

Was dieses Beispiel zeigt

  • A4 quer ohne Ränder: Der Rahmen ist schlichtes CSS auf einer Box, die genau so hoch ist wie die Seite (210mm), und reicht deshalb bis zum Rand.
  • {{qrcode verify_url size=96}} macht aus dem Prüf-Link einen QR-Code; {{upper id}} gibt die Zertifikatsnummer als CERT-8841 aus.
  • head.html lädt Inter und Playfair Display von Google Fonts; der Render-Worker holt sie einmal und liefert sie danach aus seinem Cache.
  • output: "png" rendert dieselbe Vorlage als Bild zum Teilen, und settings.image gibt dem Bild die Größe der Seite, 1123 × 794 Pixel: Sonst entsteht ein Bild aus einem Fenster von 1200 × 630, das Unterschrift und QR-Code abschneidet. Ohne output kommt das hier gezeigte PDF zurück.
  • Ein ganzer Kurs läuft über renders.batch mit zip: true: ein Job, am Ende ein Archiv.

Verwendete Helfer

Mehr zu diesem Anwendungsfall: Zertifikate per API erzeugen →

Vorlagenordner

templates/certificate/template.htmlHandlebars
<main class="certificate">
  <p class="eyebrow">Certificate of completion</p>
  <h1>{{name}}</h1>
  <p class="lead">
    completed <strong>{{course}}</strong> ({{hours}} hours)
    on {{date completed_on "d MMMM yyyy"}}.
  </p>

  <footer>
    <div class="signature">
      <span class="line"></span>
      {{instructor}}, instructor
    </div>
    <figure class="verify">
      <img src="{{qrcode verify_url size=96}}" alt="Verification code">
      <figcaption>{{upper id}}</figcaption>
    </figure>
  </footer>
</main>
templates/certificate/style.cssCSS
.certificate {
  height: 210mm;
  padding: 22mm 28mm;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  border: 6mm solid #312e81;
  outline: 1px solid #a5b4fc;
  outline-offset: -10mm;
  background: linear-gradient(135deg, #ffffff 60%, #eef2ff);
  font-family: 'Inter', sans-serif;
  color: #0f172a;
}
.eyebrow { margin: 0; letter-spacing: 0.25em; text-transform: uppercase; color: #4f46e5; font-weight: 600; }
h1 { margin: 12mm 0 4mm; font-family: 'Playfair Display', serif; font-size: 48pt; }
.lead { max-width: 190mm; font-size: 16pt; line-height: 1.5; color: #334155; }
footer { margin-top: auto; display: flex; justify-content: space-between; align-items: end; }
.signature { font-size: 11pt; color: #334155; }
.signature .line { display: block; width: 70mm; border-top: 1px solid #0f172a; margin-bottom: 2mm; }
.verify { margin: 0; text-align: center; font-size: 8pt; color: #64748b; }
templates/certificate/head.htmlHTML
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Playfair+Display:wght@700&display=swap">
templates/certificate/settings.jsonJSON
{
  "pdf": {
    "metadata": {
      "title": "Certificate · {{name}}"
    }
  },
  "paper": {
    "format": "A4",
    "landscape": true
  },
  "margin": {
    "top": "0",
    "right": "0",
    "bottom": "0",
    "left": "0"
  },
  "locale": "en-GB"
}
templates/certificate/data/default.jsonJSON
{
  "name": "Jane Doe",
  "course": "Analytical Engines 101",
  "completed_on": "2026-09-10",
  "hours": 16,
  "instructor": "John Doe",
  "id": "cert-8841",
  "verify_url": "https://academy.example/verify/cert-8841"
}

Ü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.

render.shShell
jq '{template: "certificate", data: ., output: "png", settings: {image: {width: 1123, height: 794}}}' data.json \
  | curl https://api-eu.formfeed.dev/v1/renders \
      -H "Authorization: Bearer ff_test_..." \
      -H "Content-Type: application/json" \
      --data-binary @-
render.tsTypeScript
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: 'certificate',
  data,
  output: 'png',
  settings: {
    image: { width: 1123, height: 794 },
  },
});
console.log(render.status, render.download_url);
render.pyPython
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="certificate",
    data=data,
    output="png",
    settings={
        "image": {"width": 1123, "height": 794},
    },
)
print(render.status, render.download_url)
render.phpPHP
<?php
$body = [
    'template' => 'certificate',
    'data' => json_decode(file_get_contents('data.json'), true),
    'output' => 'png',
    'settings' => [
        'image' => ['width' => 1123, 'height' => 794],
    ],
];

$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;
main.goGo
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": "certificate",
		"data":     json.RawMessage(data),
		"output":   "png",
		"settings": map[string]any{
			"image": map[string]any{"width": 1123, "height": 794},
		},
	})

	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)
}
Program.csC#
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;

var body = new
{
    template = "certificate",
    data = JsonDocument.Parse(File.ReadAllText("data.json")).RootElement,
    output = "png",
    settings = new {
        image = new { width = 1123, height = 794 },
    },
};

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")}");
Render.javaJava
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": "certificate",
          "output": "png",
          "settings": {"image":{"width":1123,"height":794}},
          "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 …
  }
}
render.rbRuby
require "json"
require "net/http"

body = {
  template: "certificate",
  data: JSON.parse(File.read("data.json")),
  output: "png",
  settings: {
    image: { width: 1123, height: 794 },
  },
}

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"]

Weitere Aufrufe

TypeScript
// The whole course at once: one job, one zip at the end
const job = await formfeed.renders.batch({
  template: 'certificate',
  zip: true,
  items: attendees.map((attendee) => ({ data: attendee })),
});

const done = await formfeed.jobs.waitFor(job.id);
console.log(`${done.succeeded}/${done.total} rendered`, done.zip_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.