Zum Inhalt springen

HandlebarsPDF

Lebenslauf mit Seitenleiste und Kompetenzbewertung

Ein einseitiger Lebenslauf in zwei Spalten bis an den Rand. Die Vorlage sortiert die Stationen nach Beginn, die Daten dürfen also in beliebiger Reihenfolge kommen, schreibt „present“ für eine Stelle ohne Enddatum und zeichnet jede Bewertung mit range und lt als fünf Punkte.

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

Gerendertes Ergebnis

Gerendertes Ergebnis: Lebenslauf mit Seitenleiste und Kompetenzbewertung
PDF · 1 Seite · 100 KB ↓ herunterladen

Was dieses Beispiel zeigt

  • Die Daten führen die Stationen in beliebiger Reihenfolge; sortBy experience "start" "desc" stellt die aktuelle nach vorn, und derselbe Helfer sortiert die Abschlüsse.
  • Eine Station ohne Enddatum zeigt „present“: {{#if end}} wählt zwischen dem formatierten Datum und dem Wort.
  • {{#each (range 0 5)}} zeichnet fünf Punkte je Kompetenz, und (lt this ../level) füllt so viele, wie die Stufe angibt; ../ erreicht die Kompetenz aus der inneren Schleife heraus.
  • join stack " · " macht aus der Liste der Technologien jeder Station eine Zeile.
  • Ohne Ränder reicht die Seitenleiste bis an die Papierkante, und min-height: 297mm streckt sie über die ganze Seite.

Verwendete Helfer

Vorlagenordner

templates/resume/template.htmlHandlebars
<main class="cv">
  <aside>
    <h1>{{person.name}}</h1>
    <p class="headline">{{person.headline}}</p>

    <h2>Contact</h2>
    <ul class="plain">
      <li>{{person.email}}</li>
      <li>{{person.phone}}</li>
      <li>{{person.location}}</li>
      <li>{{person.web}}</li>
    </ul>

    <h2>Skills</h2>
    <ul class="skills">
      {{#each skills}}
      <li>
        <span>{{name}}</span>
        <span class="dots">{{#each (range 0 5)}}<i class="{{#if (lt this ../level)}}on{{/if}}"></i>{{/each}}</span>
      </li>
      {{/each}}
    </ul>

    <h2>Languages</h2>
    <ul class="plain">
      {{#each languages}}
      <li><strong>{{name}}</strong> · {{level}}</li>
      {{/each}}
    </ul>
  </aside>

  <div class="content">
    <h2>Profile</h2>
    <p>{{person.summary}}</p>

    <h2>Experience</h2>
    {{#each (sortBy experience "start" "desc")}}
    <article>
      <h3>{{role}}</h3>
      <p class="where">{{company}} · {{location}} · {{date start "MMM yyyy"}} – {{#if end}}{{date end "MMM yyyy"}}{{else}}present{{/if}}</p>
      <ul>
        {{#each highlights}}
        <li>{{this}}</li>
        {{/each}}
      </ul>
      <p class="stack">{{join stack " · "}}</p>
    </article>
    {{/each}}

    <h2>Education</h2>
    {{#each (sortBy education "start" "desc")}}
    <article>
      <h3>{{degree}}</h3>
      <p class="where">{{school}} · {{date start "yyyy"}} – {{date end "yyyy"}}</p>
    </article>
    {{/each}}
  </div>
</main>
templates/resume/style.cssCSS
body { margin: 0; font-family: 'Inter', sans-serif; font-size: 9pt; line-height: 1.5; color: #1e293b; }
.cv { display: grid; grid-template-columns: 68mm 1fr; min-height: 297mm; }
aside { padding: 16mm 9mm; background: #1e1b4b; color: #e0e7ff; }
aside h1 { margin: 0; font-size: 22pt; line-height: 1.1; color: #fff; }
.headline { margin: 2mm 0 0; font-size: 10.5pt; color: #a5b4fc; }
aside h2 { margin: 10mm 0 3mm; font-size: 7.5pt; letter-spacing: 0.12em; text-transform: uppercase; color: #a5b4fc; }
ul { margin: 0; padding: 0; list-style: none; }
.plain li { margin-bottom: 1.2mm; overflow-wrap: anywhere; }
.skills li { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.6mm; }
.dots { display: flex; gap: 1.2mm; }
.dots i { width: 2.4mm; height: 2.4mm; border-radius: 50%; background: #3730a3; }
.dots i.on { background: #a5b4fc; }
.content { padding: 16mm 14mm 16mm 12mm; }
.content h2 { margin: 0 0 3mm; padding-bottom: 1.5mm; border-bottom: 1px solid #e2e8f0; font-size: 8pt; letter-spacing: 0.12em; text-transform: uppercase; color: #4f46e5; }
.content > p { margin: 0 0 8mm; }
article { margin-bottom: 6mm; break-inside: avoid; }
h3 { margin: 0; font-size: 10.5pt; color: #0f172a; }
.where { margin: 0.5mm 0 1.5mm; color: #64748b; }
article ul { padding-left: 4mm; list-style: disc; }
article li { margin-bottom: 0.8mm; }
.stack { margin: 1.5mm 0 0; font-size: 8pt; color: #4f46e5; }
templates/resume/head.htmlHTML
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap">
templates/resume/settings.jsonJSON
{
  "pdf": {
    "metadata": {
      "title": "{{person.name}}"
    }
  },
  "paper": {
    "format": "A4"
  },
  "margin": {
    "top": "0",
    "right": "0",
    "bottom": "0",
    "left": "0"
  },
  "locale": "en-GB"
}
templates/resume/data/default.jsonJSON
{
  "person": {
    "name": "Jane Doe",
    "headline": "Senior backend engineer",
    "email": "jane@doe.example",
    "phone": "+44 0000 000000",
    "location": "Exampletown, United Kingdom",
    "web": "janedoe.example",
    "summary": "Backend engineer with ten years of building order, billing and document systems. I like boring infrastructure, small pull requests and documents that come out the same on every run."
  },
  "experience": [
    {
      "role": "Software engineer",
      "company": "Fennlor Studio GmbH",
      "location": "Musterstadt, Germany",
      "start": "2017-09-01",
      "end": "2021-02-28",
      "highlights": [
        "Built the order and invoicing service that replaced a nightly spreadsheet export.",
        "Introduced contract tests between the shop and the warehouse system."
      ],
      "stack": [
        "Python",
        "Django",
        "PostgreSQL",
        "RabbitMQ"
      ]
    },
    {
      "role": "Senior backend engineer",
      "company": "Olvarest Ltd",
      "location": "Exampletown",
      "start": "2021-03-01",
      "end": null,
      "highlights": [
        "Lead the billing platform team of five, from usage metering to the monthly invoice run.",
        "Moved invoice and contract PDFs from a home-grown renderer to templates in Git with snapshot tests.",
        "Cut the month-end batch from six hours to forty minutes."
      ],
      "stack": [
        "TypeScript",
        "Node.js",
        "PostgreSQL",
        "Kubernetes"
      ]
    },
    {
      "role": "Junior developer",
      "company": "Olvarest GmbH",
      "location": "Beispielstadt, Germany",
      "start": "2016-10-01",
      "end": "2017-08-31",
      "highlights": [
        "Maintained the customer portal and its reporting exports."
      ],
      "stack": [
        "PHP",
        "MySQL"
      ]
    }
  ],
  "education": [
    {
      "degree": "BSc Computer Science",
      "school": "University of Exampletown",
      "start": "2012-10-01",
      "end": "2015-07-31"
    },
    {
      "degree": "MSc Software Engineering",
      "school": "University of Exampletown",
      "start": "2015-10-01",
      "end": "2016-09-30"
    }
  ],
  "skills": [
    {
      "name": "TypeScript",
      "level": 5
    },
    {
      "name": "PostgreSQL",
      "level": 5
    },
    {
      "name": "Python",
      "level": 4
    },
    {
      "name": "Kubernetes",
      "level": 3
    },
    {
      "name": "Go",
      "level": 2
    }
  ],
  "languages": [
    {
      "name": "English",
      "level": "native"
    },
    {
      "name": "German",
      "level": "fluent (C1)"
    },
    {
      "name": "French",
      "level": "basic (A2)"
    }
  ]
}

Ü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: "resume", data: ., filename: "cv-jane-doe.pdf"}' 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: 'resume',
  data,
  filename: 'cv-jane-doe.pdf',
});
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="resume",
    data=data,
    filename="cv-jane-doe.pdf",
)
print(render.status, render.download_url)
render.phpPHP
<?php
$body = [
    'template' => 'resume',
    'data' => json_decode(file_get_contents('data.json'), true),
    'filename' => 'cv-jane-doe.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;
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": "resume",
		"data":     json.RawMessage(data),
		"filename": "cv-jane-doe.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)
}
Program.csC#
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;

var body = new
{
    template = "resume",
    data = JsonDocument.Parse(File.ReadAllText("data.json")).RootElement,
    filename = "cv-jane-doe.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")}");
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": "resume",
          "filename": "cv-jane-doe.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 …
  }
}
render.rbRuby
require "json"
require "net/http"

body = {
  template: "resume",
  data: JSON.parse(File.read("data.json")),
  filename: "cv-jane-doe.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.