HIH Exposé-Generator – automatisches PDF-Layout aus WordPress mit JetEngine

Für die Hamburger Immobiliengesellschaft HIH habe ich ein WordPress-Plugin entwickelt, das Immobilien-Exposés komplett automatisch aus JetEngine-Daten generiert: Adresse, Preise, Flächen, Bilder, Grundrisse, Energieausweis, Lagekarte, Ansprechpartner – ein Klick im Frontend, und das fertige PDF im Corporate Design liegt zum Download bereit.

Die alte Welt sah anders aus: Exposés wurden in einem Adobe-Layout-Tool von Hand zusammengebaut. Daten aus dem CRM kopiert, Bilder einzeln importiert, Schriften mühsam ausgerichtet, am Ende als PDF exportiert. Heute pflegen die Makler Adresse, Preis und Flächen einmalig in WordPress – und der Generator baut das fertige PDF in unter zehn Sekunden zusammen, mit pixelgenauem Corporate Design.

In diesem Artikel zeige ich, wie das technisch funktioniert: das Datenmodell mit JetEngine als Single Source of Truth, die PDF-Pipeline mit Dompdf, FPDI und PHP-GD, das Hash-basierte Cache-Versioning – und der vermutlich kniffligste Teil: ein Multiply-Blending-Filter, der in Dompdf nicht möglich ist und deshalb pixelweise in PHP-GD vorberechnet wird.

Wie wurden Exposés vorher erstellt?

Die alte Welt war ein klassischer Layout-Workflow in einem Adobe-Programm. Für jedes Inserat wurde ein Master-Dokument geöffnet, Daten aus dem CRM rüberkopiert, Bilder einzeln eingefügt, Tabellen für Flächen und Preise von Hand gepflegt, Lageplan und Energieausweis als externe PDFs eingefügt – am Ende ein PDF-Export, manuelle Ablage und Versand per E-Mail.

Das funktioniert. Aber jeder Schritt ist eine Quelle für Fehler oder Inkonsistenzen: Tippfehler bei der Adresse, falsche Quadratmeterzahl, das Logo aus dem letzten Quartal, ein veralteter Disclaimer, eine vergessene Bildlegende. Außerdem braucht es Layout-Skills – nicht jeder Makler im Team kennt sich mit Adobe-Tools aus, und Vertretungen werden zum Engpass.

Der eigentliche Engpass

Es geht nicht nur um Zeit. Layout-Software setzt Skills voraus, blockiert eine Vorlage immer einen Bearbeiter und macht Layout-Konsistenz von menschlicher Disziplin abhängig. Der größte Gewinn der Automatisierung ist nicht die Sekundenanzeige – es ist, dass jeder im Team mit zwei Klicks ein Exposé im sauberen Corporate Design produzieren kann.

Was musste das Plugin leisten?

Das Plugin sollte den kompletten Workflow vom Dateneingabe-Backend bis zum Download-Link ersetzen. Konkret:

Anforderungen im Überblick
  • Daten aus JetEngine als zentrale Quelle, keine doppelte Pflege
  • PDF-Layout im HIH-Corporate-Design (Farben, Schriften, Bildstile)
  • Mehrseitiges A4-Querformat: Titelseite, Beschreibung, Galerie, Flächen, Grundrisse, Lage, Ansprechpartner, Disclaimer
  • Externe PDFs (Grundrisse, Energieausweise) müssen in das Exposé eingebettet werden
  • Automatische Aktualisierung bei Datenänderung – ohne Cron, ohne manuellen Trigger
  • Sicherer Download per signierter URL und Rate-Limit
  • Concurrent-Safe: zwei gleichzeitige Anfragen dürfen sich nicht in die Quere kommen
  • Standard-Hosting reicht aus – kein Headless Browser, kein externer Service

Das letzte Kriterium war wichtig: keine Headless-Chrome-Lösung, keine externe API. Die Erzeugung läuft komplett auf dem WordPress-Server, mit reinem PHP. Das vereinfacht Hosting, Backup, Wartung und Datenschutz – die Daten verlassen den eigenen Server nicht.

JetEngine als Single Source of Truth

Die Datenebene baut komplett auf JetEngine auf. Zwei Custom Post Types tragen die Inhalte: inserate für die Immobilien selbst, ansprechpartner für die zuständigen Makler. Verknüpft sind beide über JetEngine-Relations, die intern in einer eigenen Datenbank-Tabelle (wp_jet_rel_default) gespeichert werden.

Pro Inserat werden etwa 35 Meta-Felder gepflegt: Adresse, Lage, Lagebeschreibung, Provisionsstatus, minimale und maximale Fläche, Preise pro Quadratmeter, Nebenkosten, Objektbeschreibung, Entfernungen zu ÖPNV und Autobahn, Ausstattung, Highlights, Featured Image, Bildergalerie, Parkplatz-Daten in mehreren Varianten (Außen, Tiefgarage, E-Lade-Station mit jeweils Anzahl und Preis), Energieausweis und Datum.

Das Bemerkenswerte: Die Makler arbeiten ausschließlich im Standard-WordPress-Backend. Keine eigene Eingabemaske, keine separate Anwendung. Das senkt die Lernkurve und macht den Datenpflege-Workflow für Vertretungen oder neue Mitarbeiter trivial – wer ein Inserat bearbeiten kann, kann auch das Exposé generieren.

Datensammlung: 35 Felder plus Property Table

Manche Immobilien sind keine Einzelobjekte, sondern komplette Gebäude oder Quartiere mit mehreren Einheiten – Bürogebäude mit fünfzehn Mietflächen, ein Wohnkomplex mit unterschiedlichen Wohnungstypen. Für diese Fälle ist eine flache Feldstruktur nicht genug.

Die Lösung: ein zweites Plugin pflegt eine strukturierte Property-Tabelle pro Inserat – Buildings, in jedem Building mehrere Units, pro Unit Etage, Fläche, Mietstatus, Preis ab, Nebenkosten, Verfügbarkeitsdatum, Beschreibung und Anhänge wie Grundriss, Musterplanung und Energieausweis. Diese Daten werden als JSON in einem einzigen Meta-Feld (cptsrs_site_plan_json) abgelegt und vom Generator beim Sammeln aufgelöst.

Die Datensammlung selbst sitzt in data-collector.php und liefert ein einziges, sauberes Daten-Array zurück, das das Template dann nur noch ausgeben muss. Trennung von Datenmodell und Darstellung, klassisches Pattern. Eine Besonderheit ist der eigene Dezimal-Parser: Quadratmeter und Preise kommen mal als 1.234,56, mal als 1234.56 – der Parser erkennt heuristisch, welches Trennzeichen gemeint ist, und produziert immer den korrekten float.

PDF-Pipeline: Dompdf, FPDI und PHP-GD im Zusammenspiel

Für die PDF-Erzeugung kommen drei Libraries zusammen, jede für einen klar abgegrenzten Job:

  • Dompdf 2.x rendert HTML+CSS zu PDF. Das ist der Hauptweg.
  • FPDI mergt externe PDFs (Grundrisse, Energieausweise) in das Endergebnis ein. Dompdf kann das nicht – FPDI schon.
  • PHP-GD übernimmt die Bildvorverarbeitung, bevor die Bilder ins HTML eingebettet werden. Multiply-Blending, Cover-Cropping, Filter-Overlays für Featured Images.

Das Layout selbst ist ein klassisches Templating-Setup: expose-template.php ist ein PHP-File mit Output-Buffering, das Daten aus dem Collector holt und HTML produziert. Ein einziges Master-Template für alle Exposés – das ist der Wendepunkt. Ein Master-Template heißt: konsistentes Corporate Design über alle Exposés ohne Disziplin-Aufwand. Niemand kann mehr versehentlich das Logo aus dem alten Quartal benutzen, weil es nur eine Stelle gibt, an der das Logo herkommt: aus dem Plugin-Settings-Bereich.

Mehrseitiges PDF-Exposé im Corporate Design – Titelseite, Galerie und Flächen-Tabelle
Mehrseitiges A4-Querformat – Titelseite, Beschreibung, Galerie und Flächen-Tabelle entstehen aus einem Master-Template.

Die Schriften (Proxima Nova und Helvetica Neue) werden nicht per @font-face eingebunden, sondern als System-Fonts in Dompdf registriert – das ist robuster und vermeidet Probleme mit Schriftpfaden in der PDF-Engine.

Hash-basiertes Cache-Versioning

Für das Cache-Verhalten braucht es eine elegante Antwort auf eine ungemütliche Frage: Wann muss ein PDF neu generiert werden? Die naheliegende Lösung wäre ein Cron-Job, der täglich neu rendert. Oder ein manueller "Aktualisieren"-Button. Beides ist fehleranfällig.

Stattdessen berechnet das Plugin einen Hash über alle relevanten Datenfelder. Dieser Hash ist die Cache-Version. Wenn er sich beim nächsten Download-Aufruf vom gespeicherten Wert unterscheidet, wird neu generiert; sonst kommt die Datei aus dem Cache.

Der Vorteil: Datenänderung im WordPress-Backend triggert automatisch die Neugenerierung beim nächsten Download. Niemand muss daran denken. Niemand muss einen Knopf drücken. Solange nichts sich ändert, kommt das PDF in unter einer Sekunde aus dem Cache. Sobald jemand den Preis ändert oder ein Bild austauscht, fällt der Cache automatisch um.

PHP
// Cache-Versioning + Concurrent-Lock in einer Funktion
function inex_get_or_generate_pdf($post_id, $force_regenerate = false) {
    $filepath        = inex_get_expose_directory() . '/' . inex_get_cached_pdf_filename($post_id);
    $cache_version   = get_post_meta($post_id, '_inex_pdf_cache_version', true);
    $current_version = inex_get_cache_version($post_id);

    // Cache-Hit: PDF existiert und Hash stimmt → fertige Datei zurückgeben
    if (!$force_regenerate && file_exists($filepath)
         && hash_equals((string) $cache_version, (string) $current_version)) {
        return $fileurl;
    }

    // Concurrent-Lock: nur ein Worker generiert dieselbe Datei gleichzeitig
    if (!inex_acquire_pdf_generation_lock($post_id)) {
        return new WP_Error('pdf_generation_locked', 'PDF wird gerade erstellt…');
    }

    try {
        $data = inex_collect_expose_data($post_id);
        inex_render_expose_pdf_file($data, $filepath . '.tmp');
        @rename($filepath . '.tmp', $filepath);
        update_post_meta($post_id, '_inex_pdf_cache_version', $current_version);
        return $fileurl;
    } finally {
        inex_release_pdf_generation_lock($post_id);
    }
}

Bemerkenswert sind drei Details: Erstens das hash_equals für den Cache-Vergleich – zwar hier nicht sicherheitskritisch, aber konsequent zur Vermeidung von Timing-Issues. Zweitens das rename auf eine .tmp-Datei, das atomar ist; nie sieht ein paralleler Reader eine halbfertige Datei. Drittens das try/finally, das den Lock auch dann sauber freigibt, wenn beim Rendering eine Exception fliegt.

Multiply-Blending in PHP-GD

Wer schon mal mit Dompdf gearbeitet hat, kennt eine seiner Schwächen: CSS-Opacity wird nicht zuverlässig unterstützt. Wenn das Layout aber – wie hier – mit halb-transparenten Filter-Overlays über dem Featured Image arbeitet (klassisches Maklerexposé-Look mit dunklem Farbverlauf für die Lesbarkeit der Headline), wird das schnell zum Problem.

Die Lösung: Das Blending wird gar nicht erst in CSS versucht, sondern vor dem Rendering pixelweise in PHP-GD durchgeführt. Featured-Image und Filter-Overlay werden in den Speicher geladen, dann läuft eine Schleife über jeden Pixel und kombiniert die beiden Bilder mit der Multiply-Formel: für jeden Farbkanal ergibt sich der neue Wert aus (a × b) / 255. Das Ergebnis wird als JPEG mit hoher Qualität encoded und als Base64-Data-URI ins HTML eingebettet.

Das ist Brute-Force-Mathematik in PHP, kein Highlight für die Performance – die Bildverarbeitung ist der Bottleneck der Kalt-Generierung. Aber das Ergebnis ist pixelgenau das, was ein Designer im Layout-Tool produziert hätte. Und durch das Cache-Versioning passiert die Berechnung nur, wenn sich tatsächlich etwas geändert hat.

Wenn die Library etwas nicht kann

Die typische Reaktion auf "Dompdf kann kein CSS-Opacity" wäre: andere Library nehmen. Bei den meisten Projekten ist das auch richtig. Bei einer einzelnen, klar abgegrenzten Layout-Anforderung ist eine Pre-Computation-Stage in PHP-GD oft schneller umgesetzt als ein kompletter Library-Wechsel – und sie hält das Hosting-Setup einfach.

Concurrent-Safety & signierte URLs

Sobald mehrere Personen gleichzeitig Exposés generieren, wird Lock-Logik wichtig. Zwei parallele Anfragen für dasselbe Inserat dürfen nicht beide die teure Bildverarbeitung starten – einer rendert, der andere wartet. Die Lösung läuft über einen WordPress-Transient als Mutex: wer zuerst den Lock setzt, rendert; alle anderen pollen mit kurzer Wait-Loop (max. drei Sekunden), bevor sie den fertigen Cache-Eintrag bekommen.

Für die Auslieferung des PDFs gibt es keine direkten URLs auf die Dateinamen im uploads-Ordner. Stattdessen ist der Speicher-Ordner per .htaccess mit Options -Indexes gegen Directory-Listing gesperrt, und jeder Download geht über eine signierte URL: HMAC-SHA256 über Post-ID und Ablaufzeit, signiert mit dem WordPress-Auth-Salt, TTL eine Stunde. Beim Download-Request wird die Signatur per hash_equals verglichen (Timing-Attack-sicher), und falls valide, die Datei mit korrektem MIME-Type ausgeliefert.

Zusätzlich greift ein Rate-Limit: zehn Downloads pro Stunde pro IP-Adresse, ebenfalls über Transients realisiert. Reicht für reguläre Nutzung, blockiert Crawler und Scraper.

Zahlen & Architektur

Die Eckdaten des Plugins:

  • ~9.350 Zeilen PHP, CSS und JavaScript ohne Vendor-Code
  • 2 Custom Post Types (Inserate, Ansprechpartner) plus Property-Table-JSON
  • ~35 Meta-Felder pro Immobilie
  • 1 Master-Template, 1 CSS-Datei mit ~1.800 Zeilen
  • Composer-Pakete: dompdf/dompdf 2.x, setasign/fpdi 2.3, phenx/php-svg-lib 0.5
  • PHP ≥ 7.4
Datei Zeilen Code Verantwortlichkeit
pdf-generator.php ~3.190 Dompdf-Pipeline, GD-Bildvorverarbeitung, FPDI-Merge, Cache-Logik
data-collector.php ~1.320 JetEngine-Datensammlung, Property-Table-Parsing, Dezimal-Parser
expose-template.php ~1.440 HTML-Master-Template mit Output-Buffering
expose-styles.css ~1.825 Layout, Page-Breaks, Corporate Design
settings-page.php ~750 Admin-UI, Logos, Disclaimer, Cache-Tools
ajax-handler.php ~400 Nonces, Token, Rate-Limit, signierte URLs

Auffällig ist die Größe von pdf-generator.php. Das wäre ein guter Refactoring-Kandidat – die Bildverarbeitungslogik (etwa 600 Zeilen für Multiply-Blending und Cover-Crop) gehört eigentlich in eine eigene Klasse. Funktioniert hat es so wie es ist, und stabilität-vor-Ästhetik war bei diesem Projekt ein bewusst gesetzter Kompromiss.

Wann lohnt sich Eigenentwicklung gegenüber Makler-SaaS?

Maklerportale wie onOffice, FlowFact oder propstack liefern Exposé-Generierung als Modul mit. Das ist kein Vorwurf – im Gegenteil, für viele Maklerteams ist das die richtige Wahl, weil es CRM, Kundenverwaltung, Portalanbindung und Exposés in einem Paket bringt.

Eigenentwicklung als WordPress-Plugin lohnt sich in dieser Konstellation:

  • Es gibt eine bestehende WordPress-Website, auf der die Inserate ohnehin gepflegt werden – Exposé und Website teilen Daten und Bilder, keine doppelte Pflege
  • Das Layout muss exakt zum Corporate Design passen, ohne Layout-Editor-Limits
  • Das Team ist klein bis mittelgroß – die typischen 50–150 Euro pro Nutzer und Monat summieren sich
  • Datenhoheit ist wichtig – die Inserate verlassen den eigenen Server nicht
  • Es soll kein zweites System mit eigenem Login, eigener Pflege, eigenem Audit-Log geben
Kriterium Makler-SaaS WordPress-Plugin
Layout-Anpassbarkeit Vorlagen mit Templates Pixelgenau im Code
Setup-Kosten Niedrig Einmalig vier- bis fünfstellig
Laufende Kosten Pro Nutzer und Monat Hosting + Wartung
Datenhoheit Beim Anbieter Auf eigenem Server
Workflow-Integration Eigenes System Direkt in WordPress
Roadmap-Abhängigkeit Vom Anbieter bestimmt Selbst entscheidbar

Die größte Stärke des Eigenbaus ist, dass Website und Exposé keine zwei Welten sind. Die Bilder, Beschreibungen und Daten, die auf der Inserate-Detailseite im Web stehen, sind exakt dieselben, die ins PDF fließen. Kein Export, kein Sync, kein "welche Version ist aktuell?" – ein Hash-Vergleich, ein Klick, fertig.

Was Eigenentwicklung nicht ersetzt

Ein Plugin ist kein CRM. Wer Kundenstammdaten, Besichtigungstermine, Vertragsmanagement und Portalanbindung in einer Oberfläche braucht, fährt mit einer Makler-Software besser. Der Exposé-Generator ist eine Spezialdisziplin – exzellent in seinem Bereich, aber nicht der Ersatz für die ganze Maklersoftware.

Häufige Fragen zum Exposé-Generator

Wie wird ein Exposé-PDF aktualisiert, wenn sich Daten in WordPress ändern?

Das Plugin berechnet einen Hash über alle relevanten Felder und vergleicht diesen mit der Cache-Version des bereits generierten PDFs. Bei jeder Änderung an Adresse, Preis, Bildern oder Beschreibung ändert sich der Hash – beim nächsten Download wird das PDF automatisch neu erzeugt. Solange sich nichts ändert, kommt das PDF aus dem Cache und der Download ist in unter einer Sekunde da.

Welche WordPress-Komponenten braucht der Exposé-Generator?

JetEngine als Datenebene mit zwei Custom Post Types (inserate, ansprechpartner) und etwa 35 Meta-Feldern pro Immobilie. Optional ein zweites Plugin für strukturierte Flächenverwaltung mit Buildings, Units und Floor-Plans, dessen Daten als JSON ins Property-Meta einfließen. Der Generator selbst kommt mit Dompdf, FPDI und PHP-GD aus – kein externer Service, kein Cron, keine REST-API.

Warum Dompdf und nicht mPDF oder wkhtmltopdf?

Dompdf läuft als reine PHP-Library – kein binäres Headless-Browser-Setup wie bei wkhtmltopdf, kein Server-Side-Service. Das vereinfacht Hosting und Deployment auf jedem Standard-WordPress-Hoster. mPDF wäre eine valide Alternative; im Verlauf des Projekts gab es einen Test-Branch dafür. Aktuell läuft Dompdf 2.x produktiv, ergänzt um FPDI für das Mergen externer PDFs (Grundrisse, Energieausweise) und PHP-GD für die Bildvorverarbeitung.

Wie funktioniert das Multiply-Blending der Bilder ohne CSS-Opacity?

Dompdf unterstützt CSS-Opacity nicht zuverlässig. Stattdessen rechnet das Plugin mit PHP-GD pixelweise: Featured-Image und Filter-Overlay werden vor der PDF-Generierung im Speicher kombiniert, das Multiply-Blending wird mathematisch nachgebildet, und das fertige Bild wird als Base64-Data-URI ins HTML eingebettet. Aufwendig, aber das Ergebnis ist pixelgenau das, was ein Designer im Layout-Tool produzieren würde.

Wie sicher sind die PDF-Download-Links?

Jeder Download läuft über eine signierte URL: HMAC-SHA256 über Post-ID und Ablaufzeit, signiert mit dem WordPress-Auth-Salt und einer TTL von einer Stunde. Vergleich per hash_equals() gegen Timing-Attacks. Zusätzlich greift ein Rate-Limit von zehn Downloads pro Stunde pro IP via Transient. Der Speicher-Ordner ist per .htaccess gegen Directory-Listing gesperrt.

Was kostet eine Makler-SaaS-Alternative wie onOffice oder FlowFact?

Marktpreise liegen typischerweise bei 50–150 Euro pro Nutzer und Monat – je nach Funktionsumfang und Vertrag. Bei zehn Nutzern summiert sich das auf 6.000–18.000 Euro pro Jahr, ohne dass man am Layout, an den Datenfeldern oder am Workflow groß drehen kann. Eine eigene Lösung in WordPress amortisiert sich bei mittelgroßen Maklerteams oft nach 12–24 Monaten – plus volles Eigentum an Daten, Layout und Code.

Eigene Exposés automatisieren?

Du planst etwas Ähnliches – Exposé-Generierung, automatische PDFs aus WordPress-Daten oder eine Layout-Engine im Corporate Design? Schreib mir, ich schaue mir deinen Workflow an.

Kontakt