Schon einige Male in den vergangenen Jahren habe ich versucht, meinen Blog von WordPress auf Hugo umzuziehen. Immer gab es dabei für mich unüberwindbare Hürden. Jetzt endlich hat es mal in einem neuen Anlauf funktioniert.

Hier berichte ich davon, wie genau ich das gemacht habe, was es zu bedenken galt und wie es im Ergebnis geworden ist. In erster Linie ist es für mich als Dokumentation gedacht, damit ich das in x Zeit noch nachvollziehen kann. Und wenn jemand anderes damit auch noch etwas anfangen kann, dann ist es noch besser.

Dabei schreibe ich übrigens das erste Mal einen Blogbeitrag mit einem einfachen Editor, offline, ohne Browser, in einem einfachen Textbearbeitungsprogramm. Und das fühlt sich gut an.

Wieso überhaupt weg von WordPress?

Ich habe WordPress seit 2009 für meinen Blog benutzt und es immer auf eigenen Servern selbst gehostet. Ich war immer sehr zufrieden damit. WordPress hat es mir ermöglicht, mich immer nur auf den Inhalt konzentrieren zu können und hat mir die ganze Arbeit drumherum abgenommen. Und es ist immer noch so. Das schätze ich an dem Content Management System WordPress. Jedes Upgrade auf eine neue Version war immer fehlerfrei mit nur wenigen Klicks möglich.

Die meisten Funktionen von WordPress habe ich nie benutzt. Und mit dem Update auf den Gutenberg-Editor und jetzt kürzlich auf 5.9 mit dem Super Feature “Full Site Editing” ist noch klarer geworden, dass ich solch ein komplexes System nicht benötige. Es ist immer noch sehr einfach zu bedienen und zu aktualisieren, aber ich brauche es eben nicht.

Was ich brauche, ist eine einfache Möglichkeit, Texte zu schreiben, die Maus möglichst nicht bewegen zu müssen und außerdem benötige ich nur die grundlegensten Formatierungen. Kurzum das, was Markdown bietet.

Natürlich sind da noch andere Aspekte:

  • Wartbarkeit: Webserver, PHP, Datenbank, alles will gepflegt werden und benötigt ab und zu Aufmerksamkeit. Mit Hugo kann man den kompletten Blog, der aus statischen Dateien (HTML, CSS, JavaScript) besteht, auf einen Webserver werfen und für immer dort liegen lassen.
  • Portierbarkeit/Backup: Sollte ich mal vom Rad fallen und nicht mehr aufstehen, kann man sich das Archiv herunterladen und hat weiterhin Zugriff auf die Inhalte des Blogs. Entsprechend wird man sich später das einmal alle x Zeit aktualisierte Archiv herunterladen können. Dazu später mehr.
  • Sicherheit: Da nichts dynamisch ausgeführt wird, gibt es auch keine Sicherheitslücken, die jemand ausnutzen könnte.
  • “Einfachheit”: Text-Editor, Hugo laufen lassen, hochladen per SFTP. Fertig.

Fenster eines Texteditors unter KDE/Plasma
Fenster eines Texteditors unter KDE/Plasma

Ein schönes Theme für Hugo finden

Man kann natürlich auch selbst ein Theme bauen, aber das ist nichts für mich.

Eine Liste von aktuell 285 Themes gibt es auf der Webseite von Hugo, siehe hier.

Ich habe mich für das Theme Bootstrap Theme for Personal Blog and Documentations entschieden, weil es fast alles liefert, was ich benötige und vielfältige Einstellungsmöglichkeiten hat.

Was ich besonders mag:

  • Dark Mode/Light Mode
  • Suchfunktion (fuse.js)
  • Durch Ausblenden der Seitenliste kann der Inhaltsbereich breiter gemacht werden.
  • Es gibt bereits Shortcodes für Nachrichtenboxen (Info, Alarm, …). Siehe hier.
  • Twitter Cards und Open Graph
  • Responsive
  • optional mehrsprachig
  • Archivseiten
  • Shortcods für Verleinern von Bildern. Die verkleinerten Bilder werden beim Rendern von Hugo aus den Originaldateien erzeugt.
  • Lazy loading für Bilder
  • Inhaltsverzeichnis für Blogbeiträge
  • Keine Einbindung von externen Inhalten
  • Liste von “ähnlichen Beiträgen”

Vor dem Export in WordPress aufräumen

Tabellen aufräumen

Vor dem Export der Daten kann man den Blog noch etwas aufräumen. Hat man z. B. wie ich über die Jahre viele verschiedene Plugins ausprobiert und wieder entfernt, dann könnten die Daten der Plugins immer noch irgendwo in den Tabellen der Datenbank liegen und beim Export in den Daten landen. Das wären alles zusätzliche Daten, die man nicht benötigt.

Dazu hatte ich mal einen eigenen Blogbeitrag verfasst, siehe hier.

Entwürfe

Werden Entwürfe exportiert, dann erhalten diese in der exportierten Version kein Datum und sind somit erstmal aus dem Sichtbereich raus oder bekommen seltsame Namen/URLs zugewiesen wie z. B. “?p=52696/”, das der fortlaufenden ID von Blogbeiträgen entspricht. Das will man später alles nicht einzeln bearbeiten müssen. Deshalb Entwürfe löschen oder veröffentlichen.

Verlinkungen in WordPress

Man kann in WordPress entweder die komplette URL verwenden (Standard), um auf einen Beitrag im eigenen Blog zu verlinken oder die ID des Beitrags verwenden.

Die komplette URL wäre z. B. “natenom.de/2021/01/beitrag-ueber-den-kleinen-elefanten” während die ID “natenom.de/?p=12345” wäre.

Solche Verlinkungen sollte man noch im laufenden WordPress Blog finden und in die erste Form (lange URL) überführen, da die zweite Form mit der ID in Hugo nicht funktionieren wird.

Dazu reicht die Suchfunktion im Adminpanel aus. Man sucht nach “/?p=”. In meinem Fall fanden sich ca. 130 solcher Beiträge mit ID Verlinkungen.

(optional) Backup des Blogs – Für Hugo nicht relevant

Da der Blog am Ende abgeschaltet werden soll, habe ich ein letztes Mal Backups gemacht:

  • Backup der WordPress Datenbank
  • Als Admin in WordPress angemeldet und unter Werkzeuge -> Daten exportieren -> Alle Inhalte ausgewählt und die xml-Datei heruntergeladen. In dieser Datei sind alle Beiträge, alle Seiten und auch die Kommentare von Benutzern enthalten. Nicht jedoch die hochgeladenen Dateien. In meinem Fall ist die Datei um 50 MiB groß und hat um die 50000 Zeilen.
  • Backup des kompletten WordPress-Verzeichnisses inklusive den hochgeladenen Dateien und dem ganzen PHP-Zeug
  • Herunterladen der kompletten Webseite mit wget, um später noch nachvollziehen zu können, unter welchen URLs was zu finden war. Für solche URLs, die Hugo nicht generiert. Die könnte man dann mit einem Webserver passend weiterleiten. Die Kommandozeile dafür ist:
wget --recursive --domains=natenom.de --html-extension --page-requisites --convert-links --no-parent "/" -R "*xmlrpc*" --reject-regex ".*wp-content.*"
Update

Wenn es Probleme gibt und eine Meldung in der Form no follow attribute found, dann benötigt man zusätzlich noch diesen Parameter:

-erobots=off

Das Verzeichnis wp-content habe ich bewusst ausgelassen, da es nur hochgeladene Dateien enthält (ca. 7 GiB), die bereits in einem anderen Backup enthalten sind.

Export Tool von WordPress. Exportiert alles außer der hochgeladenen Dateien.

Daten für Hugo aus WordPress exportieren

Mit dem Tool WordPress to Hugo Exporter kann man alle Seiten und Beiträg eines Blogs in eine Zip-Datei exportieren.

Ich gehe hier nicht darauf ein, wie man das Tool verwendet, da das ausreichend gut auf dessen Webseite erklärt ist. Wichtig ist hier nur, dass man am Ende eine Datei erhält, hier im Beispiel mit dem Namen wpexport.zip.

Auf der Projektseite steht, was man an der PHP-Datei anpassen muss, damit auch Kommentare exportiert werden.

Der Export kann eine ganze Weile dauern und sollte deshalb auf der Kommandozeile ausgeführt werden. Die wpexport.zip meines Blogs hatte eine Dateigröße von etwas mehr als 7 GiB.

Die Datei wpexport.zip hat diese Struktur:

seite-a/index.md
seite-b/index.md
irgendeine-seite/index.md
posts/
    2022-01-05-mein-beitrag-im-januar
    2022-01-06-mein-beitrag-ueber-bluemeleinchen
    2022-02-08-ein-neuer-beitrag-mit-tollen-fahrraedern
wp-content/
    uploads/
        2022/
            01/
                2022-01-04-hochgeladenes-bild-1.jpg
                2022-01-04-hochgeladenes-bild-2.jpg
config.yaml
  • Es sind alle “Seiten” des Blogs als Verzeichnisse enthalten, in den index.md-Dateien sind die Inhalte.
  • Die “Beiträge” (Englisch “Posts”) liegen als md-Dateien im Verzeichnis “posts”.
  • Die hochgeladenen Dateien wie Fotos, Screenshots oder auch Audio und Video liegen im Verzeichnis wp-content/uploads, sortiert nach Jahr und Monat.
  • Dann gibt es noch die Datei config.yaml, welche die URL des Blogs enthält, den Namen und die Beschreibung.

Die Daten lädt man dann auf den eigenen PC herunter.

Eine Webseite mit Hugo anlegen und Theme einfügen

Ich will hier nicht darauf eingehen, wie man eine Webseite mit Hugo anlegt und das Theme einfügt, das ist auf der Projektseite bereits ausführlich gut dokumentiert, siehe hier.

Sobald das erledigt und das Theme eingebunden ist, kann man die exportierten Daten aus dem heruntergeladenen Archiv entpacken und die Daten in das Verzeichnis des neuen Blogs hineinkopieren:

  • Die Datei config.yaml habe ich weg gelassen.
  • Das Verzeichnis posts wird nach content kopiert.
  • Das Verzeichnisse seite-x werden nach content/pages kopiert.
  • Das Verzeichnis wp-content wird nach static kopiert.

Ein bisschen Konfiguration

Eine Liste aller Einstellungsmöglichkeiten gibt es auf der Seite des Theme-Entwicklers.

Taxonomie

Bei der Taxonomie wollte ich die URLs von WordPress möglichst erhalten.

Per Voreinstellung verwendet Hugo für tags in der URL “tags”, WordPress jedoch “tag”. Bei Kategorien verwendet Hugo “categories” und WordPress “category”.

Das kann man in der config.toml anpassen mit:

[taxonomies]
  category = 'category'
  tag = 'tag'

In meinem ausgewählten Theme funktioniert das jedoch leider nicht, da zwingend “tags” und “categories” notwendig sind. Kann man sicher anpassen, ich aber nicht. Also bleibt es bei den Voreinstellungen. Ich werde es dann einfach mit dem Webserver weiterleiten lassen.

In Nginx z. B. mit:

rewrite ^/tag/(.*)$ /tags/$1 redirect;
rewrite ^/category/(.*)$ /categories/$1 redirect;

Wenn das gut funktioniert, kann man das später auf permanent umschreiben.

Beiträge nicht gelistet

Auf der Startseite des Blogs wurden die Beiträge nicht gelistet. Mit Herumprobieren konnte ich herausfinden, dass das an dem Eintrag “type: post” im Front Matter (Metadaten eines Beitrags) lag. Daher habe ich mit dem folgenden Shell-Aufruf diese Zeile in allen md-Dateien gelöscht:

sed -i "/^type: post$/d" *.md

Sonstige Konfiguration

Hier noch ein paar weitere wichtige Dinge, die ich in der Konfiguration eingestellt habe.

[]
baseURL = "/"
rssLimit = 15
[]
[params]
comment = false
breadcrumb = true
palette = "blue"
dateFormat = "Mon, 02 Jan 2006 15:04"
mainSections = ["posts", "pages" ]
poweredBy = false
math = false
diagram = false
logo = false
description = " Verkehrswende, Fahrrad, CriticalMass, OpenBikeSensor, SimRa, Mumble, Open Source, Minimalist, OpenStreetMap, Müllsammeln"
recentPostCount = 5
relatedPostCount = 6
categoryCount = 200
tagCount = 150
searchBar = true
post.copyright = false
search.fuse.threshold = 0.0
countTaxonomyPosts = true
post.excerpt = "description"
viewer = false
showShare = false
socialShare = false

[outputs]
  home = ["HTML", "RSS", "JSON"]

Von absoluten URLs zu relativen URLs

Ich habe keine Ahnung von SEO und habe mich nie damit beschäftigt. Könnte es sein, dass WordPress aus SEO-Gründen absoulte URLs verwendet und nicht realtive? Ich habe dazu die Info auf einer Webseite gefunden, dass relative Links problematisch werden könnten, wenn der Server nicht richtig eingerichtet sei.

Hat man Bedenken, dann weiter zum nächsten Abschnitt.

Aber: Will man den neuen Blog auf dem eigenen PC anschauen, benötigt man ohne die Veränderungen der URLs eine Internetverbindung, damit eingebettete Inhalte wie z. B. Bilder vom Browser von der ursprünglichen Webseite heruntergeladen werden können. Oder aber man biegt das DNS temporär um, muss dann aber zumindest aus https http machen, und so weiter.

Absolute URLs auf hochgeladene Dateien

Alle Verlinkungen auf Fotos, Bilder und Videos sind in WordPress mit der absoulten URL angegeben, in meinem Fall mit natenom.de/wp-content/uploads/.... Und das wird so auch in die md-Dateien exportiert.

Deshalb kann man die URLs mit dem Streameditor sed umschreiben lassen:

sed -i -E -e 's#/wp-content/uploads/#/wp-content/uploads/#g' *.md

(Ich habe das aktuell erstmal noch nicht getan.)

Das Kommando führt man in allen Verzeichnissen aus, in denen md-Dateien liegen. Man könnte das auch so umschreiben, dass es ausgehend vom neuen Blogverzeichnis alle md-Dateien findet und so alle erfasst. Aber so reicht es mir.

Absolute URLs auf Blogbeiträge

Das selbe gilt auch für Verlinkungen im Blogbeiträgen auf andere Beiträg im eigenen Blog. Auch diese URLs lasse ich umschreiben:

sed -i -E -e 's#/#/#g' *.md

(Ich habe das aktuell erstmal noch nicht getan.)

Aus dem absoulten Link / wird dadurch /.

Umlaute in URLs von Kategorien und Tags

Ich verwende im Blog das Tag “Müllsammeln” und die Kategorie “Mobilität”. Wordpress hat dabei automatisch die Umlaute in den dafür genutzten URLs in die alternativen Schreibweisen “muellsammeln” und “mobilitaet” umgewandelt. Hugo macht das nicht, weshalb dort das Tag auch “müllsammeln” lautet.

Es gibt mehrere Lösungen:

  1. Es dabei belassen, denn mittlerweile sind diese Zeichen kein Problem mehr. Es sieht nur nicht sonderlich schön aus, wenn man statt mobilität das hier sieht mobilit%C3%A4t
  2. Ohne Veränderungen an den Beiträgen wäre die Option removePathAccents von Hugo möglich, die aus “mobilität” “mobilitat” macht.
  3. Hugo selbst zu verändern, siehe hier.
  4. Alle Vorkommen der Tags in den Beiträgen mit sed umschreiben und ö durch oe ersetzen usw.
  5. Weiterleitungen durch den Webserver, siehe unten.

Einen Issue mit vielen Informationen zu diesem Theme gibt es auf Github, siehe hier. Ein weitere Issue zu dem Thema ist seit November 2021 noch offen, siehe hier.

Ich habe mich entschieden, das so zu belassen das erst einmal mit Weiterleitungen zu “fixen” und zu schauen, was sich bei dem Issue tut.

In Nginx sind diese:

rewrite ^/tag/muellsammeln/(.*)$ /tags/müllsammeln/$1 redirect;
rewrite ^/category/mobilitaet/(.*)$ /categories/mobilität/$1 redirect;
rewrite ^/tag/(.*)$ /tags/$1 redirect;
rewrite ^/category/(.*)$ /categories/$1 redirect;

Ich nutze bei solchen Dingen am Anfang immer erst redirect, was dem HTTP Status 302 entspricht und ändere das dann irgendwann zu permanent (Status 301) um.

Fehlerseite für 404

Falls doch mal eine Seite nicht gefunden wird, habe ich die 404.html in Nginx eingerichtet:

error_page 404 /404.html;

Hier kann man die Seite mal ansehen: Dieser Link zeigt auf eine nicht vorhandene Seite.

Damit erstmal keine Inhalte fehlen

Würde man jetzt schon Hugo starten, würden vermutlich viele Inhalte in verschiedenen Beiträgen fehlen. Das liegt daran, dass das oben genannte Export-Tool viele HTML Tags nicht in Markdown konvertieren kann und dann an diesen Stellen den HTML-Quelltext in die exportierten md-Dateien schreibt. Doch Hugo filtert diesen HTML-Quelltext beim Rendern der Website heraus.

Damit ich nicht erst alle 2560 Blogbeiträge manuell überprüfen und korrigieren muss, bevor ich den neuen Blog öffentlich machen kann, habe ich mich entschieden, dieses Verhalten von Hugo umzustellen und Hugo anzuweisen, den HTML-Quelltext nicht herauszufiltern. Das ist zwar nicht im Sinne des Erfinders aber so habe ich die Möglichkeit, die Arbeit auf irgendwann später zu verschieben.

Details dazu findet man hier. Suche nach unsafe.

In die config.toml fügt man dazu Folgendes ein:

[markup]
  [markup.goldmark]
    [markup.goldmark.renderer]
      unsafe = true

Run Hugo, Run

Jetzt kann man Hugo veranlassen, den neuen Blog das erste Mal zu rendern und dann lokal zur Verfügung zu stellen. Per Voreinstellung ist er unter localhost:1313 erreichbar.

Dazu wechselt man auf der Kommandozeile in das Verzeichnis des neuen Blogs und führt aus:

hugo server --renderToDisk -D -E -F -v

Jetzt werden alle gerenderten Dateien und auch die aus dem static-Verzeichnis in das Verzeichnis public kopiert, welches Hugo automatisch erstellt, wenn es noch nicht existiert.

Sobald alles bereit ist, gibt es eine Meldung wie etwa:

                   |  DE   
-------------------+-------
  Pages            | 4350  
  Paginator pages  | 1694  
  Non-page files   |    1  
  Static files     |   88  
  Processed images |    0  
  Aliases          |  889  
  Sitemaps         |    1  
  Cleaned          |    0  

Built in 45228 ms
Watching for changes in /home/natenom-web-hugo/{assets,content,layouts,static,themes}
Watching for config changes in /home/natenom-web-hugo/config.toml, []
Environment: "development"
Serving pages from /home/natenom-web-hugo/public
Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender
Web Server is available at http://localhost:1313/ (bind address 127.0.0.1)
Press Ctrl+C to stop

Erstmal fertig

Das reicht dann auch wieder für diesen Beitrag. Man kann jetzt schon im eigenen Blog offline umgucken und wird hier und da noch unschöne Dinge finden, wie falsch dargestellte Bilder.

In Teil 2 geht es darum, für die Übergangszeit das übrig gebliebene HTML, das nicht zu Markdown konvertiert werden konnte, automatisiert so zu verändern, dass der Blog trotzdem ansehnlich ist, wenn auch noch nicht perfekt.