Aufbau von elleXX

elleXX ist eine Website, die sich an Frauen richtet, um sie bei der Schliessung von Finanzlücken zu unterstützen. Sie wird von Journalistinnen betrieben, die viele nützliche Inhalte zu diesem Thema verfassen. Als Frau in der Schweiz war es mir eine Ehre, an diesem Thema, das auch die Werte von Liip widerspiegelt, mitzuarbeiten. Das entscheidende Kriterium für elleXX bestand darin, Inhalte schnell und einfach veröffentlichen zu können. Ausserdem gibt es einige Integrationen mit Produkten, welche die Leser*innen bei ihren finanziellen Entscheidungen unterstützen. Wichtig war zudem, die Website-Member im Auge behalten zu können.

Beim Content Management haben wir uns für Ghost CMS entschieden – und das aus gutem Grund. Hauptsächlich, weil sich Ghost für unser Hauptanliegen sehr gut eignet: Content. Der zweite Grund besteht darin, dass Ghost zu 100 % durch seine User*innen finanziert wird – es ist ein Open-Source-CMS!

Um Leads und Kontakte im Auge zu behalten, haben wir uns für Friendly Automate entschieden. Das beste Argument hierfür: Es ist ein lokales Schweizer Unternehmen. Ausserdem basiert es auf Mautic, einem bekannten Framework für Marketinglösungen.

Anschliessend brauchten wir einige kundenspezifische Integrationen mit Vontobel, MigrosBank und Cap Rechtsschutz. Dafür haben wir mit dem JS-Framework Nest.js unsere eigene API entwickelt. Nest.js macht die Erstellung einer API einfach und schnell. Dank diesem Framework können wir Entwickler uns auf die Logik der API konzentrieren und müssen uns weniger um Dinge wie das Routing usw. kümmern. Danach verwenden wir die API unserer Ghost-Instanz.

Ghost war hinsichtlich der benutzerdefinierten Funktionen auf der Website nicht so flexibel, wie wir es uns gewünscht hätten. Deshalb mussten wir einen Ghost-Fork erstellen und unsere eigene Kopie bearbeiten. Beispiel: Der Kunde wollte die Anzeige bestimmter Inhalte vereinheitlichen und einfacher machen. Für gewöhnlich versucht man die Abspaltung (Fork) einer Codebasis zu vermeiden, aber sie brachte uns sehr viel mehr Flexibilität, die wir sonst nicht gehabt hätten.

REST-API-Design mit Nest.js
Nest ist ein Node.js-Framework, das die Erstellung skalierbarer, serverseitiger Anwendungen vereinfacht. Als PHP-Entwicklerin habe ich mich anstelle einer PHP-Lösung, an die ich gewöhnt bin, für Nest entschieden, weil das Team, mit dem ich dann arbeite, besser mit JavaScript zurechtkommt. Für mich ist der wichtigste Faktor bei der Wahl einer Technologie, wie einfach sie für die Menschen ist, die sie warten müssen. Und wir sind wirklich froh darüber, dass wir uns für dieses Framework entschieden haben! Es hat wirklich grossen Spass gemacht, damit eine API zu erstellen.

Nest ist in Module unterteilt, wobei jedes Modul seine eigene Konfiguration wie auch seine eigenen Controller und Services/Provider hat. In unserem Projekt haben wir uns dazu entschieden, für jede Integration ein eigenes Modul zu erstellen, das heisst also ein Vontobel-Modul, ein MigrosBank-Modul und ein Cap-Rechtsschutz-Modul. Wir haben auch noch ein paar weitere notwendige Module ergänzt, so zum Beispiel «Auth», «Mail», «Ghost», «Customer» ...

Wir haben einen ganzen Controller in Nest gebaut, um ein neues Ghost-Member zu schaffen:

import { Controller, Post, Body, Get } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { GhostService } from './ghost.service';

@Controller('ghost')
@ApiTags('ghost')
export class GhostController {
  constructor(private readonly ghostService: GhostService) {}

  @Post('new')
  async newMember(@Body() body): Promise<any> {
    return this.ghostService.getGhostMemberUuidFromEmailCreateNewMemberIfNotExists(
      body.firstname,
      body.email,
    );
  }
}

In Nest verwenden wir viele Annotationen, was das Programmieren einfach und unkompliziert macht. Beachte die Verwendung von @Controller, um darauf hinzuweisen, dass es sich um einen Controller @ApiTags für offene API-Dokumentationen und @Post für die Route handelt!

Die Code-Injektion funktioniert aufgrund der Konfiguration im Modul:

import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { GhostController } from './ghost.controller';
import { GhostService } from './ghost.service';

@Module({
  imports: [HttpModule],
  controllers: [GhostController],
  providers: [GhostService],
})
export class GhostModule {}

Beachte, dass wir dort einen «Ghost-Service» als Provider haben, also einen Service, den wir selbst entwickelt haben. Der Ghost-Service verfügt über Methoden zur Kommunikation mit Ghost. Wir verwenden diese, um für unsere Integrationen Ghost-Member mit den API-Membern zu verbinden und auch, um Member zu Friendly Automate hinzuzufügen, sobald sie sich registrieren.

TypeScript ist vollständig typisiert, was uns durch die Verwendung von Objekten für API-Antworten ausserdem den Vorteil einer schönen Dokumentation mit Open API bietet. In den Objekten vermerkst du die Eigenschaften für die Open API zur Ausgabe von Dokumentationen wie:

@ApiProperty({
    required: true,
    description:
      'The id that the frontend calling the ellexx api is using to identify their user, is usually a unique identifier',
    example: 'sdfsd-23432-sfdfsd-2323',
  })
  public frontendId: string;

Auf diese Weise kann eine Open API mit wenig Aufwand erstellt werden, bietet aber eine unschätzbare Dokumentation für API-Consumer.

Infrastruktur

Für das Hosting wollten wir eine Schweizer Lösung und haben uns für Managed Kubernetes von Exoscale entschieden. Ausserdem verwenden wir Longhorn für MySQL. Für unser Projekt ist die Verwendung von Kubernetes definitiv ein Overkill, aber es funktioniert wunderbar. Wir nutzen GitLab, um unseren Code zu hosten und GitLab CI, um unseren Workflow zu automatisieren. Beim Pushen von Code wird ein Image erstellt und bereitgestellt. «to stage» für den Stage-Branch und «to prod» für den Haupt-Branch. Unsere Kubernetes-Lösung hat den Vorteil, dass sie skalierbar ist. ABER: Ghost ist nicht skalierbar. Dieses Problem solltest du bei einer viel besuchten Website mittels HTTP-Caching lösen. Wir haben sicherheitshalber einen einfachen Varnish-Cache erstellt. Die API ist hingegen skalierbar. Aufgrund der dynamischen Natur der Endpunkte können wir in diesem Fall keinen grossen Nutzen aus der Verwendung eines Cache ziehen.

Post-Mortem

Hätten wir bei der Verwendung von Ghost CMS, Nest, Friendly Analytics, GitLab, GitLab CI, Kubernetes und MySQL mit Longhorn etwas anders machen können? Nein, eigentlich nicht! Wir sind sehr zufrieden mit unserem Set-up. Das Einzige, das wir möglicherweise hätten anders machen können, ist, Ghost als Headless CMS einzusetzen. Aber letztlich hat es auch so gut funktioniert, und wir sind alle sehr glücklich und stolz auf das, was wir erreicht haben.