Going Headless with Drupal
Vor einiger Zeit hat sich mein Kollege Fabian in einem Blogpost mit dem Thema Headless CMS befasst. Darauf ausgehend möchte ich heute auf Drupal als Headless CMS eingehen und aufzeigen was gut läuft und über welche Hürden man dabei stößt.
Wie in Fabians Blogbeitrag beschrieben wurde, geht es bei Headless CMS darum, Inhalte über eine Schnittstelle (Bsp.: REST) in einem standardisierten Format bereitzustellen, damit diese von diversen Clients (Bsp.: Website Frontend, Native App, …) konsumiert werden können. Man kann natürlich so weit gehen, die Inhalte auch über die Schnittstelle zu verwalten, aber da Drupal bereits mit einigen Administration-Themes ausgeliefert wird und mit Contrib-Themes, wie dem Gin-Theme, auch out-of-the-box gut zu bedienen ist, werden wir uns in diesem Beitrag nur auf das Bereitstellen von Inhalten beschränken.
Drupal Core Module
Zu Beginn gleich eine gute Nachricht: Drupal 9 wird bereits mit allen notwendigen Modulen ausgeliefert, um REST APIs bereitzustellen! Im Folgenden werde ich kurz die Funktionalität der Module zusammenfassen.
Serialization
Drupal verarbeitet Inhalte intern als Entities. Details zur Entity-API können auf der offiziellen Drupal Seite nachgelesen werden. Für diesen Blogbeitrag ist nur wichtig zu verstehen, dass alle Entities für Drupal PHP-Objekte sind.
Um diese Objekte via HTTP(S) an Clients übertragen zu können, müssen sie in ein Format konvertiert werden, welches von den Clients gelesen werden kann. Hier kommt das Serialization-Modul ins Spiel. Es stellt ein Service bereit, welches Daten (wie unser Entity-Objekt) in Formate wie JSON oder XML serialisiert. Zusätzlich kann das Service natürlich auch den umgekehrten Weg gehen: also Daten, welche in Formaten wie JSON oder XML vorliegen, in PHP-Objekte zu deserialisieren.
In der Dokumentation der Serialization-API kann man nachlesen, wie das Serialisieren/Deserialisieren technisch implementiert ist, wie man neue Formate (Bsp.: CSV) hinzufügen kann und wie die Funktionalität bereits existierende Serializer ändern kann.
Als gängiges Format für die Kommunikation in REST-APIs hat sich über die Zeit JSON etabliert. Daher wird dieses Format standardmäßig vom Serialization-Modul unterstützt.
Das Serialization-Modul wird von allen weiteren Modulen zum Serialisieren/Deserialisieren von Daten verwendet.
RESTful Web Services
Dieses Modul kann sowohl Entitäten als auch benutzerdefinierte Ressourcen via REST API bereitstellen. Da man beim Implementieren von Custom REST-Ressourcen weder beim Aufbau der URL noch bei der Struktur des Request-Bodies beschränkt ist, kann man alle möglichen komplexen Business-Cases implementieren. Hier sind der Komplexität und Kreativität keine Grenzen gesetzt.
Note: Wie in der Dokumentation gut beschrieben ist, muss jedes RestResource-Plugin, welches man für die REST-API freischalten möchte, eine entsprechende Config-Entity angelegt werden. Mithilfe des Contrib-Moduls REST UI, kann dieser Schritt bequem im User Interface durchgeführt werden. Das erleichtert die Arbeit um ein Vielfaches! 😉
JSON:API
Das JSON:API Modul implementiert die JSON:API Spezifikation für Drupal Entities. Im Gegensatz zum RESTful Web Services-Modul stellt das JSON:API-Modul auch Collection-Ressourcen bereit. Beim Laden der Entity-Listen bietet es eine Vielzahl von Funktionen, wie das Filtern von Inhalten, das Laden und Inkludieren von Relations, benutzerdefinierte Sortierung und Paginierung.
Zu beachten ist jedoch, dass das Core-Modul keine Konfigurationsmöglichkeiten bietet und somit alle Entity-Typen via REST-API bereitstellt! Das ist in den meisten Fällen nicht notwendig und auch nicht erwünscht. Glücklicherweise gibt es für diesen Fall ein Contrib-Modul namens JSON:API Extras, das die Konfiguration einzelner Ressourcen erlaubt. Damit kann man einzelne Entity-Typen und Bundles für die REST API freigeben.
Anmerkung zur Datensicherheit
Als Sicherheitsaspekt sei noch erwähnt, dass beide Module Drupals Entity-Access respektieren. Also unabhängig, ob man Entities über das RESTful Web Services-Modul oder das JSON:API-Modul bereitstellt, werden Inhalte, auf die ein Benutzer keinen Zugriff hat, nicht an den Verbraucher ausgeliefert.
Das Headless Setup bei Liechtenecker
Da das JSON:API-Modul out-of-the-box mehr Funktionen als die Entity-Ressource des RESTful Web Services-Modul bietet, verwenden wir das JSON:API-Modul zum Bereitstellen von Entities. In der Dokumentation des Moduls gibt es eine Tabelle, welche die Unterschiede zwischen dem JSON:API– und dem RESTful Web Services-Modul übersichtlich darstellt.
Wenn wir für Spezialfälle komplexe Abfragen oder Workflows benötigen, welche den Funktionsumfang von JSON:API sprengen, implementieren wir dafür custom RestResource-Plugins, die dann vom RESTful Web Services-Modul via REST-API bereitgestellt werden.
Challenges
So toll das auch klingt, es gab immer noch einige Herausforderungen zu bewältigen, bevor wir Drupal als vollwertiges Headless CMS akzeptiert haben. Im Folgenden werde ich kurz auf die größten eingehen, die uns bei der Implementierung aufgefallen sind und kurz umreißen, wie wir die Probleme gelöst haben.
Routing/Path aliases
Die systeminternen URLs zu Inhalten sind in Drupal nicht sonderlich schön. Die URLs werden gewöhnlich aus der Entity-Type-ID und der Entity-ID zusammengebaut. Also sieht die URL eines Blogposts beispielsweise so aus: /node/3927.
node ist dabei die Entity-Type-ID und 3927 die Entity-ID.
Da man keinen SEO-Experten auf dieser Welt finden wird, der mit dieser URL Struktur zufrieden ist, wird Drupal mit einem Modul namens Path ausgeliefert. Mit diesem kann man für jede Inhalts-Seite einen URL-Alias definieren, welcher dann wiederum von dem Modul auf die jeweilige interne URL auflöst. Das Path-Modul funktioniert allerdings nur in einem monolithischen Kontext, da das JSON:API-Modul eigene Routes zum Laden von Entities via REST-API bereitstellt.
Die internen JSON:API Routes haben folgende Struktur:
/jsonapi/<entity-type>/<bundle>/<entity-uuid>
Als Beispiel für einen Blogpost also:
/jsonapi/node/blogpost/1a7cb8d0-3210-43dd-802b-2003b3c81ace
Man stelle sich nun vor, ein Benutzer öffnet den Browser und besucht folgende URL:
Wir gehen hier von einem Decoupled-Kontext aus, daher löst die Domain liechtenecker.at auf unsere Frontend-Web-App (Nuxt) auf, welche als Datenquelle die Drupal-REST-API verwendet. Sofern nicht alle verfügbaren Seiten in Nuxt pre-rendered vorliegen, kennt das Frontend nicht alle im CMS verfügbaren URLs — vor allem nicht die Drupal-internen mit den Entity-UUIDs, die zum Laden von JSON:API-Ressourcen benötigt werden.
Wir brauchen also die Möglichkeit, JSON:API-Ressourcen via Path-Alias zu laden:
GET /jsonapi/blog/going-headless-with-drupal
Dafür habe ich mir angeschaut, wie das Path-Modul diese Aufgabe löst: dort prüft ein Inbound-Path-Processor, ob die aktuelle URL ein registrierter Alias ist — wenn dem so ist, wandelt der Processor den Alias auf den internen Pfad um, der dann vom Routing-System verarbeitet werden kann.
Da dachte ich mir: cool gelöst! Exakt denselben Schmäh können wir auch für unsere /jsonapi/ URLs implementieren! Gesagt, getan: so kam es, dass wir einen Inbound Path Processor implementiert haben, welcher alle eingehenden URLs auf /jsonapi/<diverse-strings-und-subpaths> analysiert und prüft, ob <diverse-strings-und-subpaths> ein registrierter Path-Alias ist. Sofern ein Alias gefunden wurde, wandelt der Path-Processor den Alias in den Drupal-internen JSON:API-Pfad um, der dann von Drupals Routing-System verarbeitet werden kann → solved ✅
Previews
Als Content-Manager hat man bei großen Änderungen oft das Bedürfnis, Inhalte vor der Veröffentlichung zu reviewen. In einem decoupled Szenario ist das nicht so einfach umzusetzen, da die Inhalte ja eigentlich nicht gespeichert werden sollten, bevor das OK vom Reviewer gegeben wurde. Das Problematische an der Sache ist, dass es nicht möglich ist, nicht gespeicherte Inhalte über eine REST-API bereitzustellen — ein Dilemma!
Um das Problem zu umgehen, nutzen wir Drupals built-in Content-Versioning-System in Kombination mit dem Content Moderation-Modul. Dieses erlaubt es neue Revisionen von Inhalten zu erstellen, welche nicht gleich veröffentlicht werden, sondern erstmal als “Draft” vorliegen, während die bereits veröffentlichte Version davon unverändert bleibt! Erst wenn der neuere Draft vom Editor veröffentlicht wird, werden die Änderungen für End User sichtbar.
Des Weiteren nutzen wir die Funktion des JSON:API-Moduls, Inhalte in bestimmten Versionen mittels resourceVersion Parameter zu laden.
Hier ist noch folgendes zu beachten: der User, der die Preview-Seite aufruft, benötigt die entsprechenden Berechtigungen um auf unveröffentlichte Revisionen der Inhalte zugreifen zu dürfen (der Draft ist ja nicht veröffentlicht)! Das kann unterschiedlich gelöst werden:
- Um die Previews im Frontend aufzurufen, muss man auch im Frontend authentifiziert sein.
- Man generiert im CMS einen kurzlebigen previewToken und gewährt Benutzern mit gültigen previewToken programmatisch lesenden Zugriff auf die Inhalte.
Diese Optionen müssen pro Projekt abgewogen und entschieden werden.
Um Previews für Content-Editoren so angenehm wie möglich zu gestalten, haben wir die Funktionalität des Preview-Buttons bei Inhalten angepasst, dass beim Klick darauf folgendes passiert (bei Option a ohne previewToken):
- Alle Änderungen werden als neue Revision im Status “draft” gespeichert. Die neue Revisions-ID wird dann beim Generieren der Preview-URL verwendet.
- (nur bei Option b) Das CMS generiert einen temporären Preview-Token, der später für die Zugriffskontrolle von unveröffentlichten Inhalts-Revisionen dient.
- Das CMS generiert die Frontend-Preview-URL:
https://<frontend-base-url>/<path-alias>?previewToken=<previewToken>&resourceVersion=<revisionID> - Das CMS öffnet einen neuen Tab mit der Frontend-Preview-URL.
- Die Frontend-Web-App versucht den Inhalt für den Pfad <path-alias> dynamisch aus der CMS-REST-API zu laden und übergibt dabei den previewToken und resourceVersion Parameter:
https://<cms-api-base-url>/jsonapi/<path-alias>?previewToken=<previewToken>&resourceVersion=<revisionID> - Da das JSON:API-Modul out-of-the-box das Laden von spezifischen Revisions unterstützt, wird automatisch die korrekte Version zurückgegeben!
→ solved ✅
Conclusion
Zusammenfassend kann man sagen, dass Drupal Core bereits mit den wichtigsten Features für ein Headless CMS ausgeliefert wird. Trotz der Challenges, die beim initialen Einrichten für etwas Kopfzerbrechen gesorgt haben, sind wir überzeugt davon, dass wir mit Drupal auf ein zukunftssicheres erweiterbares System setzen. Da wir die größten Challenges bereits gelöst haben, ist das Setup von neuen Headless-Projekten nun rasch erledigt, wodurch wir uns auch mit Drupal direkt auf das Modellieren der Daten konzentrieren können.
Beim Vergleich mit anderen Node-JS-basierten Systemen gewinnt Drupal in Punkten Dokumentation und Erweiterbarkeit. Was uns auch an den sonst weit verbreiteten Headless CMS stört, ist, dass es bei den meisten keine Möglichkeit des Self-Hostings mehr gibt.
Ein weiterer großer Pluspunkt von Drupal als CMS (sei es nun Headless oder monolitisch) ist die sehr engagierte Open-Source Community. Auf https://drupal.org werden Issues auf sehr hohem Niveau diskutiert, Neuentwicklungen im Core werden mit großartig kritischen Augen betrachtet und es wird hoher Wert auf automatisiert abgetesteten Code gelegt.
Gibt es Fragen oder Anmerkungen? Schreibt uns gerne in die Kommentare!
Du willst mit jemanden über das Thema plaudern?
Einen kostenlosen Termin mit CEO Susanne vereinbaren!UX Snacks Vol.09
That’s a wrap on UX Snacks 2024. Am 7. November hat die vierte und letzte Ausgabe in diesem Jahr stattgefunden und wir nehmen mit diesem Recap ganz viel positive UX-Energie mit ins neue Jahr. Und keine Angst: Schon bald verkünden wir die Daten für 2025.
Jetzt lesenFolge #62 mit Susanne Liechtenecker
In Folge 62 besinnt sich Susanne auf die Anfänge dieses Podcasts und begrüßt keinen Gast, sondern erzählt über das Buch "Jäger, Hirten, Kritiker" von Richard David Precht und warum es sie inspiriert hat.
Jetzt anhören