Migration des Blogs von Drupal auf Hugo

Vorgeschichte

Meine persönliche Webseite hatte ich seit etwa 2010 mit dem CMS Drupal realisiert, zunächst mit der Version 6, dann ab etwa 2012 mit der Version 7. Es war klar, dass irgendwann ein Umstieg auf eine neuere Version von Drupal anstehen würde, auch wenn das für Ende des Jahres angekündigte Ende des Supports von Drupal 7 inzwischen um ein Jahr verschoben wurde. Da schon die Umstellung von Drupal 6 auf Drupal 7 recht aufwändig war, wollte ich mir die Umstellung auf die aktuelle Drupal-Version nicht antun. Drupal war seinerzeit sicher die beste Wahl, aber es war immer einen Tick zu komplex für eine einfache private Homepage. Selbst für eine Vereinsseite wie veloentet.fr, die ich erstellt habe und administriere, ist Drupal überdimensioniert und hat in diesem Segment eher Exotenstatus gegenüber WordPress.

Als Alternative stand der Umstieg auf ein anderes CMS, allen voran WordPress, im Raum. Da ich die Seite vorrangig der technischen Herausforderung und Spielerei wegen betreibe, erschien mir dieser Weg zu einfach (“WP kann ja heutzutage jeder”). Irgendwo hatte ich auch schon von statischen Webseiten gelesen, mich aber noch nicht näher damit beschäftigt. Zufällig bin ich über https://bikerouter.de/ auf den Blog des Betreibers dieser Seite gestoßen und auf den Beitrag Blog-Relaunch mit Hugo. Angeregt durch diesen Bericht über die Migration einer Wordpress-Seite zu einer mit Hugo erzeugten statischen Seite habe ich mich über Hugo näher informiert und die Migration meiner Seite in Angriff genommen.

Zusammengefasst:

  • Der eigentliche Inhalt, wie Texte, Fotos, angehängte Dateien, konnte nahezu komplett per Skript überführt werden.
  • Existierende Verweise von fremden Seiten konnten durch geeignete Rewrite-Regeln funktionsfähig erhalten bleiben.
  • Für die Struktur der Reiseberichte (Drupal-Books) habe ich keine Out-of-the-box-Lösung gefunden. Eine Lösung konnte mit mässigen Anpassungen der Templates realisiert werden. Das könnte zu Problemen bei Updates von Hugo oder des Designs führen.
  • Die Migration der interaktiven Karten, anfangs als möglicherweise unmöglich und daher für die Migration blockierend eingestuft, erwies sich als weit weniger aufwändig als befürchtet.
  • Das gewählte Design unterstützt keine mehrstufigen Menüs. Das kann mit Änderungen am Design gelöst werden, würde aber aber wohl zu einer endgültigen Abkopplung von Updates des Designs führen bzw. einen Umstieg auf ein anderes Design sehr aufwändig machen.

Bilder und Dateien

Bilder und andere in den Beiträgen verwendete Dateien habe ich mit unveränderter Verzeichnisstruktur vom Drupal-Verzeichnis /files in das Unterverzeichnis /static/files im Hugo-Root-Verzeichnis kopiert. Sie werden von dort nach /files in das Home-Verzeichnis der Webseite übernommen. Damit sollten eigentlich Links von externen Seiten auf solche Bilder und Dateien weiterhin funktionieren, aber leider funktionierte das erst mal nicht. Das lag daran, dass Drupal Links in der Form http://blog.velocarte66.fr/sites/blog.velocarte66.fr/files//imagepicker/1/2013-08-24_1.jpg erzeugt hatte. Zum Glück ließ sich das einfach mit einer Rewrite-Regel lösen:

RewriteRule ^(.*?)(sites\/blog\.velocarte66\.fr\/)(.*)$ \/$3 [L,R=301]

Reiseberichte / Drupal-Books

Mit dem Drupal-Modul Books können Inhalte hierarchisch gegliedert werden. Dies habe ich für Reiseberichte meiner Radtouren genutzt. Für jede Reise gibt es eine Übersichtsseite und eine Detailseite pro Etappe. Die Übersichtsseite verweist auf die Etappenseiten, zwischen den Etappenseiten kann vor und zurück navigiert werden. Für diese Struktur habe ich keine adäquate Losung in Hugo gefunden, mit keinem der in Betracht gezogenen Themes. Ein Hinweis in diesem Beitrag auf dem Hugo-Forum hat mir den Weg zur Lösung gewiesen. Diese basiert auf Hugo-Page-Bundles:

  • eine neue Section trips für Reiseberichte
  • unter /trips ein Verzeichnis pro Reisebericht (branch), darin:
    • eine Datei _index.md mit dem Inhalt der Übersichtsseite,
    • ein Unterverzeichnis für jede Etappe (leaf), darin eine Datei index.md mit dem Inhalt der Etappenseite

Standardmäßig rendert Hugo für die Branches eines Bundles nur eine Liste der zugehörigen Leafs. Damit auch der Inhalt aus der _index.md gerendert wird, habe ich ein eigenes Template trips/list.html erstellt. Außerdem waren Änderungen an einigen Partials notwendig, u.a. damit in den Listen nur die Übersichtsseite, nicht aber die Etappenseiten aufgeführt werden.

      content/trips/fruhjahrstour-2015-sudwestfrankreich/
      ├── _index.md
      ├── logo.jpg
      ├── albi-castres
      │   └── index.html
      ├── brengues-villefranche-de-rouergue
      │   └── index.md
      ├── cahors-brengues
      │   └── index.html
      ├── castelnaudary-deyme
      │   └── index.md
      ├── castres-saint-martin-lalande
      │   └── index.md
      ├── clone-of-castelnaudary-deyme
      │   └── index.md
      ├── deyme-montech
      │   └── index.md
      ├── montech-cahors
      │   └── index.md
      └── villefranche-de-rouergue-albi
          └── index.html
  
Dateistruktur eines Reiseberichts

Interaktive Karten

In den Reiseberichten werden interaktive Karten verwendet, auf denen die gefahrene Strecke angezeigt. Hierfür habe ich unter Drupal das Modul OpenLayers verwendet. Die Tracks der Strecken sind als GPX-Datei vorhanden. Da ich aus anderen Projekten über Erfahrung mit Openlayers verfüge, habe ich auf dieser Basis ein Javascript-Skript erstellt, welches Tracks und Wegpunkte mit OpenLayers auf einer Karte anzeigt, die mit Shortcodes in eine Hugo-Seite integriert werden kann. Details habe ich in einem eigenen Post beschrieben.

Mehrsprachigkeit

Hugo und das verwendete Theme unterstützen Mehrsprachigkeit. Nicht-deutsche, in meinem Fall also französischer Inhalt, wird mit dem Sprachcode (fr) zwischen Dateiname und Extension versehen, z.B. index.fr.md.

Inhalts- und Verzeichnisstruktur

Drupal-Artikel habe ich nach in die Sektion posts übernommen, Reiseberichte nach trips, alle anderen Inhalte nach pages. Damit ergibt sich diese Dateistruktur:

   .
   ├── assets
   │   └── scss
   ├── content
   │   ├── about
   │   ├── pages
   │   ├── posts
   │   └── trips
   ├── static
   │   ├── css
   │   ├── files
   │   ├── icons
   │   ├── images
   │   ├── js
   │   └── tracks
   └── themes
       ├── meme
       └── my-theme
   
Dateistruktur des Hugo-Quellverzeichnisses

Verweise von fremden Seiten funktionieren wegen der teilweise geänderten Inhaltstypen/Sektionen nicht mehr. Außerdem ist in Drupal die Sprachversion im URL der Seite enthalten, bei Hugo ist das nicht der Fall. Das habe ich Rewrite-Regeln dieser Form gelöst:

RewriteRule ^(.*?)(de\/story\/)(.*)$ \/posts\/$3 [L,R=301]
RewriteRule ^(.*?)(de\/tour\/)(.*)$ \/pages\/$3 [L,R=301]

Menüs

Die alte Seite hatte zweistufige Dropdown-Menüs.Das verwendete Hugo-Theme bietet nur ein einstufiges Menü an. Das hat sich als ausreichen erwiesen. IMHO wird die Bedeutung von Menüs auf Webseiten überschätzt. Vermutlich hangelt sich kaum jemand über zwei oder gar mehr Ebenen durch das Menü. Um Inhalte zu finden, wird vielmehr die Suchfunktion der Seite oder eine Suchmaschine genutzt.

Automatisierte Übernahme des Drupal-Contents

Dafür habe ich ein Python-Skript geschrieben, welches den Content der Drupal-Nodes aus der Datenbank extrahiert und in eine Hugo-Content-Datei konvertiert. Der DB-Zugriff erfolgte mittels des Python-Pakets mysql-connector:

query = "SELECT body_format, body_summary, body_value FROM drupalfield_data_body "\
        + "WHERE entity_id=%d" %(nid)
cursorFields.execute(query)

Für einfache HTML- oder Markdown-Seiten mussten dann nur noch die URLs an die Hugo-Dateistruktur angepasst werden. Im wesentlichen war das mit einigen re.sub-Aufrufen machbar und bot die Gelegenheit, meine Regex-Kenntnisse mal wieder aufzufrischen.

Aufwändiger war die Übernahmen von Content, für den selbst erstellte Drupal-Inhaltstypen verwendet wurden. Bei diesen Inhaltstypen wurden unter Drupal der HTML-/Markdown-Code für die Bilder anhand einer Field-Collection-Liste automatisch generiert. Dieser Vorgang musste bei der Migration nachgebildet werden:

query_field = "SELECT uri, field_image_alt, field_image_title, field_image_width, field_image_height "\
                  + "FROM drupalfield_data_field_image as fi, drupalfile_managed as fm "\
                  + "where fi.entity_id=%d and fm.fid=fi.field_image_fid order by delta"
cursorFields.execute(query_field % (nid))
# Erzeuge <figure>-Element aus dem Ergebnis der SQL-Abfrage 

Das Ergebnis der Umwandlung habe ich stichprobenartig durch Augenschein der erzeugten Seiten überprüft. Neben Formatierungs-Fehlern waren Links eine der häufigsten Fehlerursachen. Hier erwies sich das Tool LinkChecker als sehr nützlich:

linkchecker -F html http://localhost:1313/

Formatierung mit CSS

Damit die übernommenen Inhalte mit dem Hugo-Design möglichst so dargestellt wurden, wie unter Drupal, war etwas CSS notwendig. Dieses wurde in assets/scss/custom/_custom.scss abgelegt.

Abschliessende Arbeiten

Nach einigen Iterationen war das Ergebnis des Migrationsskripts so gut, dass nur noch einige wenige Fälle von Hand nachgearbeitet werden mussten. Meist ließ sich das mit einer sed-Substitution erledigen, z.B:

grep -RiIl '<strong>' content| xargs sed -i 's/<strong>/<br><strong>/g'