Using Google Drive as a CMS

Avatar of Nathan Babcock
Nathan Babcock am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

We’re going to walk through the technical process of hooking into Google Drive’s API to source content on a website. We’ll examine the step-by-step implementation, as well as how to utilize server-side caching to avoid the major pitfalls to avoid such as API usage limits and image hotlinking. A ready-to-use npm package, Git repo, and Docker image are provided throughout the article.

Aber... warum?

I irgendwann im Entwicklungsprozess einer Website steht man an einem Scheideweg: Wie werden Inhalte verwaltet, wenn die Person, die sie verwaltet, technisch nicht versiert ist? Wenn die Inhalte von Entwicklern auf unbestimmte Zeit verwaltet werden, reichen reines HTML und CSS aus – aber das verhindert eine breitere Teamzusammenarbeit; außerdem möchte kein Entwickler für immer für Inhaltsaktualisierungen zuständig sein.

Was passiert also, wenn ein neuer nicht-technischer Partner Bearbeitungszugriff erhalten muss? Das könnte ein Designer, ein Produktmanager, eine Marketingperson, ein Unternehmensvorstand oder sogar ein Endkunde sein.

Dafür ist ein gutes Content Management System doch da, oder? Vielleicht so etwas wie WordPress. Aber das birgt eigene Nachteile: Es ist eine neue Plattform, mit der sich Ihr Team auseinandersetzen muss, eine neue Benutzeroberfläche zum Erlernen und ein neuer Angriffsvektor für potenzielle Angreifer. Es erfordert die Erstellung von Vorlagen, einem Format mit eigener Syntax und Eigenheiten. Benutzerdefinierte oder Drittanbieter-Plugins müssen für spezifische Anwendungsfälle geprüft, installiert und konfiguriert werden – und jedes davon ist eine weitere Quelle für Komplexität, Reibung, technische Schulden und Risiken. Der Umfang dieser gesamten Einrichtung kann Ihre Technik auf eine Weise beeinträchtigen, die dem eigentlichen Zweck der Website entgegenwirkt.

Was wäre, wenn wir Inhalte von dort beziehen könnten, wo sie bereits sind? Darauf zielen wir hier ab. An vielen Orten, an denen ich gearbeitet habe, wird Google Drive zum Organisieren und Teilen von Dateien verwendet, einschließlich Entwürfen für Blog- und Landingpage-Inhalte. Könnten wir die API von Google Drive nutzen, um ein Google Doc direkt mit einer einfachen REST-Anfrage als rohes HTML auf einer Website zu importieren?

Natürlich können wir das! Hier ist, wie wir es bei uns gemacht haben.

Was Sie brauchen

Nur ein paar Dinge, die Sie sich vielleicht ansehen möchten, während wir beginnen

Authentifizierung mit der Google Drive API

Der erste Schritt ist die Herstellung einer Verbindung zur Google Drive API, und dafür müssen wir eine Art von Authentifizierung durchführen. Das ist eine Voraussetzung für die Nutzung der Drive API, auch wenn die betreffenden Dateien öffentlich geteilt sind (mit aktivierter „Linkfreigabe“). Google unterstützt verschiedene Methoden dafür. Die gebräuchlichste ist OAuth, die den Benutzer mit einem Google-branded Bildschirm auffordert: „[App-Name] möchte auf Ihr Google Drive zugreifen“ und auf die Zustimmung des Benutzers wartet – nicht gerade das, was wir hier brauchen, da wir auf Dateien in einem einzigen zentralen Drive zugreifen möchten, anstatt auf das Drive des Benutzers. Außerdem ist es etwas schwierig, nur den Zugriff auf bestimmte Dateien oder Ordner zu gewähren. Der Scope `https://www.googleapis.com/auth/drive.readonly`, den wir verwenden könnten, wird wie folgt beschrieben:

Alle Ihre Google Drive-Dateien anzeigen und herunterladen.

Das ist genau das, was auf dem Zustimmungsbildschirm steht. Das ist potenziell alarmierend für einen Benutzer und, was noch wichtiger ist, es stellt eine potenzielle Sicherheitslücke für jedes zentrale Entwickler-/Administrator-Google-Konto dar, das die Website-Inhalte verwaltet; alles, worauf es zugreifen kann, wird über das Backend des Website-CMS offengelegt, einschließlich seiner eigenen Dokumente und alles, was mit ihm geteilt wurde. Nicht gut!

Der „Service-Account“

Stattdessen können wir eine etwas weniger verbreitete Authentifizierungsmethode nutzen: ein Google Service Account. Stellen Sie sich einen Service Account wie ein Dummy-Google-Konto vor, das ausschließlich von APIs und Bots verwendet wird. Es verhält sich jedoch wie ein vollwertiges Google-Konto; es hat seine eigene E-Mail-Adresse, seine eigenen Token zur Authentifizierung und seine eigenen Berechtigungen. Der große Vorteil hierbei ist, dass wir Dateien diesem Dummy-Service-Account wie jedem anderen Benutzer zugänglich machen – indem wir die Datei mit der E-Mail-Adresse des Service-Accounts teilen, die etwa so aussieht:

[email protected]

Wenn wir ein Dokument oder eine Tabelle auf der Website anzeigen möchten, klicken wir einfach auf die Schaltfläche „Teilen“ und fügen diese E-Mail-Adresse ein. Jetzt kann der Service-Account nur die Dateien oder Ordner sehen, die wir ihm explizit freigegeben haben, und dieser Zugriff kann jederzeit geändert oder widerrufen werden. Perfekt!

Erstellen eines Service-Accounts

Ein Service-Account kann (kostenlos) in der Google Cloud Platform Console erstellt werden. Dieser Prozess ist in den Entwicklerressourcen von Google gut dokumentiert und wird zusätzlich Schritt für Schritt im Begleit-Repository dieses Artikels auf GitHub beschrieben. Der Kürze halber spulen wir direkt nach erfolgreicher Authentifizierung eines Service-Accounts vor.

Die Google Drive API

Jetzt, wo wir drin sind, können wir anfangen, uns mit den Fähigkeiten der Drive API zu beschäftigen. Wir können mit einer modifizierten Version des Node.js Quickstart-Beispiels beginnen, angepasst an die Verwendung unseres neuen Service-Accounts anstelle von Client-OAuth. Dies wird in den ersten paar Methoden von `driveAPI.js` gehandhabt, die wir zur Verwaltung aller unserer Interaktionen mit der API erstellen. Der Hauptunterschied zu Googles Beispiel liegt in der `authorize()`-Methode, wo wir eine Instanz von `jwtClient` anstelle des `oauthClient` aus Googles Beispiel verwenden.

authorize(credentials, callback) {
  const { client_email, private_key } = credentials;

  const jwtClient = new google.auth.JWT(client_email, null, private_key, SCOPES)

  // Check if we have previously stored a token.
  fs.readFile(TOKEN_PATH, (err, token) => {
    if (err) return this.getAccessToken(jwtClient, callback);
    jwtClient.setCredentials(JSON.parse(token.toString()));
    console.log('Token loaded from file');
    callback(jwtClient);
  });
}

Node.js vs. Client-Seite

Noch ein Hinweis zur Einrichtung – dieser Code ist für den Aufruf von serverseitigem Node.js-Code vorgesehen. Das liegt daran, dass die Client-Anmeldeinformationen für den Service-Account geheim gehalten werden müssen und Benutzern unserer Website nicht offengelegt werden dürfen. Sie werden in einer Datei `credentials.json` auf dem Server gespeichert und über `fs.readFile` innerhalb von Node.js geladen. Sie sind auch in der `.gitignore` aufgeführt, um die sensiblen Schlüssel aus der Quellcodeverwaltung fernzuhalten.

Abrufen eines Dokuments

Nachdem die Bühne bereitet ist, wird das Laden von rohem HTML aus einem Google Doc ziemlich einfach. Eine Methode wie diese gibt ein Promise eines HTML-Strings zurück.

getDoc(id, skipCache = false) {
  return new Promise((resolve, reject) => {
    this.drive.files.export({
      fileId: id,
      mimeType: "text/html",
      fields: "data",
    }, (err, res) => {
      if (err) return reject('The API returned an error: ' + err);
      resolve({ html: this.rewriteToCachedImages(res.data) });
      // Cache images
      this.cacheImages(res.data);
    });
  });
}

Der Endpunkt `Drive.Files.export` erledigt die ganze Arbeit für uns hier. Die `id`, die wir übergeben, ist einfach das, was in der Adressleiste Ihres Browsers angezeigt wird, wenn Sie das Dokument öffnen, und zwar unmittelbar nach `https://docs.google.com/document/d/`.

Beachten Sie auch die beiden Zeilen zum Caching von Bildern – dies ist eine besondere Überlegung, die wir vorerst überspringen und im nächsten Abschnitt im Detail behandeln werden.

Hier ist ein Beispiel für ein Google-Dokument, das extern als HTML mit dieser Methode angezeigt wird.

Abrufen eines Tabellenblatts

Das Abrufen von Google Sheets ist mit Spreadsheets.values.get fast genauso einfach. Wir passen das Antwortobjekt nur ein wenig an, um es in ein vereinfachtes JSON-Array zu konvertieren, das mit Spaltenüberschriften aus der ersten Zeile des Tabellenblatts beschriftet ist.

getSheet(id, range) {
  return new Promise((resolve, reject) => {
    this.sheets.spreadsheets.values.get({
      spreadsheetId: id,
    range: range,
  }, (err, res) => {
    if (err) return reject('The API returned an error: ' + err);
    // console.log(res.data.values);
    const keys = res.data.values[0];
    const transformed = [];
    res.data.values.forEach((row, i) => {
      if(i === 0) return;
      const item = {};
      row.forEach((cell, index) => {
        item[keys[index]] = cell;
      });
       transformed.push(item);
      });
      resolve(transformed);
    });
  });
}

Der Parameter `id` ist derselbe wie bei einem Dokument, und der neue Parameter `range` bezieht sich hier auf einen Zellbereich, aus dem Werte abgerufen werden sollen, in der A1-Notation von Sheets.

Beispiel: Dieses Tabellenblatt wird gelesen und geparst, um benutzerdefiniertes HTML auf dieser Seite darzustellen.

…und mehr!

Diese beiden Endpunkte bringen Sie schon sehr weit und bilden das Rückgrat eines benutzerdefinierten CMS für eine Website. Aber tatsächlich kratzen sie nur an der Oberfläche des Potenzials von Drive für die Inhaltsverwaltung. Es kann auch

  • alle Dateien in einem bestimmten Ordner auflisten und sie in einem Menü anzeigen,
  • komplexe Medien aus einer Google Slides-Präsentation importieren und
  • benutzerdefinierte Dateien herunterladen und cachen.

Die einzigen Grenzen sind Ihre Kreativität und die Einschränkungen der vollständigen Drive API, hier dokumentiert.

Caching

Wenn Sie mit den verschiedenen Arten von Abfragen experimentieren, die die Drive API unterstützt, erhalten Sie möglicherweise eine Fehlermeldung wie „User Rate Limit Exceeded“. Es ist ziemlich einfach, dieses Limit durch wiederholtes Testen und Ausprobieren während der Entwicklungsphase zu erreichen, und auf den ersten Blick scheint es, als ob dies ein hartes Hindernis für unsere Google Drive-CMS-Strategie darstellen würde.

Hier kommt das Caching ins Spiel – jedes Mal, wenn wir eine *neue Version* einer Datei in Drive abrufen, cachen wir sie lokal (auch bekannt als serverseitig, innerhalb des Node.js-Prozesses). Danach müssen wir nur noch die *Version* jeder Datei prüfen. Wenn unser Cache veraltet ist, rufen wir die neueste Version der entsprechenden Datei ab, aber dieser Abruf erfolgt nur *einmal pro Dateiversion* und nicht einmal pro Benutzeranfrage. Anstatt nach der Anzahl der Website-Besucher zu skalieren, können wir nun nach der Anzahl der Aktualisierungen/Bearbeitungen in Google Drive als limitierenden Faktor skalieren. Unter den aktuellen Drive-Nutzungslimits für ein kostenloses Konto könnten wir bis zu 300 API-Anfragen pro Minute unterstützen. Caching sollte uns gut innerhalb dieses Limits halten, und es könnte durch Batch-Anfragen noch weiter optimiert werden.

Umgang mit Bildern

Die gleiche Caching-Methode wird auf Bilder angewendet, die in Google Docs eingebettet sind. Die `getDoc`-Methode parst die HTML-Antwort auf Bild-URLs und führt eine sekundäre Anfrage durch, um sie herunterzuladen (oder holt sie direkt aus dem Cache, falls sie bereits vorhanden sind). Dann schreibt sie die ursprüngliche URL im HTML um. Das Ergebnis ist statisches HTML; wir **verwenden niemals Hotlinks** zu Google-Bild-CDNs. Bis dahin sind die Bilder bereits vorab gecached.

Respektvoll und reaktionsschnell

Caching sorgt für zwei Dinge: Erstens, dass wir die API-Nutzungslimits von Google **respektieren** und Google Drive wirklich als Frontend für Bearbeitung und Dateiverwaltung nutzen (wofür das Tool gedacht ist), anstatt kostenlosen Bandbreiten- und Speicherplatz zu „leechen“. Es hält die Interaktion unserer Website mit den Google APIs auf das absolut notwendige Minimum, um den Cache bei Bedarf zu aktualisieren.

Der andere Vorteil ist einer, den die Benutzer unserer Website genießen werden: eine **reaktionsschnelle** Website mit minimalen Ladezeiten. Da gecachte Google Docs als statisches HTML auf unserem eigenen Server gespeichert werden, können wir sie sofort abrufen, ohne auf den Abschluss einer externen REST-Anfrage warten zu müssen, und so die Ladezeiten der Website minimieren.

Einhüllung in Express

Da all diese Basteleien im serverseitigen Node.js stattgefunden haben, benötigen wir eine Möglichkeit, damit unsere Client-Seiten mit den APIs interagieren können. Indem wir die `DriveAPI` in einen eigenen REST-Service einpacken, können wir einen Vermittler-/Proxy-Service erstellen, der die gesamte Logik des Caching/Abrufens neuer Versionen abstrahiert und dabei die sensiblen Authentifizierungsdaten sicher auf der Serverseite behält.

Eine Reihe von `express`-Routen oder das Äquivalent in Ihrem bevorzugten Webserver erledigt die Aufgabe, mit einer Reihe von Routen wie dieser.

const driveAPI = new (require('./driveAPI'))();
const express = require('express');
const API_VERSION = 1;
const router = express.Router();

router.route('/getDoc')
.get((req, res) => {
  console.log('GET /getDoc', req.query.id);
  driveAPI.getDoc(req.query.id)
  .then(data => res.json(data))
  .catch(error => {
    console.error(error);
    res.sendStatus(500);
  });
});

// Other routes included here (getSheet, getImage, listFiles, etc)...

app.use(`/api/v${API_VERSION}`, router);

Sehen Sie die vollständige Express.js-Datei im Begleit-Repository.

Bonus: Docker-Deployment

Für die Produktionsbereitstellung können wir den Express-Server neben Ihrem bestehenden statischen Webserver ausführen. Oder, wenn es praktisch ist, könnten wir ihn leicht in ein Docker-Image einhüllen.

FROM node:8
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm@5+)
COPY package*.json ./
RUN npm install
# If you are building your code for production
# RUN npm ci --only=production
# Bundle app source
COPY . .
CMD [ "node", "express.js" ]

…oder dieses vorgefertigte Image auf Docker Hub verwenden.

Bonus 2: NGINX Google OAuth

Wenn Ihre Website öffentlich zugänglich ist (für jeden im Internet erreichbar), sind wir fertig! Aber für unsere Zwecke bei Motorola veröffentlichen wir eine rein interne Dokumentationsseite, die zusätzliche Sicherheit erfordert. Das bedeutet, dass die Linkfreigabe für alle unsere Google Docs **deaktiviert** ist (sie waren auch zufällig in einem isolierten und dedizierten Google Team Drive gespeichert, getrennt von allen anderen Unternehmensinhalten).

Diese zusätzliche Sicherheitsebene haben wir so früh wie möglich auf der **Serverebene** gehandhabt, indem wir NGINX verwendet haben, um alle Anfragen abzufangen und reverse zu proxyen, bevor sie den Express-Server oder statische Inhalte, die von der Website gehostet werden, überhaupt erreichen. Dafür verwenden wir das exzellente Docker-Image von Cloudflare, um allen Mitarbeitern, die auf Website-Ressourcen oder Endpunkte zugreifen (sowohl auf den Express-Server der Drive API als auch auf die statischen Inhalte daneben), einen Google-Anmeldebildschirm anzuzeigen. Es integriert sich nahtlos mit dem Unternehmens-Google-Konto und dem Single-Sign-On, auf das sie bereits Zugriff haben – kein zusätzliches Konto erforderlich!

Fazit

Alles, was wir gerade in diesem Artikel behandelt haben, ist genau das, was wir bei uns gemacht haben. Es ist eine leichtgewichtige, flexible und dezentrale Content-Management-Architektur, bei der die Rohdaten dort leben, wo Google Drive ist, wo unser Team bereits arbeitet, mit einer Benutzeroberfläche, die jedem bereits vertraut ist. Alles wird in das Frontend der Website integriert, das die volle Flexibilität von reinem HTML und CSS in Bezug auf die Darstellung und mit minimalen architektonischen Einschränkungen beibehält. Ein wenig zusätzliche Arbeit von Ihnen, dem Entwickler, schafft eine nahezu nahtlose Erfahrung sowohl für Ihre nicht-technischen Mitarbeiter als auch für Ihre Endbenutzer.

Wird so etwas für jeden funktionieren? Natürlich nicht. Verschiedene Websites haben unterschiedliche Bedürfnisse. Aber wenn ich eine Liste von Anwendungsfällen aufstellen würde, wann Google Drive als CMS verwendet werden kann, sähe sie etwa so aus:

  • Eine interne Website mit ein paar hundert bis ein paar tausend täglichen Benutzern – Wäre dies die Titelseite der globalen Unternehmenswebsite gewesen, könnte selbst eine einzelne Anfrage nach Metadaten zur Dateiversion pro Benutzer dieses Drive API-Nutzungslimits erreichen. Weitere Techniken könnten helfen, dies zu mildern – aber es ist am besten für kleine bis mittelgroße Websites geeignet.
  • Eine Single-Page-Anwendung – Dieses Setup hat es uns ermöglicht, die Versionsnummern aller Datenquellen in einer einzigen REST-Anfrage abzufragen, einmal **pro Sitzung**, anstatt einmal **pro Seite**. Eine Nicht-Single-Page-Anwendung könnte denselben Ansatz verfolgen, vielleicht sogar Cookies oder lokalen Speicher verwenden, um dieselbe „einmal pro Besuch“-Versionsabfrage zu erreichen, aber auch hier wäre etwas zusätzliche Arbeit erforderlich.
  • Ein Team, das bereits Google Drive verwendet – Vielleicht am wichtigsten ist, dass unsere Mitarbeiter angenehm überrascht waren, dass sie zur Website über ein Konto und einen Workflow beitragen konnten, auf die sie bereits Zugriff hatten und mit denen sie vertraut waren, einschließlich aller Verfeinerungen von Googles WYSIWYG-Erlebnis, leistungsstarkem Zugriffsmanagement und dem Rest.