Web Technology: headless with strategy part II
Background
Marketing-Webseiten (gerade unsere) sind eher statisch: Es ändert sich eher wöchentlich als minütlich etwas. Daher ist es aus meiner Sicht kaum zu vertreten, sie nicht statisch zu generieren. Neben den Performance-Gewinnen vereinfacht dies das Hosting ungemein, es kann mit eigentlich jeder Lastspitze umgegangen werden und die Idee, dass irgendwo statisch generiertes, echtes HTML herumliegt wirkt einfach “richtig” (vermutlich auch aus Sentimentalität – so haben wir schließlich mal mit der Webentwicklung angefangen).
Zweitag V3, der Vorgänger unserer Website, war deshalb auch mit Middleman umgesetzt, einem statischen Webseitengenerator in Ruby (erinnert sehr stark an Jekyll, wenn man sich mit dem Thema schonmal beschäftigt hat). Für Assets wie JavaScript und (S)CSS haben wir von jeher Webpack eingesetzt, u.a. da das Zweitag-eigene CSS-System (ZASAF) sich SCSS zu Nutze macht. Die Website war (natürlich) ein git Repository. Ein Merge in den Production-Branch hat ein neues Deployment ausgelöst. Und das stellte mittelfristig auch ein zentrales Problem dar: Um etwas an der Website zu ändern, bedurfte es einem Grundwissen in diesen Technologien.
Für CMS Systeme wie Wordpress haben wir – neben den im letzten Artikel beschriebenen Problemen – einfach nicht genug Domänenwissen. Sicherlich kommen die meisten bei uns auch mit PHP und dem “Templating” von Wordpress zurecht, es ist aber nicht unser Tagesgeschäft und hätte damit den Charakter einer Insellösung. Das versuchen wir aber immer zu vermeiden.
Unser Vorgehen
Im ersten Schritt haben wir daher Zweitag V3 an ein Headless CMS angebunden. Mitarbeitende konnten jetzt Beiträge erstellen und bearbeiten, ohne den Source der Website bearbeiten zu müssen. Jedoch hat sich bei Middleman ein zentrales Problem gezeigt, dass alle klassischen statische Website-Generatoren wie Hugo, Jekyll, usw. haben: Das Rendering findet komplett im Backend statt. Man muss also für dynamische Vorschauen entweder einen Server laufen lassen oder darauf warten, dass eine neue Version der Seite generiert wurde. Das störte unseren Veröffentlichungsprozess enorm. Auch waren viele Dinge, die wir für unsere Website brauchten “hausgemacht”, weil es keine Pakete für middleman gab.
Es musste also ein neuer Websitegenerator her, der folgende Anforderungen erfüllt:
- passt in unseren Technologiestack
- erlaubt das Rendern sowohl im Browser (als klassische Single Page Application) als auch (vorgerendert) auf dem Server
- hat eine große Community
- erlaubt einen guten Migrationspfad
Unsere Wahl ist auf Nuxt gefallen. Vue ist unser go-to Frontend Framework und Nuxt schien alles zu haben, was wir von einem statischen Framework verlangen konnten. Wir hatten Nuxt an anderer Stelle schon für Kundenprojekte evaluiert und auf Vue Konferenzen kennen gelernt, fühlten uns also wohl damit, das Experiment “neue Website” weiter zu treiben.
Herausforderungen
Da wir das CSS einfach kopieren konnten, war der Anfang schnell gemacht. Trotzdem haben sich – wie in jedem Projekt – auch hier Herausforderungen aufgetan, die wir vorher nicht geahnt hatten. Hier sind einige davon:
Definition der dynamischen Routes für das statische Generieren
Damit der statische Generator von Nuxt weiß, welche Websiten es gibt, kann man in der zentrale Konfiguration (nuxt.config.js) alle dynamischen Routen angeben:
https://nuxtjs.org/docs/2.x/configuration-glossary/configuration-generate#routes
Das ist vor allem deshalb schön, weil man beim Laden der möglichen Seiten (z.B. Blog Posts) gleich den Seiteninhalt mit laden und der Seite übergeben kann. So kann man viele Seiten in einem Request laden und generieren lassen. Gerade für Blog Posts macht das unheimlich viel Sinn, hat aber einen ganz entscheidenden Nachteil: Logik-Duplikation. So werden die Seiten einerseits in einer Vue Komponente mit Inhalten versorgt, andererseits aber die Inhalte auch “von außen” geladen und in die Komponente gegeben. Darüber hinaus war gerade das Routing eine Herausforderung: Unsere Pfade sind multilingual und übersetzt. Dir Routing Methode erwartet aber, dass wir ihr sagen, welchen Pfad die zu generierende Seite hat. Wir mussten also das Routing, welches normalerweise nur im Router definiert ist auch nochmal in der Konfiguration festhalten.
Im Endeffekt haben wir hier folgenden Kompromiss gefunden: Wenn es “einfache” Routen und Payloads sind, definieren wir die statischen Seiten mit der oben genannten Konfigurationsmethode. Greift die Seite auf mehrere Daten zu und werden die Routen komplex (wie z.B. bei Blog-Kategorieseiten mit Pagination), so lassen wir den magischen Crawler seine Dienste tun, auch wenn dies bedeutet, dass das Generieren minimal länger dauert, da jede Seite ihre Daten einzeln zusammen sucht.
Rendern von Komponenten in einen String
Wir haben z.B. für das Image-Resizing eine kleine Komponente geschrieben. Es gab jedoch Stellen, wo wir dieses Komponente gerne “als String” genutzt hätten (z.B. um Bilder in Markdown zu resizen, den wir aus dem Headless CMS laden). Hier gibt es zwar Strategien, jedoch waren uns diese für den ersten Aufschlag zu komplex und wir haben einfach die meiste Logik der Komponente in wiederverwendbare JS Klassen gesteckt. Hier wäre es sehr schön gewesen, wenn es einen “ToStringRenderer” für Vue gegeben hätte (wie z.B. den React DOM Server).
Verlinkungen im Headless CMS
Das ist kein Nuxt-spezifisches Problem aber wir wollen natürlich auch im Headless CMS Seiten verlinken, dabei aber nicht die Routing-logik replizieren (davon soll das Headless CMS natürlich nichts wissen).
Vermutlich werden wir hier einfach eigene URLs definieren (z.B. projects://fillibri), welche wir dann in unserem Frontend entsprechend anpassen. Auch hier begegnet uns aber wieder das Problem, dass hier sinnvolle Komponenten (wie in diesem Fall NuxtLink) schwer in das gerenderte Markdown zu bekommen sind. Wir schreiben natürlich follow-up Blogbeiträge, wenn wir hierfür gute Lösungen gefunden haben.
TypeScript / Vue 3
Wir hätten gerne alles in TypeScript gemacht, wollten dafür aber auf Vue 3 warten, was wiederum von Nuxt noch nicht unterstützt wird. Wir haben TypeScript zwar gut vorbereitet, es ist war aber schade, dass wir nicht gleich die Umsetzung komplett in TypeScript machen konnten.
Metadaten, SEO-Tags und Lokalisierungen
Ohne ein gutes Google-Ranking erzeugt man kaum Conversions, daher sind aussagekräftige SEO-Tags, die richtigen Metadaten und die passenden Lokalisierungen von Relevanz. In Middleman gab es dafür das Frontmatter, was man sich zu Nutze ziehen konnte. Für jede Seite individuelle Meta-Tags oder der Fallback auf einen sinnvollen weiteren Standard. Bei Nuxt gibt es mit Nuxt Content Front Matter etwas vergleichbares, allerdings war das für unsere Umsetzung nicht nutzbar.
Das Nuxt-i18n Plugin bietet bereits SEO-Funktionalität und auf Basis dessen haben wir nun unseren eigenen SEO-Helper geschrieben, der allgemeine Attribute setzt und individuelle Attribute setzen lässt. So haben wir unser Front Matter in die head
-Methode der einzelnen Komponenten ausgelagert. Gerade für Blogbeiträge und Job-Posts ist ein individuelles Handling der Metadaten unumgänglich.
Ergebnis
Wir sind mit der neuen Website sehr glücklich. Nuxt hat eine gute Portion “Magie” (wie das Preloading der nächsten Inhalte, die Möglichkeit zwischen Server-Side Rendering, Client-Side Rending usw. hin- und her zu wechseln usw.) ohne dass es sich zu abstrakt anfühlt. Das Tempo, welches wir mit dem beschriebenen Vorgehen erreichten, war toll und das “Entwicklungstoolset” (z.B. mit der Möglichkeit die Seite lokal statisch zu generieren und dann zu testen) herausragend.
Wie in jeder Technologie gibt es Dinge, die wir noch verbessern wollen. Grundsätzlich konnten wir aber in kurzer Zeit die Technologie der Website auf die neuen organisatorischen Anforderungen anpassen und uns viel Flexibilität für zukünftige Weiterentwicklung kaufen.