staging.inyokaproject.org

23. Februar 2022

PeerTube 4.1 das Opensource Videoportal ist veröffentlicht worden.

 

Das ist an sich eine sehr tolle Nachricht. Auch dass manche Funktionen standardmäßig ausgeschaltet werden können. Wie zum Beispiel das P2P Protokoll, das die Bandbreitenlast vom anbietenden Server zwischen den KonsumentInnen aufteilen kann.

 

Was an sich eine sehr solidarische Funktion ist, ist im Gegenzug natürlich auch ein Privatsphärenproblem. Darüber sollten wir anfangen eine gesellschaftliche Diskussion zu führen, ohne dass die großen Konzerne in Altherren und -damen Manier “hörthört” grunzen.

 

Zuallererst sei vorangestellt: Privatssphäre ist wichtig! Aber darauf folgt ein sehr dickes ABER. Viele der extremen PrivatsphärenverferchterInnen blockieren mit ihrem Verhalten eine Ausbreitung des Schutzes der Privatsphäre!

 

Die Zwei Finanzierungs-Aspekte leiten mich zu dieser Aussage

  1. Die Person die Geld investiert, um diesen Service zur Verfügung zu stellen
  2. Die Person, die die Inhalte zur Verfügung stellt.

 

Wir alle haben es gerne und dankend angenommen, dass “Internet” und alle damit zusammengehörigen Dienste für die Allgemeinheit wenig bis gar nichts kostet (kostenlos). Dafür haben Konzerne neue Wege gefunden “kostenlos” so gewinnbringend zu vermarkten, dass selbst die Hölle vor Neid erblasst.

 

Übersetzt heißt das “kostenlos gewinnbringend": wir bezahlen dafür.

 

Wir bezahlen dafür vielleicht nicht direkt mit Geld, aber indirekt schon mit Geld. Es gibt genügend Beispiele die das beweisen (Schufa, Cambridge Analytica, uvm).

 

Dienste die von irgendjemand bereit gestellt werden kosten Geld. Ob diese Dienste ein laufendes System sind, oder ob diese Dienste Ergebnisse von Recherchen, oder Zusammenfassungen sind, die uns interessieren. Wir haben ein Interesse solche Dienste in Anspruch zu nehmen und es wäre extrem egozentrisch, sich auf den Standpunkt zu stellen, dass man einen kostenlosen Anspruch darauf hätte!

 

Im Gegenzug darf man sich aber auch nicht beschweren, wenn bei ausbleibendem Support der Anbieter sich eine Finanzierungsquelle sucht. Egal ob es um die Erhaltung des Dienstes, oder eine Gewinnerzielungsabsicht trägt. Eine Diskussion darum ist eine reine Neiddebatte, die es nicht gäbe, wenn wir so einen Dienst überhaupt nicht wollten.

 

Daher mein Aufruf an alle PrivatspährennutzerInnen: Wenn ihr Dienste in Anspruch nehmt, die die Privatspähre schützen wollen, dann sehr euch auch in der Pflicht etwas dafür zu tun. Das heißt nicht immer Geld. Aber das heisst IMMER bezahlen.

Das umschließt auch die sogenannten Datenkraken, bei denen ebenso von ContentanbieterInnen konsumiert wird. Dort ist das Zahlungsmittel Likes, Abos und vor allem Kommentare. Der Algorithmus ist das Transaktionssystem!

 

Wenn ihr als PrivatspährennutzerInnen also den Menschen nicht schaden wollt, die für euch wichtige oder interessanten Dienste oder Content zur Verfügung stellen, dann seid fair und beraubt sie nicht, um das finanzielle Fundament, dass das erst möglich macht!

 

#FairShare

Hier der Link zur Veröffentlichung: https://joinpeertube.org/en_US/news#release-4.1

 

 

 

22. Februar 2022

Di, 22. Februar 2022, Lioh Möller

Nach über zwei Jahren Entwicklungszeit konnte Tomas Matejicek eine neue Version der Live-Distribution Slax veröffentlichen.

Ursprünglich basierte Slax, wie der Name nahelegen kann, auf Slackware. 2017 entschied sich der Entwickler jedoch auf Debian GNU/Linux als Basis zu wechseln.

Slax ist primär zur Nutzung als Live-System geeignet, unterscheidet sich jedoch von vielen anderen Distribution dadurch, dass mittels aufs eine Persistenz realisiert wurde.

Letzteres erwies sich bei der Portierung auf die aktuelle Debian 11 Version (Codename Bullseye) allerdings als grössere Hürde, da Debian standardmässig nur noch overlayfs unterstützt.

Erste Versuche mit overlayfs zeigten jedoch, dass ich damit nicht ohne weiteres der von Slax gewohnte Komfort, wie das automatische Applizieren von Änderungen, realisieren lies.

Dies bewog Tomas dazu einen eigenen angepassten Kernel mit aufs Unterstützung zu verwenden.

Darüber hinaus wird Chromium nicht mehr standardmässig ausgeliefert, lässt sich allerdings weiterhin mit wenigen Klicks installieren.

Zur Netzwerkverwaltung kommt neu connman statt wicd zum Einsatz und scite ist der Standard-Texteditor.

Changelog: https://www.slax.org/changelog.php

Di, 22. Februar 2022, Lioh Möller

Die Slackware Distribution unterstützt schon seit einiger Zeit die Installation auf EFI-basierten Systemen mithilfe des Installationsprogramms. Die Einrichtung einer verschlüsselten Partition ist allerdings eine manuelle Vorbereitung notwendig.

Nach dem Start von dem Installationsmedium in UEFI-Modus sollte zunächst das Tastaturlayout definiert werden.


In der Konsole kann daraufhin die Festplatte partitioniert werden. Im Folgenden gehen wir davon aus, dass die gesamte Festplatte für die Slackware Installation zur Verfügung steht.

Zur Erstellung des Partitionslayouts kann cfdisk verwendet werden, welches initial mit der Option -z gestartet werden sollte. Die Device-Nodes müssen dabei an die lokalen Gegebenheiten angepasst werden.

cfdisk -z /dev/sda

Als Partitionslabel sollte gpt gewählt werden.

Zunächst wird eine unverschlüsselte EFI Boot-Partition mit einer empfohlenen Grösse von 512MB erstellt.

Im verbleibenden Speicherplatz wird eine Partition des Typs Linux LVM angelegt.

Nach dem Speichern der Änderungen kann cfdisk verlassen werden.

Nun erfolgt die eigentliche LUKS-Einrichtung.

Zunächst wird die Partition entsprechend formatiert. Während des Systemstarts wird standardmässig das US-Tastaturlayout verwendet. Daher sollte zu diesem Zeitpunkt ein Passwort verwendet werden, welches auch mit diesem Layout gut zu nutzen ist.

Alternativ kann eine angepasste initrd mithilfe von mkinitrd und der Angabe des Parameters -l KeyboardLayout erstellt werden. Beispiel -l sg-latin1 für Schweizerdeutsch.

cryptsetup luksFormat /dev/sda2

Dies muss durch die Eingabe von YES bestätigt werden

Daraufhin lässt sich das Volumen wie folgt öffnen:

cryptsetup open /dev/sda2 slackware

In diesem können nun die Logical Volumes erstellt werden. Üblicherweise wird dort zunächst ein Physical Volume definiert.

pvcreate /dev/mapper/slackware

Daraufhin kann eine Volume Group angelegt werden:

vgcreate slackwarevg /dev/mapper/slackware

In dieser lassen sich die eigentlichen Logical Volumes erstellen. Dabei wird in diesem Beispiel eine SWAP Partition von 4G Grösse angelegt und der verbleibende Speicherplatz als Root-Partition genutzt:

lvcreate -L 4G slackwarevg -n swap
lvcreate -l 100%FREE slackwarevg -n root


Mit mkswap kann der SWAP Speicher aktiviert werden und mittels mkfs.ext4 das Root-Logical-Volume formatiert werden.

mkswap -L swap /dev/mapper/slackwarevg-swap
mkfs.ext4 /dev/mapper/slackwarvg-root

Abschliessen kann die eigentliche Installation von Slackware mithilfe des setup Befehls gestartet werden.

Es ist dabei wichtig zu beachten, dass das richtige Volumen als Root Partition ausgewählt wird (/dev/mapper/slackwarevg-root) und elilo statt lilo installiert wird.

Offizielle Anleitung: http://ftp.riken.jp/Linux/slackware/slackware-15.0/README_CRYPT.TXT

übersetzt die wichtigsten -Mathe-Notationen in HTML. Es ist dabei schneller als MathJax, hat im Gegenzug aber noch nicht (sooooo) viele Notationen umgesetzt wie der Platzhirsch. Zudem unterscheidet sich die Anwendung in Blogdown und HUGO.

KaTeX in HUGO einbinden

In meinem Themeordner erstelle ich die Datei /MEINTHEME/layouts/partials/katex.html und gebe ihr folgenden Inhalt:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css" integrity="sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ" crossorigin="anonymous">

<!-- The loading of KaTeX is deferred to speed up page rendering -->
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.js" integrity="sha384-VQ8d8WVFw0yHhCk5E8I86oOhv48xLpnDZx5T9GogA/Y84DcCKWXDmSDfn13bzFZY" crossorigin="anonymous"></script>

<!-- To automatically render math in text elements, include the auto-render extension: -->
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/contrib/auto-render.min.js" integrity="sha384-+XBljXPPiv+OzfbB3cVmLHf4hdUFHlWNZN5spNQ7rmHTXpd7WvJum6fIACpNNfIR" crossorigin="anonymous" onload="renderMathInElement(document.body);"></script>

<script>
    document.addEventListener("DOMContentLoaded", function() {
        renderMathInElement(document.body, {
            delimiters: [
                {left: "$$", right: "$$", display: true},
                {left: "$", right: "$", display: false}
            ]
        });
    });
</script>

Ihr solltet nachschauen, ob die Version 0.15.2 immer noch die aktuellste ist und ggfs. die Zeilen entsprechend anpassen.

In der Datei /MEINTHEME/layouts/partials/head.html ergänze ich:

 {{ if .Params.math }}{{ partial "katex.html" . }}{{ end }}

Wenn ich später in Blogdown bei einem Post im Meta-Kopf folgenden Paramter setze:

 math: true

…dann wird KaTeX geladen und kann verwendet werden. Die Informationen hierzu hab ich in Mert Bakirs Blog gefunden.


Beispiele

Die Anwendung unterscheidet sich, je nachdem, ob .md, .Rmarkdown oder .Rmd-Files verwendet werden.



.md

In einfachen Markdowndokumenten funktioniert die Inline-Verwendung mittels Dollarzeichen:

 Dies ist ein Text mit $E=m\cdot c^2$ 

Das ergibt:

Dies ist ein Text mit

Das funktioniert auch mit Sonderzeichen:

 Wir verwenden $\LaTeX \rightarrow \KaTeX$ und das klappt ganz gut

Das ergibt:

Wir verwenden und das klappt ganz gut

Mathe-Umgebungen werden mit zwei Dollarzeichen eingerahmt:

$$
E=m\cdot c^2
$$ 

Das ergibt:

Mit dem align-Environment werden die Formelzeilen automatisch am rechten Rand durchnummeriert. Environments müssen nicht per Dollarzeichen eingeführt werden sondern funktioniert direkt im Text :

Dies ist eine Formel

\begin{align}
E=m\cdot c^2
\end{align}

und ich finde sie gut

Das ergibt:

Dies ist eine Formel \begin{align} E=mc^2 \end{align} und ich finde sie gut

So funktioniert auch das equation-Environment direkt im Text:

Einstein hat mal gesagt:

\begin{equation}
E=m\cdot c^2
\end{equation}

und ich glaube, er hatte Recht

Das ergibt:

Einstein hat mal gesagt:

\begin{equation} E=mc^2 \end{equation}

und ich glaube, er hatte Recht

Eine Übersicht aller unterstützer Environments findet ihr hier.



.Rmarkdown

In .Rmarkdown-Files ist die Notation leicht anders. So müssen bei der Inline-Verwendung die Dollarzeichen mittels Backslash auskommentiert werden:

 Dies ist ein Text mit \$a^2 + b^2 = c^2\$ 

Das ergibt:

Dies ist ein Text mit

Das gilt auch für alle Sonderzeichen:

 Wir verwenden \$\\LaTeX \\rightarrow \\KaTeX\$ und das klappt ganz gut

Das ergibt:

Wir verwenden und das klappt ganz gut

Mathe-Umgebungen werden wie gewohnt mit zwei Dollarzeichen eingerahmt.

$$
a^2 + b^2 = c^2
$$ 

Das ergibt:

Auch hier müssen die Backslashs von Sonderzeichen auskommentiert werden:

$$
\\LaTeX \\rightarrow \\LaTeX
$$ 

Das ergibt:

In .Rmarkdown-Files müssen bei allen Environments und Sonderzeichen die \ von \begin{} und \end{} auskommentiert werden, damit sie direkt im Text funktionieren. Für das align-Environment sieht das dann so aus:

Irgendwas mit Dreiecken
\\begin{align}
a^2 + b^2 = c^2
\\end{align}
Was war das noch?

Das ergibt:

Irgendwas mit Dreiecken \begin{align} a^2 + b^2 = c^2 \end{align} Was war das noch?


Genau so funktioniert auch das equation-Environment:

Pythagoras hat mal gesagt:

\\begin{equation}
a^2 + b^2 = c^2
\\end{equation}

und ich glaube, er hatte Recht

Das ergibt:

Pythagoras hat mal gesagt:

\begin{equation} a^2 + b^2 = c^2 \end{equation}

und ich glaube, er hatte Recht

Eine Übersicht aller unterstützer Environments findet ihr hier.



.Rmd

In .Rmd-Dokumenten funktioniert die Inline-Verwendung wieder mittels Dollarzeichen:

 Dies ist ein Text mit $\Reals \backsim 2^\N$ 

Das ergibt:

Dies ist ein Text mit

Das funktioniert auch mit Sonderzeichen:

 Wir verwenden $\LaTeX \rightarrow \KaTeX$ und das klappt ganz gut

Das ergibt:

Wir verwenden und das klappt ganz gut

Mathe-Umgebungen werden mit zwei Dollarzeichen eingerahmt:

$$
\Reals \backsim 2^\N
$$ 

Das ergibt:

Mit dem align-Environment werden die Formelzeilen automatisch am rechten Rand durchnummeriert. Environments müssen nicht mit Dollarzeichen eingerahmt werden, sondern funktionieren direkt im Text:

Was war das noch für eine Formel
\begin{align}
\Reals \backsim 2^\N
\end{align}
Und was bedeutet sie?

Das ergibt:

Was war das noch für eine Formel \begin{align} \mathbb{R} \backsim 2^N \end{align} Und was bedeutet sie?

Das equation-Environment funktioniert genau so:

Cantor hat mal gesagt:

\begin{equation}
\Reals \backsim 2^\N
\end{equation}

und ich glaube, er hatte Recht

Das ergibt:

Cantor hat mal gesagt: und ich glaube, er hatte Recht

Eine Übersicht aller unterstützer Environments findet ihr hier.



Allgemein

Zeilenumbrüche funktionieren per \newline, aber nicht mit \\:

\begin{align}
 E &= m \cdot c^2 \newline
 a^2 + b^2 &= c^2 \newline
 \Reals &\backsim 2^\N
\end{align}

Das ergibt:

Das Prozentzeichen muss mit zwei Backslashs auskommentiert werden:

$$
14\\% + 15\\% = 29\\%
$$

Das ergibt:

Links

übersetzt die wichtigsten -Mathe-Notationen in HTML. Es ist dabei schneller als MathJax, hat im Gegenzug aber noch nicht (sooooo) viele Notationen umgesetzt wie der Platzhirsch. Zudem unterscheidet sich die Anwendung in Blogdown und HUGO.

KaTeX in HUGO einbinden

In meinem Themeordner erstelle ich die Datei /MEINTHEME/layouts/partials/katex.html und gebe ihr folgenden Inhalt:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css" integrity="sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ" crossorigin="anonymous">

<!-- The loading of KaTeX is deferred to speed up page rendering -->
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.js" integrity="sha384-VQ8d8WVFw0yHhCk5E8I86oOhv48xLpnDZx5T9GogA/Y84DcCKWXDmSDfn13bzFZY" crossorigin="anonymous"></script>

<!-- To automatically render math in text elements, include the auto-render extension: -->
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/contrib/auto-render.min.js" integrity="sha384-+XBljXPPiv+OzfbB3cVmLHf4hdUFHlWNZN5spNQ7rmHTXpd7WvJum6fIACpNNfIR" crossorigin="anonymous" onload="renderMathInElement(document.body);"></script>

<script>
    document.addEventListener("DOMContentLoaded", function() {
        renderMathInElement(document.body, {
            delimiters: [
                {left: "$$", right: "$$", display: true},
                {left: "$", right: "$", display: false}
            ]
        });
    });
</script>

Ihr solltet nachschauen, ob die Version 0.15.2 immer noch die aktuellste ist und ggfs. die Zeilen entsprechend anpassen.

In der Datei /MEINTHEME/layouts/partials/head.html ergänze ich:

 {{ if .Params.math }}{{ partial "katex.html" . }}{{ end }}

Wenn ich später in Blogdown bei einem Post im Meta-Kopf folgenden Paramter setze:

 math: true

…dann wird KaTeX geladen und kann verwendet werden. Die Informationen hierzu hab ich in Mert Bakirs Blog gefunden.


Beispiele

Die Anwendung unterscheidet sich, je nachdem, ob .md, .Rmarkdown oder .Rmd-Files verwendet werden.



.md

In einfachen Markdowndokumenten funktioniert die Inline-Verwendung mittels Dollarzeichen:

 Dies ist ein Text mit $E=m\cdot c^2$ 

Das ergibt:

Dies ist ein Text mit

Das funktioniert auch mit Sonderzeichen:

 Wir verwenden $\LaTeX \rightarrow \KaTeX$ und das klappt ganz gut

Das ergibt:

Wir verwenden und das klappt ganz gut

Mathe-Umgebungen werden mit zwei Dollarzeichen eingerahmt:

$$
E=m\cdot c^2
$$ 

Das ergibt:

Mit dem align-Environment werden die Formelzeilen automatisch am rechten Rand durchnummeriert. Environments müssen nicht per Dollarzeichen eingeführt werden sondern funktioniert direkt im Text :

Dies ist eine Formel

\begin{align}
E=m\cdot c^2
\end{align}

und ich finde sie gut

Das ergibt:

Dies ist eine Formel \begin{align} E=mc^2 \end{align} und ich finde sie gut

So funktioniert auch das equation-Environment direkt im Text:

Einstein hat mal gesagt:

\begin{equation}
E=m\cdot c^2
\end{equation}

und ich glaube, er hatte Recht

Das ergibt:

Einstein hat mal gesagt:

\begin{equation} E=mc^2 \end{equation}

und ich glaube, er hatte Recht

Eine Übersicht aller unterstützer Environments findet ihr hier.



.Rmarkdown

In .Rmarkdown-Files ist die Notation leicht anders. So müssen bei der Inline-Verwendung die Dollarzeichen mittels Backslash auskommentiert werden:

 Dies ist ein Text mit \$a^2 + b^2 = c^2\$ 

Das ergibt:

Dies ist ein Text mit

Das gilt auch für alle Sonderzeichen:

 Wir verwenden \$\\LaTeX \\rightarrow \\KaTeX\$ und das klappt ganz gut

Das ergibt:

Wir verwenden und das klappt ganz gut

Mathe-Umgebungen werden wie gewohnt mit zwei Dollarzeichen eingerahmt.

$$
a^2 + b^2 = c^2
$$ 

Das ergibt:

Auch hier müssen die Backslashs von Sonderzeichen auskommentiert werden:

$$
\\LaTeX \\rightarrow \\LaTeX
$$ 

Das ergibt:

In .Rmarkdown-Files müssen bei allen Environments und Sonderzeichen die \ von \begin{} und \end{} auskommentiert werden, damit sie direkt im Text funktionieren. Für das align-Environment sieht das dann so aus:

Irgendwas mit Dreiecken
\\begin{align}
a^2 + b^2 = c^2
\\end{align}
Was war das noch?

Das ergibt:

Irgendwas mit Dreiecken \begin{align} a^2 + b^2 = c^2 \end{align} Was war das noch?


Genau so funktioniert auch das equation-Environment:

Pythagoras hat mal gesagt:

\\begin{equation}
a^2 + b^2 = c^2
\\end{equation}

und ich glaube, er hatte Recht

Das ergibt:

Pythagoras hat mal gesagt:

\begin{equation} a^2 + b^2 = c^2 \end{equation}

und ich glaube, er hatte Recht

Eine Übersicht aller unterstützer Environments findet ihr hier.



.Rmd

In .Rmd-Dokumenten funktioniert die Inline-Verwendung wieder mittels Dollarzeichen:

 Dies ist ein Text mit $\Reals \backsim 2^\N$ 

Das ergibt:

Dies ist ein Text mit

Das funktioniert auch mit Sonderzeichen:

 Wir verwenden $\LaTeX \rightarrow \KaTeX$ und das klappt ganz gut

Das ergibt:

Wir verwenden und das klappt ganz gut

Mathe-Umgebungen werden mit zwei Dollarzeichen eingerahmt:

$$
\Reals \backsim 2^\N
$$ 

Das ergibt:

Mit dem align-Environment werden die Formelzeilen automatisch am rechten Rand durchnummeriert. Environments müssen nicht mit Dollarzeichen eingerahmt werden, sondern funktionieren direkt im Text:

Was war das noch für eine Formel
\begin{align}
\Reals \backsim 2^\N
\end{align}
Und was bedeutet sie?

Das ergibt:

Was war das noch für eine Formel \begin{align} \mathbb{R} \backsim 2^N \end{align} Und was bedeutet sie?

Das equation-Environment funktioniert genau so:

Cantor hat mal gesagt:

\begin{equation}
\Reals \backsim 2^\N
\end{equation}

und ich glaube, er hatte Recht

Das ergibt:

Cantor hat mal gesagt: und ich glaube, er hatte Recht

Eine Übersicht aller unterstützer Environments findet ihr hier.



Allgemein

Zeilenumbrüche funktionieren per \newline, aber nicht mit \\:

\begin{align}
 E &= m \cdot c^2 \newline
 a^2 + b^2 &= c^2 \newline
 \Reals &\backsim 2^\N
\end{align}

Das ergibt:

Das Prozentzeichen muss mit zwei Backslashs auskommentiert werden:

$$
14\\% + 15\\% = 29\\%
$$

Das ergibt:

Links

21. Februar 2022

Die MZLA Technologies Corporation hat mit Thunderbird 91.6.1 ein Update für seinen Open Source E-Mail-Client veröffentlicht.

Neuerungen von Thunderbird 91.6.1

Mit dem Update auf Thunderbird 91.6.1 hat die MZLA Technologies Corporation ein Update außer der Reihe für seinen Open Source E-Mail-Client veröffentlicht und behebt damit mehrere Fehler der Vorgängerversion. Diese lassen sich in den Release Notes (engl.) nachlesen. Dies schließt auch eine geschlossene Sicherheitslücke ein.

Der Beitrag Thunderbird 91.6.1 veröffentlicht erschien zuerst auf soeren-hentzschel.at.

In Teil 1 dieser Artikelserie habe ich mein Ansinnen ausführlich beschrieben. Dieser Teil widmet sich der Entwicklung einer Ansible-Rolle zum Deployment des Nextcloud-Apache-Container-Images.

In den folgenden Abschnitten beschreibe ich die Einrichtung eines Python Virtual Environments, die Installation von Ansible in dem zuvor erstellten Environment und die Installation der Ansible-Collection containers.podman, bevor ich mich abschließend der eigentlichen Ansible-Rolle widme.

Python Virtual Environments für Ansible

Zur Einrichtung habe ich mich an den englischsprachigen Artikel „How to set up and use Python virtual environments for Ansible“ von Gineesh Madapparambath gehalten. Die notwendigen Schritte werden hier kurz und bündig dokumentiert.

[t14s ~]$ python3 --version
Python 3.9.7

[t14s ~]$ mkdir python-venv
[t14s ~]$ cd !$
cd python-venv

[t14s python-venv]$ python3 -m venv ansible-core2.x
[t14s python-venv]$ source ansible-core2.x/bin/activate
(ansible-core2.x) [jkastning@t14s python-venv]$ python3 -m pip install --upgrade pip
Requirement already satisfied: pip in ./ansible-core2.x/lib/python3.9/site-packages (21.0.1)
Collecting pip
  Downloading pip-21.3.1-py3-none-any.whl (1.7 MB)
     |████████████████████████████████| 1.7 MB 2.3 MB/s 
Installing collected packages: pip
  Attempting uninstall: pip
    Found existing installation: pip 21.0.1
    Uninstalling pip-21.0.1:
      Successfully uninstalled pip-21.0.1
Successfully installed pip-21.3.1

(ansible-core2.x) [t14s python-venv]$ python3 -m pip install ansible-core
Collecting ansible-core
[...]

(ansible-core2.x) [t14s python-venv]$ ansible --version
ansible [core 2.11.6] 
  config file = None
  configured module search path = ['/home/tronde/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/tronde/python-venv/ansible-core2.x/lib64/python3.9/site-packages/ansible
  ansible collection location = /home/tronde/.ansible/collections:/usr/share/ansible/collections
  executable location = /home/tronde/python-venv/ansible-core2.x/bin/ansible
  python version = 3.9.7 (default, Aug 30 2021, 00:00:00) [GCC 11.2.1 20210728 (Red Hat 11.2.1-1)]
  jinja version = 3.0.2
  libyaml = True

Damit ist die Installation von ansible-core abgeschlossen. Im folgenden Code-Block wird geprüft, ob Ansible sich grundsätzlich mit dem Zielsystem verbinden und dort einen Python-Interpreter identifizieren kann.

(ansible-core2.x) [t14s python-venv]$ ansible -i hosts --private-key ~/.ssh/ansible_id_rsa -m ping example.com
example.com | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

Installation der Ansible-Collection containers.podman

Um Podman auf dem Zielsystem konfigurieren zu können, wird die genannte Ansible-Collection benötigt, welche mit folgendem Befehl installiert werden kann. Der Code-Block zeigt zusätzlich die Ausgabe während der Installation.

(ansible-core2.x) [t14s ansible-core2.x]$ ansible-galaxy collection install containers.podman
Starting galaxy collection install process
Process install dependency map
Starting collection install process
Downloading https://galaxy.ansible.com/download/containers-podman-1.8.2.tar.gz to /home/tronde/.ansible/tmp/ansible-local-8729oh0om8w3/tmp7tv2yrae/containers-podman-1.8.2-9rw3fd1y
Installing 'containers.podman:1.8.2' to '/home/tronde/.ansible/collections/ansible_collections/containers/podman'
containers.podman:1.8.2 was installed successfully

Ansible-Rolle: Deployment von Nextcloud und MariaDB als Pod

Nextcloud benötigt für den Betrieb eine Datenbank. Hierfür könnte man eine integrierte SQLite nutzen. Dies wird jedoch nur für kleine Umgebungen empfohlen. Während der Entstehung dieses Artikels wird MariaDB als Datenbank-Backend vom Nextlcoud-Projekt empfohlen. Daher habe ich mich entschieden, das Nextcloud-Image zusammen mit einem MariaDB-Container zu deployen. Dazu greife ich auf die beiden folgenden Container-Repositorien zurück:

Das Grundgerüst bzw. die Verzeichnisstruktur für die Ansible-Rolle wurde erstellt mit:

$ ansible-galaxy role init --offline ansible_role_deploy_nextcloud_with_mariadb_pod

Die aktuelle Version der Ansible-Rolle ist auf GitHub zu finden. Ich werde ihre Bestandteile hier im Einzelnen vorstellen.

Die Variablen in defaults/main.yml

In der Datei defaults/main.yml habe ich Standardwerte für Variablen definiert, die geeignet sind, eine funktionsfähige Nextcloud-Instanz zu initialisieren. Die Bezeichner der Variablen sind dabei der Dokumentation der verwendeten Container-Repositorien entnommen.

In Zeile 4-7 und 10 werden die Namen für Podman-Volumes definiert, welche die persistent zu speichernden Daten aufnehmen werden.

     1	---
     2	# defaults file for ansible_role_deploy_nextcloud_with_mariadb_pod
     3	# Podman volumes for Nextcloud
     4	NC_HTML: nc_html
     5	NC_APPS: nc_apps
     6	NC_CONFIG: nc_config
     7	NC_DATA: nc_data
     8	
     9	# Podman volume for MariaDB
    10	MYSQL_DATA: mysql_data

Die Zeilen 13-17 definieren Variablen für die MariaDB-Instanz, wie z.B. Namen der Datenbank, Benutzername und Passwörter für diese Datenbank und den DB-Host. Diese werden neben dem MariaDB-Container auch von dem Nextcloud-Container benötigt, um eine Verbindung zur Datenbank herstellen zu können.

    12	# MySQL/MariaDB vars
    13	MYSQL_DATABASE: nc_db
    14	MYSQL_USER: nextcloud
    15	MYSQL_PASSWORD: ToPSeCrEt2021!
    16	MYSQL_ROOT_PASSWORD: ToPSeCrEt2021!
    17	MYSQL_HOST: 127.0.0.1
    18	
    19	# Vars for MariaDB container
    20	MARIADB_CONMON_PIDFILE: /tmp/mariadb_conmon.pid
    21	MARIADB_IMAGE: docker.io/library/mariadb:10.5.7
    22	MARIADB_NAME: nc_mariadb

Zeile 20-22 definiert Variablen, die für den MariaDB-Container benötigt werden. Hier wird z.B. die Version des Container-Images (MARIADB_IMAGE) und ein Name für die Container-Instanz (MARIADB_NAME) festgelegt.

Die folgenden Zeilen widmen sich den Variablen für den Nextcloud-Container. Dort werden in den Zeilen 25 u. 26 Benutzername und Passwort für den Nextcloud-Admin definiert, gefolgt von einigen Variablen, welche bei Nutzung eines Reverse-Proxy benötigt werden und SMTP-Variablen, welche der Nextcloud den Mailversand ermöglichen.

    24	# Nextcloud vars
    25	NEXTCLOUD_ADMIN_USER: nc_admin
    26	NEXTCLOUD_ADMIN_PASSWORD: VSnfD2021!
    27	NEXTCLOUD_OVERWRITEPROTOCOL: ""
    28	NEXTCLOUD_OVERWRITECLIURL: ""
    29	NEXTCLOUD_TRUSTED_DOMAINS: ""
    30	
    31	# SMTP vars
    32	SMTP_HOST: smtp.example.com
    33	SMTP_SECURE: tls # ssl to use SSL, or tls zu use STARTTLS
    34	SMTP_PORT: 587 # (25, 465 for SSL, 587 for STARTTLS)
    35	SMTP_AUTHTYPE: LOGIN
    36	SMTP_NAME: bob@example.com
    37	SMTP_PASSWORD: MailSecret1!
    38	MAIL_FROM_ADDRESS: no-reply@example.com
    39	MAIL_DOMAIN: "@example.com"

Bei den SMTP-Variablen handelt es sich um Beispiel-Werte. Diese müssen an die konkrete Umgebung angepasst werden.

Es folgen nun noch ein paar Variablen, welche dem Pod und dem Nextcloud-Container einen Namen geben, sowie die Version des zu verwendenden Nextcloud-Container-Images festlegen.

    41	# Vars for podman-pod(1)
    42	POD_NAME: nc_pod
    43	POD_PORT: 127.0.0.1:40231:80
    44	POD_INFRA_CONMON_PIDFILE: /tmp/nc_pod_infra.pid
    45	
    46	# Vars for Nextcloud container
    47	NC_CONMON_PIDFILE: /tmp/nc_conmon.pid
    48	NC_IMAGE: docker.io/library/nextcloud:23-apache
    49	NC_NAME: nextcloud

Durch POD_PORT: 127.0.0.1:40231:80 wird definiert, dass der Port 40231 an das Loopback-Interface gebunden und mit Port 80 des Pods verknüpft wird. Mit dieser Einstellung ist die Nextcloud-Instanz nur von dem Host aus erreichbar, auf dem sie ausgebracht wurde. Möchte man sie auch von anderen Hosts aus erreichbar machen, kann man entweder den Teil mit 127.0.0.1: weglassen oder einen Reverse-Proxy wie z.B. NGINX verwenden. Ich empfehle an dieser Stelle letzteres.

Hinweis: In defauts/main.yml stehen Passwörter im Klartext. Diese sind mit der Veröffentlichung der Ansible-Rolle allgemein bekannt und sollten gegen solche ersetzt werden, die geheimgehalten werden. Dies kann z.B. geschehen, in dem man die entsprechenden Variablen in vars/main.yml oder host_vars/hostname neu definiert. Es bietet sich an, diese zusätzlich mit Ansible-Vault zu verschlüsseln.

Die Tasks in tasks/main.yml

Im vorstehenden Abschnitt wurden die Variablen definiert, welche für die nun folgenden Tasks benötigt werden. Diese sind in tasks/main.yml definiert und werden im folgenden wieder abschnittsweise erläutert.

     1	---
     2	# tasks file for ansible_role_deploy_nextcloud_with_mariadb_pod
     3	- name: Main folder, needed for updating
     4	  containers.podman.podman_volume:
     5	    state: present
     6	    name: "{{ NC_HTML }}"
     7	    recreate: no
     8	    debug: no
     9	
    10	- name: Volume for installed/modified apps
    11	  containers.podman.podman_volume:
    12	    state: present
    13	    name: "{{ NC_APPS }}"
    14	    recreate: no
    15	    debug: no
    16	
    17	- name: Volume for local configuration
    18	  containers.podman.podman_volume:
    19	    state: present
    20	    name: "{{ NC_CONFIG }}"
    21	    recreate: no
    22	    debug: no
    23	
    24	- name: Volume for the actual data of Nextcloud
    25	  containers.podman.podman_volume:
    26	    state: present
    27	    name: "{{ NC_DATA }}"
    28	    recreate: no
    29	    debug: no
    30	
    31	- name: Volume for the MySQL data files
    32	  containers.podman.podman_volume:
    33	    state: present
    34	    name: "{{ MYSQL_DATA }}"
    35	    recreate: no
    36	    debug: no

Die ersten Zeilen enthalten Tasks, durch welche die Podman-Volumes zur persistenten Datenspeicherung auf dem Zielsystem erstellt werden. Diese Tasks sind, wie für Ansible üblich, deklarativ und idempotent. Existiert ein Volume bereits, liefert der entsprechende Task ein ‚OK‘ zurück, da keine Aktionen erforderlich sind.

Die folgenden Zeilen erstellen den Podman-Pod und fügen ihm einen Nextcloud- sowie einen MariaDB-Container hinzu. Die Dokumentation der verwendeten Module findet sich in Punkt 5 und 6 im Abschnitt Quellen und weiterführende Links.

    38	- name: Create the podman-pod(1)
    39	  containers.podman.podman_pod:
    40	    debug: no
    41	    infra: yes
    42	    infra_conmon_pidfile: "{{ POD_INFRA_CONMON_PIDFILE }}"
    43	    publish: "{{ POD_PORT }}"
    44	    name: "{{ POD_NAME }}"
    45	    state: started
    46	
    47	- name: Create MariaDB container
    48	  containers.podman.podman_container:
    49	    debug: yes
    50	    conmon_pidfile: "{{ MARIADB_CONMON_PIDFILE }}"
    51	    image: "{{ MARIADB_IMAGE }}"
    52	    image_strict: yes
    53	    pod: "{{ POD_NAME }}"
    54	    recreate: yes
    55	    state: started
    56	    name: "{{ MARIADB_NAME }}"
    57	    env:
    58	      MYSQL_USER: "{{ MYSQL_USER }}"
    59	      MYSQL_PASSWORD: "{{ MYSQL_PASSWORD }}"
    60	      MYSQL_ROOT_PASSWORD: "{{ MYSQL_ROOT_PASSWORD }}"
    61	      MYSQL_DATABASE: "{{ MYSQL_DATABASE }}"
    62	    volume: "{{ MYSQL_DATA }}:/var/lib/mysql:Z"
    63	
    64	- name: Wait for DB to initilize
    65	  wait_for:
    66	    timeout: 20
    67	
    68	- name: Create Nextcloud container
    69	  containers.podman.podman_container:
    70	    debug: no 
    71	    conmon_pidfile: "{{ NC_CONMON_PIDFILE }}"
    72	    image: "{{ NC_IMAGE }}"
    73	    image_strict: yes
    74	    pod: "{{ POD_NAME }}"
    75	    recreate: yes
    76	    state: started
    77	    name: "{{ NC_NAME }}"
    78	    env:
    79	      MYSQL_DATABASE: "{{ MYSQL_DATABASE }}"
    80	      MYSQL_USER: "{{ MYSQL_USER }}"
    81	      MYSQL_PASSWORD: "{{ MYSQL_PASSWORD }}"
    82	      MYSQL_HOST: "{{ MYSQL_HOST }}"
    83	      NEXTCLOUD_ADMIN_USER: "{{ NEXTCLOUD_ADMIN_USER }}"
    84	      NEXTCLOUD_ADMIN_PASSWORD: "{{ NEXTCLOUD_ADMIN_PASSWORD }}"
    85	      NEXTCLOUD_TRUSTED_DOMAINS: "{{ NEXTCLOUD_TRUSTED_DOMAINS }}"
    86	      SMTP_HOST: "{{ SMTP_HOST }}"
    87	      SMTP_SECURE: "{{ SMTP_SECURE }}"
    88	      SMTP_PORT: "{{ SMTP_PORT }}"
    89	      SMTP_AUTHTYPE: "{{ SMTP_AUTHTYPE }}"
    90	      SMTP_NAME: "{{ SMTP_NAME }}"
    91	      SMTP_PASSWORD: "{{ SMTP_PASSWORD }}"
    92	      MAIL_FROM_ADDRESS: "{{ MAIL_FROM_ADDRESS }}"
    93	      MAIL_DOMAIN: "{{ MAIL_DOMAIN }}"
    94	      OVERWRITEPROTOCOL: "{{ NEXTCLOUD_OVERWRITEPROTOCOL }}"
    95	      OVERWRITECLIURL: "{{ NEXTCLOUD_OVERWRITECLIURL }}"
    96	    volume:
    97	      - "{{ NC_HTML }}:/var/www/html:Z"
    98	      - "{{ NC_APPS }}:/var/www/html/custom_apps:Z"
    99	      - "{{ NC_CONFIG }}:/var/www/html/config:Z"
   100	      - "{{ NC_DATA }}:/var/www/html/data:Z"

In Zeile 64-66 habe ich einen Task definiert, der einfach nur 20 Sekunden wartet. Dies wurde erforderlich, da ich Laufzeitprobleme feststellen konnte, wenn der Nextcloud-Container startet, bevor die Datenbank im MariaDB-Container initialisiert war. Dieses Konstrukt ist nicht schön und ich bin für Verbesserungsvorschläge offen.

Zwischenfazit

Die Erstellung der Ansible-Rolle hat länger gedauert, als angenommen. Dies liegt nur zum Teil in meiner spärlichen Freizeit begründet. Einen größeren Einfluss darauf hatte die Dokumentation zum Nextcloud-Repository. Diese geht davon aus, dass man ein Dockerfile bzw. Docker-Compose verwendet. So war noch etwas Internet-Recherche erforderlich, um den Pod letztendlich ans Laufen zu bringen.

Dieser Artikel beschäftigte sich mit den Tag-1-Aufgaben, an deren Ende eine Nextcloud-Instanz ausgebracht wurde, welche an einen Reverse-Proxy angebunden werden kann.

Im nächsten Artikel gehe ich auf die Konfiguration des NGINX-Reverse-Proxy ein. Hierbei habe ich einige Überraschungen erlebt, welche mich an der Reife des Projekts [2] zweifeln lassen.

Quellen und weiterführende Links

  1. Nextcloud System Requirements — https://docs.nextcloud.com/server/latest/admin_manual/installation/system_requirements.html
  2. Nextcloud (Official Image) — https://hub.docker.com/_/nextcloud
  3. MariaDB (Official Image) — https://hub.docker.com/_/mariadb
  4. GitHub Tronde/ansible_role_deploy_nextcloud_with_mariadb_pod
  5. podman_pod – Manage Podman pods
  6. podman_container – Manage podman containers

19. Februar 2022

Mozilla bereitet den Start seines nächsten Premium-Angebotes vor. Dieses richtet sich vor allem an Webentwickler. Der Start soll bereits im März sein, auch im deutschsprachigen Raum.

Mozilla ist vor allem für seine kostenlosen Produkte wie den Browser Firefox bekannt. Um den Nutzern zusätzliche Mehrwerte zu bieten, aber auch um die finanzielle Abhängigkeit von Suchmaschinen-Anbietern zu reduzieren, setzt Mozilla vermehrt auch auf kostenpflichtige Premium-Angebote wie das Mozilla VPN oder Firefox Relay Premium. Mozillas nächstes Premium-Angebot richtet sich in erster Linie an Webentwickler.

MDN Plus – Entwickler-Dokumentation mit Zusätzen

Die kostenfreie Entwickler-Dokumentation MDN Web Docs, ehemals Mozilla Developer Network, dürfte vermutlich jedem bekannt sein, der bereits mit dem Thema Webentwicklung in Berührung kam. Immerhin ist dies wohl für viele die Anlaufstelle Nummer Eins, wenn es um Themen wie HTML, CSS und JavaScript geht. Und um das direkt klarzustellen: Die MDN Web Docs werden weiterhin kostenlos bleiben. Allerdings wird Mozilla mit MDN Plus ein kostenpflichtiges Zusatzangebot schaffen.

Das Angebot selbst dürfte nicht jeden überraschen, immerhin testete Mozilla bereits im vergangenen Sommer eine Frühversion von MDN Plus. Das Versprechen: Monatliche Artikel mit besonderem Tiefgang zu bestimmten Themen von Branchen-Experten sowie Features, um mehr für sich persönlich aus den MDN Web Docs herauszuholen. Bereits mit Ende des Experiments kündigte Mozilla an, dass man an einem offiziellen Launch arbeiten wird.

MDN Plus Experiment Ende

Das wissen wir bereits über MDN Plus

Wie meine Recherchen ergeben haben, steht der offizielle Launch von MDN Plus unmittelbar bevor. Bereits ab dem 9. März 2022 dürfte es soweit sein. Zu den Start-Ländern gehören neben Deutschland, Österreich und der Schweiz auch die USA, Kanada, das Vereinigte Königreich, Frankreich, Italien, Spanien, Belgien, die Niederlande, Irland, Malaysia, Neuseeland sowie Singapur.

Neben den bereits erwähnten zusätzlichen Inhalten soll es mehrere Features für Plus-Nutzer geben. Dazu zählt die Möglichkeit, die MDN Web Docs offline nutzen zu können, sich seine persönliche Sammlung anlegen zu können, auf die man von jedem Gerät aus zugreifen kann, sich über Änderungen bestimmter Artikel benachrichten lassen zu können, sowie Themes, um die Plattform ein Stück weit dem eigenen Geschmack anpassen zu können. Wobei nicht unerwähnt bleiben soll, dass man mit seinem Beitrag nicht nur diese zusätzlichen Inhalte und Features bezahlt, sondern eben auch die MDN Web Docs als Plattform damit unterstützt, die von Mozilla seit über 16 Jahren kostenlos bereitgestellt wird.

Der Preis soll nach meinen Informationen bei 10 USD pro Monat respektive 100 USD pro Jahr liegen. Bei uns dürfte die gleichen Zahlen dann vermutlich als Euro-Preise gelten, wenn man von Mozillas bisherigen Premium-Produkten ausgeht.

Der Beitrag Exklusiv: Mozillas nächstes Premium-Angebot richtet sich an Entwickler erschien zuerst auf soeren-hentzschel.at.

Ubuntu 22.04 wirft seine Schatten voraus und die Derivate folgen. Nun nehmen auf die elementary-Entwickler Version 7 in den Blick, das auf Ubuntu 22.04 aufsetzen wird. Ein paar Gedanken dazu.

Bekanntermaßen finde ich die Pantheon Shell eine wirklich tolle Desktopumgebung und das insbesondere seit mit Fedora, Arch Linux und openSUSE (OBS) auch andere Distributionen diese ausliefern. Es ist ganz erstaunlich, was da aus kleinen Anfängen gewachsen ist und im Gegensatz zu anderen GNOME-Sprösslingen haben die Entwickler sich hier gleichermaßen emanzipiert, wie auch ein augenscheinlich gutes Verhältnis zu GNOME bewahrt. Deutlich kritischer ist meine Sicht auf die Distribution elementary OS. Momentan fahre ich noch ein Experiment mit elementary OS 6 und Flatpaks, dessen Ausgang weiterhin offen ist.

In ihren monatlichen Berichten haben die elementary-Entwickler Ende Januar die Verlagerung des Fokus auf elementary OS 7 angekündigt. Das ist einerseits nachvollziehbar, weil man vermutlich eine extrem lange Distanz zwischen Ubuntu-Release und elementary-Release vermeiden möchte. Wir erinnern uns: Beim letzten Mal benötigte man über ein Jahr, um eine neue Version auf neuer Ubuntu-Basis zu veröffentlichen. Andererseits ist Version 6 damit weniger als ein Jahr nach Veröffentlichung schon wieder ein Auslaufmodell. Die Erfahrung lehrt, dass die elementary-Entwickler es nicht hinbekommen, mehrere Stränge parallel zu pflegen. Elementary OS 6 wird somit nur noch die nötigsten Bugfixes erhalten.

Dank der Flatpaks erhalten die Anwender trotzdem noch Updates für die Anwendungen und der Übergang auf die kommende Version dürfte hier weniger steinig sein. Schließlich sind die Flatpaks für Version 6 auch unter Version 7 lauffähig und das App Center bleibt daher gut gefüllt. Vermutlich hat elementary hier früh aus richtige Pferd gesetzt, schließlich will auch Flathub demnächst eine Bezahlfunktion einführen.

Andere Bereiche lassen einen da deutlich mehr zweifeln. Aktuell gibt es keine Upgraderoutine, um von einer elementary-Version auf die Nächte zu aktualisieren. Die Entwickler nehmen diese zwar in den Blick, aber selbst wenn diese – was sie ja nicht versprechen – bis Version 7 kommen sollte, sind Funktionen bei elementary am Anfang selten so ausgereift, dass diese wirklich alltagstauglich sind. Weiterhin verstehe ich nicht, warum man immer noch auf Ubuntu als Basis setzt. Die Sollbruchstellen sind kaum noch zu übersehen. Ubuntu setzt auf Snaps, elementary auf Flatpak. Ubuntu will bei 22.04 noch weitestgehend auf Gtk4-Apps verzichten, elementary möchte vollständig auf Gtk4-Apps wechseln. Man kann sich durchaus fragen, wie lange das noch gut geht.

Licht und Schatten sind wie immer bei elementary ganz nah beieinander.

Der Artikel elementary OS nimmt Version 7 in den Blick erschien zuerst auf [Mer]Curius

18. Februar 2022

Yeah, ich bin zurueck 🎉
Das waren lange 3 Jahre, aber ich glaube es hat sich gelohnt 😊


Angefangen hat alles Anfang 2019 mit Treat Your Blog as Code bei noqqe .

Ich war direkt Feuer und Flamme.

Tooling / Gedanken #

Allerdings alles der Reihe nach, denn Tooling ist ebenfalls wichtig, und Hugo kannte ich damals noch nicht. CI/CD nutzten wir auf Arbeit schon mit GitLab , markdownlint war mir ebenfalls neu.

Statische Generatoren #

Bisher hatte ich meine Website immer nur mit selbst geschriebenen Bashscripts in einem selbst gehosteten Gitrepo verwaltet. Das Bash nicht die beste Loesung ist, war mir bewusst, denn Bilder einpflegen erforderte immer etwas Aufwand und die Laufzeit zum Bauen der Website mit allen Komponenten war auch eher unschoen. Zudem nicht wirklich crossplattform und teilweise buggy …

Klar war mir, dass ich immer noch eine statische Website haben wollte, nur eben mit einem besseren/schnelleren/sauberen “Backend”.

Mein erster Schritt war daher die Suche nach einem geeigneten Generator und ein bisschen auch die Ueberlegung, was ich ueberhaupt noch veraendern oder neu einbinden wollte. Laut diesen Listen gibt es ja so einige Generatoren …

Ins Auge gefasst hatte ich damals konkret:

  • Hugo

    Hugo ist in Go geschrieben und erschien 2013. Ueber Hugo bin ich oefters schon gestolpert, weil darueber einige Leute in den Planeten drueber geschrieben hatten.

  • Nanoc

    Nanoc ist in Ruby geschrieben und erschien ca. 2007 . Nanoc kannte ich vorher noch nicht.

  • Jekyll

    Jekyll setzt auf Ruby und erschien 2008. Jekyll kannte ich schon vorher, da es die treibende Kraft hinter GitHub Pages ist.

Leider weiss ich nicht mehr die konkreten Gruende (evtl. weil Hugo schon einen Webserver mitbringt, was fuer die lokale Entwicklung natuerlich hervorragend ist), aber vermutlich war es die Anzahl an GitHub Sternen , die vielen verschiedenen Themes (darunter auch viele kostenpflichtige ), die angepriesene Schnelligkeit, Flexibilitaet sowie dass Hugo Open Source und Crossplattform ist, dass die Wahl auf Hugo fiel 🎉

Auszeichnungssprachen #

Auszeichnungssprachen gibt es einige (die bekannteste duerfte HTML sein), Hugo unterstuetzt davon auch ein paar und bisher hatte ich meine Beitraege immer in Markdown geschrieben.

  • AsciiDoc

    Wenn ich tatsaechlich doch mal noch mehr will , kann ich auch einfach ein Text in AsciiDoc erstellen. Bisher war das allerdings noch nicht notwendig, da ich alles relevante in Markdown schreiben konnte oder mit Hilfe von Shortcodes .

    AsciiDoc eignet sich eben mehr fuer Leute, die ein Buch schreiben wollen . Evtl. ist es ja sogar ein geeignetes Tool fuer ein Kochbuch.

  • Markdown

    Markdown ist standardmaessig recht limitiert, was z.B. den HTML-Tag sub oder sup angeht. Je nach Parser gibt es aber auch Moeglichkeiten mehr als den Standard zu nutzen. Hugo nutzt z.B. Goldmark , was CommonMark konform ist, und mit dem ich auch Tabellen und Checklisten generieren kann. Fussnoten und Durchgestrichenes sind auch drin.

Da nur die Endung (md vs adoc) letztendlich den Unterschied ausmacht (Front Matter bleibt gleich), bleibe ich erstmal bei Markdown.
Umwandeln kann ich ja Markdown in AsciiDoc mit z.B. Pandoc recht schnell.

Front Matter / Config #

Mein Front Matter , also ein paar Zeilen, die z.B. den Titel und die Tags angeben, war bisher “eigen” (1. Zeile = Titel, 2. Zeile = kommaseparierte Liste mit Tags). Bei Hugo gibt es hauptsaechlich 3 Moeglichkeiten (die Unterschiede sind in diesem Vergleich schoen zu sehen):

  • JSON

    “JavaScript Object Notation” mag ich persoenlich nicht so gerne, nutze sie aber zwangsweise dauernd (API als Stichwort).

  • TOML

    “Tom’s Obvious, Minimal Language” kannte ich vorher noch nicht wirklich, aber die Syntax ist ja auch sehr simpel.

  • YAML

    “YAML Ain’t Markup Language” nutze ich schon recht haeufig auf Arbeit (z.B. docker-compose.yaml oder Kubernetes), daher bin ich damit schon vertraut.

Ausser JSON ist mir die Nutzung recht egal, und so habe ich mich fuer YAML im Front Matter entschieden (weil --- schoener aussieht und leichter zu tippen ist als +++) und fuer TOML in meinen Hugo Konfigurationsdateien .

Stylesheet languages #

Mit “Cascading Style Sheets” (CSS ) wird eine Website erst so richtig bunt und attraktiv. Allerdings gibt es nicht nur das reine CSS, sondern auch Varianten, die u.a. Variablen und Funktionen nutzen und dann in CSS umgewandelt werden koennen.

Wikipedia listet einige auf, angeschaut habe ich mir:

  • Less

    “Leaner Style Sheets” gibt es seit 2009 und ich hatte schon zumindest mal davon gehoert. Ein Vorteil von Less ist, dass es eine Obermenge von CSS ist, d.h. CSS-Code ist gleichzeitig auch gueltiger Less-Code.

  • Sass

    “Syntactically awesome style sheets” gibt es seit 2007 und war mir auch schon gelaeufig. Neben Sass gibt es auch noch “Sassy CSS” (SCSS), welches statt Einrueckungen die CSS Formatierung nutzt.

  • Stylus

    Stylus kannte ich vorher noch nicht, gibt es aber schon seit 2010. Statt der Nutzung von geschweiften Klammern wird hier auf Einrueckung gesetzt, Zeichen wie :, ; und , sind optional und koennen weggelassen werden.

Die Entscheidung zur Nutzung von SCSS fiel mir nicht schwer, da Hugo schon Support fuer Sass/SCSS mitbringt, was bei Less/Stylus (noch?) nicht der Fall ist .
Zudem finde ich die Nutzung von geschweiften Klammern wie bei CSS ueblich schoener als Einrueckungen 😉

Hosting und Konfiguration #

Irgendwo muss die Website ja auch laufen. Bisher teilte ich mir einen Server bei Hetzner mit Flo . Allerdings wollte ich mir mal einen eigenen Server (VPS reicht) goennen und auch dort moeglichst viel nach dem Prinzip von Infrastructure as code (IaC) umsetzen.

Zudem sollte nicht nur die Website dort laufen, sondern u.a. auch Git, Seafile , newsboat , weechat , neomutt , …
Und natuerlich alles unter Arch Linux mit root-Zugriff. Daher fielen direkt schonmal einige Hostingideen raus, die ich hier aber trotzdem mal aufliste:

  • AWS , GCP , Azure

    Ist zwar bestimmt spannend zu nutzen um mal tiefer in das Thema Kubernetes einzusteigen, aber etwas Overkill fuer meine kleine Website. Fuer AWS gibt es Arch Linux Images .

  • Schokokeks

    Kein root moeglich, aber generell empfehlenswert.

  • Uberspace

    Ebenfalls kein root moeglich, aber ebenfalls empfehlenswert.

  • Netlify

    Kein root, kein Arch Linux, kein ssh. Aber generell sinnvoll fuer Menschen, die nur ihre Website online bringen wollen. Auch erwaehenswert: Netlify CMS und Front Matter

  • Netcup

    Bieten Arch Linux nativ an 🎉

Kurz: ich habe mich fuer Netcup entschieden, einfach weil ich mit denen schon gute Erfahrungen beruflich (und auch vor Jahren privat) gesammelt habe und sie Arch Linux schon als ISO anbieten 😉
Zudem ist die Verwaltung der Server (inkl. Umzuege) sehr einfach.

Mit der Klaerung des Hostings stellte sich mir die Frage, wie ich auch das System dahinter moeglichst gut via IaC abbilden kann.

Bisher (berufliche) Erfahrung gesammelt habe ich mit:

Das ist allerdings wesentlich komplexer, fuer mich alleine und mein kleines VPS. Im Zusammenhang mit Arch Linux bin ich dann noch auf folgende Moeglichkeiten gestossen:

  • aconfmgr

    Klingt erstmal sehr gut, auch die Beschreibung im README und der Vergleich zu Puppet, Ansible, NixOS usw.
    Tatsaechlich bin ich erst spaeter darauf gestossen, als ich schon eine Loesung hatte 😉

  • Archiso

    Ein sehr interessanter Ansatz, einfach ein ISO zu erstellen und bei Netcup hochzuladen. Allerdings waere das ISO dann erstmal nur auf den Server zugeschnitten. Aenderungen sind natuerlich nachtraeglich machbar, ohne nochmal den ganzen Server aufzusetzen. Trotzdem, irgendwie aufwaendig.

  • meta package

    Sehr coole Moeglichkeit, mehrere Maschinen einfach mit den vorhandenen Boardwerkzeugen bzw. also Textdateien aufzusetzen. Darauf gekommen bin ich durch diesen Blogeintrag .

Letztendlich entschieden habe ich mich fuer meta packages, da sie einfach zu erstellen und warten, und auch mit CI/CD ausspielbar sind.

Ein Beitrag ueber die Suche und die Technik dahinter sind in Arbeit.

CI/CD #

Die Grundlage von CI /CD ist Versionsverwaltung (bei mir Git ), klar. Aber wie bekomme ich den Code jetzt gebaut und auf den Server? Hierbei habe ich mir folgende Loesungen angeschaut:

  • GitHub Actions

    GitHub Actions wurden 2019 eingefuehrt und fand ich natuerlich direkt spannend. Irgendwann habe ich es dann auch mal fuer ein paar Repos eingebunden und fuer gut befunden.

  • GitLab CI

    GitLab gibt es seit 2014 und waberte immer mal wieder durch die Blogosphaere. Seit wann CI/CD integriert ist, konnte ich nicht herausfinden. Jedenfalls nutze ich GitLab CI seit Jahren beruflich.

  • Travis

    Travis gibt es seit 2011. Ich bin immer wieder in diversen Repos darueber gestolpert und habe es dann selbst kurz ausprobiert, bevor GitHub Actions rauskam.

Mein Code fuer die Website liegt aktuell bei GitHub, daher nutze ich auch die GitHub Actions fuer das Testing und Deployment der Website. GitLab CI waere aber auch eine gute Alternative.

Anforderungen / Wuensche #

Was wollte ich ueberhaupt mit dem ganzen Umbau? Warum nicht alles so wie bisher weiterbetreiben nach dem Motto never change a running system ?

Ich war schon sehr lange nicht mehr zufrieden mit meiner Loesung, aber es hat erstmal funktioniert. Doch mit dem Anreiz, mal alles neu zu machen, habe ich mir eine Liste zusammengestellt bzw. mal einfach drauf los rumgewerkelt.

Hier also eine Liste mit Dingen, die ich mit dem neuen statischen Generator (also Hugo) umsetzen wollte bzw. will. Einige Dinge habe ich noch nicht umgesetzt oder bin noch dabei, daher die Checkboxen.

Migration / Umsetzung #

Nach der sehr einfachen Installation von Hugo ging es daran, meine bestehende Website zu migrieren.

Wo und Wie anfangen waren meine ersten Fragen 😉

Im Prinzip habe ich mich an einigen Blogposts und dem Quick Start entlang gehangelt.

hugo new site uxgch
hugo new theme uxgch

Wobei der letzte Befehl nicht ganz richtig ist, erst habe ich eins der vielen Themes kopiert (XMin ) und nach und nach durch eigenen Code ersetzt, wobei ich mich erstmal an dem Stil der alten Version orientiert habe.

Die alte 'Über mich'-Seite in Version 11.7
Die alte 'Über mich'-Seite in Version 11.7

Im Prinzip habe ich nach und nach jede Datei angepasst, sei es wegen Markdown-Anpassungen (Codebloecke z.B.), Emojis, Aktualisierungen/Ergaenzungen/Streichungen; das Hinzufuegen/Anpassen von Templates oder den Einbau von Shortcodes (Bilder z.B.).

Inspirationen gibt es dabei genug: im Forum , dank neuer Versionen , Code auf GitHub oder Blogbeitraegen .

Breaking Changes #

Beim Wechsel zu Hugo wollte ich so wenig wie moeglich an der alten Struktur veraendern. Und wenn doch, moeglichst alles umleiten, damit alte Links immer noch ans Ziel kommen.

Moeglich machen das hauptsaechlich Aliases im Frontmatter :

title: "Sinn des Lebens"
date: 2007-05-27T13:10:31+02:00
draft: false
tags: [ "tv", "sinn", "leben" ]
aliases: [ "/1180264231.htm" ] # magic

Beim Bauen von Hugo wird nun ein Ordner 1180264231.htm mit der Datei index.html angelegt, die ein meta http-equiv="refresh" zu der neuen Location beinhaltet. Das ist auch schon die ganze Magie dahinter. Laut Hugo habe ich so ca. 288 Aliase in meinen Dateien … 🙈

Fuer den Rest habe ich folgendes in meiner nginx Config:

location / {
  rewrite ^/(tagcloud|holidays|recipes)\.fcgi$ /$1 permanent;
  rewrite ^/tagcloud_(.*)\.htm$ /tags/$1 permanent;
  rewrite ^/linkdump\.xml$ /linkdump/rss.xml permanent;
  rewrite ^/(linux|vegan)\.xml$ /tags/$1/rss.xml permanent;
  rewrite ^/yh\.xml$ /block/rss.xml permanent;
  rewrite ^/all\.xml$ /rss.xml permanent;
}

Diese sorgen fuer die notwendigen Weiterleitungen, gerade fuer die Feeds in den Planeten.

Das Einzige, was ich leider nicht weiterleiten konnte, sind die einzelnen Linkdumps, das waren naemlich einfach nur Anker zu einer jaehrlichen Uebersichtsseite …

Eine Uebersicht der Linkdumps in 2012 mit Ankern in Version 11.7
Eine Uebersicht der Linkdumps in 2012 mit Ankern in Version 11.7

Zumindest wird linkdump_2012.htm weitergeleitet zur jetzigen Uebersichtsseite … Naja, es gibt schlimmeres 😉

Minify everything! #

Es sollte eigtl. Standard sein, dass alle JavaScript- und CSS-Dateien gebuendelt und “minifiziert ” werden, bevor sie ausgespielt werden. Zudem sollte auch HTML “gecrunched” werden. All das ist moeglich mit Hugo Pipes 🎉

Hier als Beispiel mein Code im von layouts/baseof.html, in dem CSS gebuendelt und minifiziert wird, unter Verwendung von Pipes :

{{ $style := resources.Get "css/style.scss" | toCSS }}
{{ $lightbox := resources.Get "css/lightbox2/lightbox.min.css" }}
{{ $fontawesome := resources.Get "fontawesome.scss" | toCSS }}
{{ $opensans := resources.Get "open-sans.scss" | toCSS }}
{{ $css := slice $fontawesome $opensans $lightbox $style | resources.Concat "css/bundle.css" | minify }}
{{ if ne hugo.Environment "development" -}}
  {{ $css = $css | fingerprint -}}
{{ end -}}
<link rel="stylesheet" href="{{ $css.RelPermalink }}">

Aehnlich verhaelt es sich mit JavaScript im

. Im Letzten Schritt kann hier noch die Data Integrity dazukommen:

{{ $js_scratch := newScratch }}
{{ $js_scratch.Set "js" slice -}}
{{ $libs := slice "js/jquery/jquery.min.js" "js/lightbox2/lightbox.min.js" -}}
{{ range $libs -}}
  {{ $js_scratch.Add "js" (resources.Get . ) -}}
{{ end -}}
{{ $js := $js_scratch.Get "js" | resources.Concat "js/bundle.js" | resources.Minify | resources.Fingerprint -}}
{{ if hugo.IsServer -}}
  <script async defer src="{{ $js.RelPermalink }}"></script>
{{ else -}}
  <script async defer src="{{ $js.RelPermalink }}" integrity="{{ $js.Data.Integrity }}"></script>
{{ end -}}

Schoen waere nun noch eine Anpassung der Content Security Policy in nginx , wenn die Data Integrity bekannt ist. Das steht noch auf meiner TODO-Liste 😊
Die CSP kann uebrigens mit einem Evaluator getestet werden.

HTML kann schliesslich mit Hugo selbst minifiziert werden (letzter Satz):

hugo --minify

Ansonsten ist es sicher sinnvoll, auch die statischen Dateien moeglichst klein zu halten, z.B. durch Nutzung von WebP (siehe Bilder) oder WebM ).

Design/Layout #

Mein Ziel war es, ein simples, aber ansprechendes Layout hinzubekommen, und nicht so weit entfernt vom alten Design. Inspirationen gibt es ja alleine mit den Hugo Themes genug, aber ganz cool fand ich die Iterationen der Motherfucking Website (Suche , Reddit ):

Zusaetzlich zur Farbpalette (was eine Wisschenschaft fuer sich ist) kommen dann noch “Feinheiten” wie “neue” HTML-Tags , typographische Elemente wie Smart Quotes , und sonstige schoene Elemente. Davon hat die Designwelt ja recht viel 🤓

Gute Anlaufstellen bieten die Front-End Checklist , das SELFHTML-Wiki , Google Style Guides und generell diverse Standards .

Am Ende habe ich einfach das Standard-CSS in etwas abgewandelter Form verwendet. Die Farben sind bei mir jetzt also Weiss(-isch) als Hintergrundfarbe (#fafafa), Schwarz(-isch) als Schriftfarbe (#27272a), Gruen(-isch) als Linkfarbe (#04815D). Ich habe nur kein Lila als “Visited” verwendet, aber Underline als “Hover” .

Wichtig waren mir dabei Farben, die die folgenden Kontrasttests bestehen (und ich hab mir mal eben schnell noch neue Shortcodes gebaut 😉):

  • color-contrast-enhanced

  • Contrast Checker (testet Hintergrundfarbe und Schriftfarbe)

    ratio AA AALarge AAA AAALarge
    ✅ 14.2 ✅ pass ✅ pass ✅ pass ✅ pass
  • Link Contrast Checker (testet Hintergrundfarbe, Schriftfarbe und Linkfarbe)

    conformance ratio
    Body Text to Background ✅ pass ✅ 14.2
    Link to Background ✅ pass ✅ 4.67
    Link to Body Text ✅ pass ✅ 3.05

CSS #

Das schwierigste (zumindest fuer mich) ist ein sauberes (S)CSS. Zudem habe ich ja mehrere Fremdsysteme mit aufgenommen (z.B. Lightbox und Font Awesome), die ja auch noch (S)CSS mitbringen. Das alles unter einen Hut zu bringen ist nicht immer ganz einfach, aber nur so lerne ich auch dazu 😉

Etwas aufgehalten hat mich auch der selbst gebastelte, datenschutzfreundliche Gist-Shortcode, denn der passt nicht mit meinen anderen Styles (besonders pre und code) zusammen. Allerdings koennen Dinge ja ueberschrieben werden 😉

Zur Vorbereitung auf den Dark Mode habe ich noch etwas anpassen bzw. hinzufuegen muessen. Und zwar habe ich erst in config/_default/markup.toml die Parameter zu Highlight wie folgt gesetzt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[highlight]
  anchorLineNos = false
  codeFences = true
  guessSyntax = false
  hl_Lines = ''
  hl_inline = false
  linkAnchors = ''
  lineNoStart = 1
  lineNos = false
  lineNumbersInTable = true
  noClasses = false # wichtig ist diese Anpassung auf false
  noHl = false
  style = 'solarized-light'
  tabWidth = 2

Mit dieser Einstellung muss ich die entsprechenden Klassen nun selbst hinzufuegen (damit kann ich sie aber z.B. spaeter fuer den Dark Mode einfach ueberschreiben). Ich habe mich fuer den Style solarized-light entschieden, eine Uebersicht gibt es in der Chroma Style Gallery . Die entsprechenden Klassen kann ich mit dem folgenden Befehl an meine assets/css/style.scss anhaengen:

hugo gen chromastyles --style 'solarized-light' >>assets/css/style.scss

Dark Mode #

Hauptmotivation: Dem Darktheme Club beitreten zu koennen 😅

Eine dunkle Variante hatte ich sogar schon eingebaut, allerdings bin ich mit dem (S)CSS noch nicht zufrieden gewesen. Das kommt also noch 😉

Update (2024-07-07): Ok, das hat etwas gebraucht. Und fertig bin ich immer noch nicht 😅
Aber hey, eine erste Iteration ist da!

Ich hatte in meinen Notizen schon sehr viele (ok, nach Ausduennung dann doch nicht mehr so viele) Informationen bzw. Inspirationen (z.B. Hugo Themes mit dem Tag “dark mode” ) zu diesem Thema gesammelt, aber es kommt ja dann doch irgendwie immer ganz anders.

Jedenfalls bin ich dank Bryce Wray ueber Tailwind CSS gestolpert (im Gegensatz zu anderen CSS Frameworks ) und fand die Idee nicht schlecht (finden auch andere gut ). Zumindest kann ich damit relativ leicht einen Dark Mode mit erstellen (durch Nutzung von prefers-color-scheme durch das Betriebssystem).

Installation und Nutzung von Tailwind CSS #

Update (2025-10-20): Hier war mal eine komplizierte Anleitung, wie Tailwind zu integrieren ist, aber es gibt das jetzt einfach in der offiziellen Doku, naemlich css.TailwindCSS .

Damit habe ich meine CSS Dateien auch ausgelagert:

 layouts/_partials/css.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{{ $style := resources.Get "css/style.scss" | toCSS }}
{{ $tohn_hugo_shortcodes := resources.Get "css/tohn_hugo_shortcodes.scss" | toCSS }}
{{ $lightbox := resources.Get "css/lightbox2/lightbox.min.css" }}
{{ $tailwind := resources.Get "css/tailwind.css" }}
{{ $tailwind_gen := css.TailwindCSS $tailwind }}
{{ $css := slice $style $lightbox $tohn_hugo_shortcodes $tailwind_gen | resources.Concat "css/bundle.css" | minify }}
{{ with $css }}
  {{ if hugo.IsDevelopment }}
    <link rel="stylesheet" href="{{ .RelPermalink }}">
  {{ else }}
    {{ with . | fingerprint }}
      <link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
    {{ end }}
  {{ end }}
{{ end }}

Und dann eben wie beschrieben in der layouts/baseof.html eingebunden.

In der CSS Datei habe ich dabei

Einige Einstellungen habe ich dann in der assets/css/tailwind.css gesetzt, z.B. habe ich die preflight-Zeile Preflight rausgenommen , damit ich nicht alle Standard-Tags nochmal neu stylen muss.

 assets/css/tailwind.css

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/* https://tailwindcss.com/docs/preflight#disabling-preflight */
/* @import "tailwindcss"; */
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities);

@layer base {
  a {
    @apply text-[#04815D] dark:text-[#1B9D69] no-underline hover:underline;
  }
  blockquote {
    @apply relative before:font-serif before:text-zinc-300 before:dark:text-zinc-500 before:-z-10 before:absolute before:content-['\201C'] before:text-7xl before:-top-4 before:-left-4;
  }
  code {
    @apply m-0 text-sm bg-[#eeefef] dark:bg-[#6e768166] py-[3.2px] px-[6.4px] rounded-[5px]
  }
  h1 {
    @apply text-center;
  }
  .footnotes hr {
    @apply my-3 bg-gradient-to-r from-zinc-500 dark:from-zinc-300 via-transparent dark:via-transparent to-zinc-500 dark:to-zinc-300 h-0.5 border-0;
  }
  .highlight {
    @apply relative mt-[calc(30px_+_1em)];
  }
  .highlight > pre {
    @apply overflow-auto leading-normal p-2.5 rounded-[5px];
  }
  .highlight > div {
    @apply rounded-[5px];
  }
  .highlight table pre {
    @apply leading-normal mx-0 my-2.5 p-0;
  }
  .highlight code {
    @apply bg-[rgba(27,31,35,0.05)] p-0;
  }
  .highlight pre > code[data-lang]:not([data-lang=""]):before {
    @apply absolute top-[-30px] content-[attr(data-lang)] text-[#899c9c] bg-[#eee8d5] dark:bg-[#002b36] pt-[5px] pb-2.5 px-[15px] rounded-[5px] left-0;
  }
  table {
    @apply w-[320px] md:w-full md:overflow-x-auto border-collapse;
  }
  table td, table th {
    @apply border p-[5px] border-solid border-[#ddd];
  }
  table th {
    @apply bg-[#f7f7f7] dark:bg-zinc-800;
  }
  table tr:nth-child(even) {
    @apply bg-[#f7f7f7] dark:bg-zinc-800;
  }
  table.search {
    @apply text-center;
  }
  table.search input[type="text"] {
    @apply w-4/5 m-[5px] p-[5px];
  }
  table.search button {
    @apply w-[10%] m-[5px] p-[5px];
  }
  img {
    @apply dark:brightness-[0.8] dark:contrast-[1.2];
  }
}

Das sind hauptsaechlich Tags, die ich so nicht einfach setzen kann, weil sie via Markdown einfach generiert werden (z.B. blockquote und code). Zudem habe ich dort auf alle img-Tags im Dark Modus einen Filter angewendet (siehe Dark Mode Images ), damit die Bilder nicht so hell sind im Vergleich.

SCSS zu Tailwind CSS #

Tja, und dann kann die Nutzung losgehen. Ich habe einfach mal mit der layouts/baseof.html angefangen (mit dem ) und quasi alles aus der SCSS in Tailwind CSS uebersetzt. Geholfen haben mir da sehr diese zwei Converter: SCSS to CSS und CSS to Tailwind CSS .

Und bin dann nach und nach alle Shortcodes etc. durchgegangen. Die meisten Probleme hatte ich mit der Syntaxhervorhebung. Dazu habe ich erstmal eine weitere Variante (naemlich solarized-dark) zu meiner assets/css/style.scss hinzugefuegt:

hugo gen chromastyles --style 'solarized-dark' >>assets/css/style.scss

Aber seit der letzten Generierung vor ein paar Jahren hat sich die Software dahinter weiterentwickelt und meine kompletten Codekaesten zerschossen. Nach langem Probieren und Rumsuchen bin ich dann dank des Hugo Entwicklers Joe Mooring auf ein paar Zeilen Loesung (noch mehr dazu von ihm ) gestossen (danke dafuer!):

.chroma .lntable .lnt,
.chroma .lntable .hl {
  display: flex;
}

.chroma .lntable .lntd + .lntd  {
  width: 100%;
}

.chroma {
  overflow-x: auto;
  margin: 0;
}

Und damit ist nun fast alles in die Shortcodes usw. gewandert, aber eben auch ein paar Sachen in assets/css/tailwind.css bzw. noch in assets/css/style.scss geblieben (zumindest den Chroma Kram wuerde ich auch dort drin lassen).

Eine coole Sache noch, die mir via diesem Guide aufgefallen ist:

<meta name="color-scheme" content="dark light">

Das im Header (also in layouts/baseof.html) sorgt dafuer, dass Formularfelder wie bei meiner Suche auch entsprechend dem Dark oder Light Mode eingefaerbt werden. The more you know 😉

Accessibility #

Ich hatte mir beim ersten Rumspielen mit Tailwind CSS so schoene Farben fuer den Dark Mode ausgesucht, doch dann kam der Link Contrast Checker 🥲

Nach einigem Rumspielen (danke an HTML Color Codes , Colour accessibility test , Contrast Grid und eine Toolsammlung ) habe ich endlich Farben fuer den Dark Mode gefunden, die auch die Tests bestehen:

  • Hintergrundfarbe: #21212C
  • Schriftfarbe: #f4f4f5
  • Linkfarbe: #1B9D69
  • Contrast Checker:
    ratio AA AALarge AAA AAALarge
    ✅ 14.4 ✅ pass ✅ pass ✅ pass ✅ pass
  • Link Contrast Checker:
    conformance ratio
    Body Text to Background ✅ pass ✅ 14.4
    Link to Background ✅ pass ✅ 4.60
    Link to Body Text ✅ pass ✅ 3.14
Was noch fehlt #

Responsive Design #

Eigentlich dachte ich ja, dass es sich mit dem folgenden Meta-Tag erledigt hat:

<meta name="viewport" content="width=device-width, initial-scale=1.0">

Aber so einfach ist es leider nicht 🥲

Mit Tailwind CSS hatte ich die Gelegenheit, von Grund auf direkt damit zu starten , habe diese aber nicht genutzt. Also steht das noch auf meiner TODO Liste.

Fonts #

Fuer die kleinen Icons nutze ich Font Awesome , Alternativen sind z.B. Feather , Fork Awesome , Simple Icons , oder Health Icons .

Ich habe lange herumprobiert und mich dann letztendlich dazu entschieden, nur die Woff2-Dateien durch etwas CSS einzubinden. Also kein JavaScript und auch keine SVG Sprites.
Zwar kann IE damit nicht umgehen , aber … who cares? 😉

Fuer das Einbinden nutze ich npm und Hugo Mounts , es geht aber auch direkt mit einem Hugo Module . Alternativ geht das auch via Shortcode .

Meine Loesung wie folgt (von hier ). Erstmal das Paket installieren:

npm i @fortawesome/fontawesome-free

Danach folgendes in die config/_default/module.toml mit aufnehmen:

[[mounts]]
  source = "node_modules/@fortawesome/fontawesome-free/scss"
  target = "assets/css/fontawesome"
[[mounts]]
  source = "node_modules/@fortawesome/fontawesome-free/webfonts"
  target = "static/fonts/fontawesome"

Dann eine kleine Datei assets/fontawesome.scss erstellen:

$fa-font-path: "../fonts/fontawesome";
@import "css/fontawesome/fontawesome.scss";
@import "css/fontawesome/solid.scss";
@import "css/fontawesome/brands.scss";

Und diese dann in die Minifizierungspipeline mit aufnehmen:

{{ $fontawesome := resources.Get "fontawesome.scss" | toCSS }}
{{ $css := slice $fontawesome $opensans $lightbox $style | resources.Concat "css/bundle.css" | minify }}

Die Nutzung erfolgt entweder direkt (also mit HTML Code) oder via Shortcode . Ich habe dazu einfach selbst einen gebastelt (layouts/_shortcodes/icon.html):

  icon.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{{- /* https://fontawesome.com */ -}}
{{- /* Set (fa-solid (solid), fa-regular (regular), fa-brands (brands)) */ -}}
{{- $set := .Get "set" | default "fa-solid" -}}
{{- /* Icon (https://fontawesome.com/icons) */ -}}
{{- $icon := .Get "icon" | default "fa-seedling" -}}
{{- /* Size (fa-(xs|sm|lg|2x|3x|5x|7x|10x) */ -}}
{{- $size := .Get "size" | default "" -}}
{{- /* Rotate (fa-(rotate-90|rotate-180|rotate-270|flip-horizontal|flip-vertical|flip-both)) */ -}}
{{- $rotate := .Get "rot" | default "" -}}
{{- /* Animate (https://fontawesome.com/docs/web/style/animate) */ -}}
{{- $animate := .Get "anim" | default "" -}}
{{- /* Color */ -}}
{{- $color := .Get "col" | default "" -}}
{{- /* Accessibility (d, s) */ -}}
{{- /* https://fontawesome.com/v6/docs/web/dig-deeper/accessibility */ -}}
{{- $access := .Get "access" | default "d" -}}
{{- $title := .Get "title" | default "" -}}
{{- $text := .Get "text" | default "" -}}
<i class='{{ $set }} {{ $icon }} {{ $size }} {{ $rotate }} {{ $animate }}' {{ with $color }}style='color:{{ . }}'{{ end }} aria-hidden='true' {{ with $title }}{{ . }}{{ end }}></i>
{{- if eq $access "s" -}}
<span class="sr-only">{{ $text }}</span>
{{- end -}}

Genutzt werden kann es nun z.B. auf einer Seite content/test.md wie folgt:

{{< icon size="fa-lg" anim="fa-spin" col="#abcdef" >}}

Info

Der obige Code musste auskommentiert werden, damit er nicht ausgefuehrt wird. Das geht, indem nach dem < ein /* und vor dem > ein */ hinzugefuegt wird.

Dieser Code erzeugt dann das hier: .

Wichtig ist hierbei, dass die Datei keine neue Zeile am Ende enthaelt, denn sonst wird das Icon mit einem Leerzeichen dahinter dargestellt.

Sofern eine .editorconfig genutzt wird, kann folgendes fuer .html-Dateien hinzugefuegt werden:

[*.html]
insert_final_newline = false

Alternativ kann das Leerzeichen laut Stackexchange mit perl -pi -e 'chomp if eof' /path/to/file entfernt werden.

Noch auf meiner TODO-Liste ist die Unterscheidung zwischen dekorativen und semantischen Icons und die entsprechende Umsetzung.

Ansonsten nutze ich als Schriftart Open Sans , was auch via npm und Hugo Mounts eingebunden und ausgeliefert wird.

Bilder #

Die Bilder wollte ich wie bisher auch mit einem Polaroid-Effekt versehen (zudem Lightbox zum schoener darstellen und navigieren), diesmal aber via CSS, statt vorgeneriert mit convert .

Polaroid Bilder in der alten Version 11.7
Polaroid Bilder in der alten Version 11.7

Inspirationen fuer die Umsetzung habe ich ein paar gefunden und damit folgenden SCSS-Code in assets/css/style.scss gebastelt:

figure {
  img {
    display: block;
    margin-left: auto;
    margin-right: auto;
  }
  &.lb {
    margin: 2em auto;
    background: white;
    background: linear-gradient(110deg, white, oldlace);
    box-shadow: 4px 4px 15px gray;
    max-width: 280px;
    vertical-align: top;
    position: relative;
    transform: rotate(4deg);
    transition: all ease 0.6s;
    text-align: center;
    a {
      text-decoration: none;
    }
    img {
      display: inline;
      max-width: 100%;
      height: auto;
      margin: 5% 5% 0 5%;
    }
    figcaption {
      width: 90%;
      min-height: 50px;
      margin: 0 5% 5% 5%;
      text-align: center;
    }
  }
}

Bisher habe ich vorwiegend Bilder in den Formaten JPEG und PNG verwendet. Allerdings gibt es seit Version v.83.0 in Hugo Support fuer WebP , was laut Can I use… in den meisten derzeit genutzten Browsern verwendet werden kann.

Ein paar der bisher genutzten Bilder habe ich nachtraeglich mit cwebp (alternativ convert ) umgewandelt. Dank Hugo kann ich aber zumindest die Thumbnails automatisch mit einem Shortcode umwandeln lassen 🎉

[Hugo kann noch viel mehr, zum Beispiel koennen auch automatisch Wasserzeichen hinzugefuegt werden oder Exif-Informationen ausgelesen und dargestellt werden. Sehr cool z.B. fuer Bildergalerien.]

Ich glaube, der Shortcode fuer Bilder ist eins der am haeufigsten angepassten Shortcodes, ein Beispiel gibt es im Forum . Mein Shortcode ist auch angepasst und hat Lightbox-Code schon integriert, zudem noch ein paar andere Dinge wie Unterschriften, Qualitaet, Linkziele, Format, …

Auf meiner TODO-Liste steht noch lazy loading , was aber etwas komplexer ist.

Favicon #

Ein Favicon hatte ich ja vorher schon, nur die Einbindung war noch nicht ganz perfekt. Ueber die Website RealFaviconGenerator habe fuer ca. alle Plattformen das richtige Bild erstellen und in layouts/baseof.html einbinden koennen:

-- realfavicongenerator.net -->
<link rel="apple-touch-icon" sizes="180x180" href="{{ "apple-touch-icon.png" | absURL }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ "favicon-32x32.png" | absURL }}">
<link rel="icon" type="image/png" sizes="192x192" href="{{ "android-chrome-192x192.png" | absURL }}">
<link rel="icon" type="image/png" sizes="16x16" href="{{ "favicon-16x16.png" | absURL }}">
<link rel="manifest" crossorigin="use-credentials" href="{{ "manifest.webmanifest" | absURL }}">
<link rel="mask-icon" href="{{ "safari-pinned-tab.svg" | absURL }}" color="#5bbad5">
<link rel="shortcut icon" href="{{ "favicon.ico" | absURL }}">
<meta name="apple-mobile-web-app-title" content="{{ site.Title }}">
<meta name="application-name" content="{{ site.Title }}">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="msapplication-config" content="{{ "browserconfig.xml" | absURL }}">
<meta name="theme-color" content="#000000">

Die Dateien von der Website habe ich alle im Ordner static abgelegt.

Codebloecke / Syntaxhighlighting #

Ich verwende viele Codebloecke, daher sollen die natuerlich auch schoen aussehen. Hugo bringt sowas schon mit und funktioniert mit Code Fences und Shortcodes . Im Hintergrund verwendet Hugo Chroma , was sehr viele Sprachen unterstuetzt . Abweichend von den Standardeinstellungen habe ich mich fuer solarized-light als Style (siehe CSS) bzw. solarized-dark (siehe Dark Mode) entschieden.

Die Inspiration, vor einen Codeblock die Sprache “anzupinnen” habe ich mir hier geholt .

Beispiele fuer den Einsatz gibt es weiter unten bei Gist oder GitHub.

Blockquotes #

Zitate verwende ich immer mal wieder und wollte die auch entsprechend aufhuebschen. Fuer das CSS habe ich mich ein bisschen an den “BQ Patterns” und “Classy Blockquotes” (ohne Bilder) orientiert und mich auch noch ein bisschen inspirieren lassen von einer anderen Website .

Mein Shortcode in layouts/_shortcodes/blockquote.html ist recht simpel gestaltet:

 layouts/_shortcodes/blockquote.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{{ $src := .Get "src" }}
{{ $dst := .Get "dst" }}

<blockquote {{ if .Get "dst" }}cite="http{{ $dst }}"{{ end }}>
{{ with .Inner }}{{ $.Page.RenderString . }}{{ end }}
{{ if .Get "src" }}
<footer class="before:content-['\2015'] before:text-zinc-300 before:dark:text-zinc-500">
  {{ if .Get "dst" }}{{ with print "[" $src "](http" $dst ")" }}{{ $.Page.RenderString . }}{{ end }}{{ else}}{{ $src }}{{ end }}
</footer>
{{ end }}
</blockquote>

Verwendet werden kann es wie folgt:

{{< blockquote src="Wikipedia" dst="s://de.wikipedia.org/wiki/Blockzitat" >}}
Ein **Blockzitat** ist eine typografische Darstellung eines Zitats, bei dem eine längere zitierte Passage als eigener Absatz herausgestellt wird.
{{< /blockquote >}}

Mein SCSS in assets/css/style.scss im Folgenden enthaelt Mixins (quasi Funktionen) und Variablen, da ich es z.B. auch fuer Twitter in aehnlicher Form verwendet habe:

$color: #575757;
$color_blockquote_before: #e6e6e6;
$color_blockquote_footer: #d3d3cf;
$font_serif: serif;

@mixin blockquote_before($color: $color_blockquote_before) {
  position: absolute;
  color: $color;
  z-index: -1;
}

blockquote {
  position: relative;

  &:before {
    font-family: $font_serif;
    content: '\201C';
    top: -50px;
    left: -15px;
    font-size: 5em;
    @include blockquote_before();
  }

  footer {
    color: $color_blockquote_footer;

    &:before {
      content: '\2015';
    }
  }
}

Das Resultat mit Quelle (bzw. Footer) sieht nun so aus:

Ein Blockzitat ist eine typografische Darstellung eines Zitats, bei dem eine längere zitierte Passage als eigener Absatz herausgestellt wird.

Render Hooks #

Das Feature Render Hooks gibt es seit Version v0.62.0 und erlaubt es, bei Headern (also z.B.

) den Permalink daneben hinzuzufuegen (bei mir durch ein # gekennzeichnet) oder Links zu anderen Websites mit einem Icon (bei mir ) kenntlich zu machen.

Der Code in layouts/_markup/render-heading.html fuer die Header sieht so aus:

<h{{ .Level }} id="{{ .Anchor | safeURL }}">{{ .Text | safeHTML }} <a href="#{{ .Anchor | safeURL }}">#</a></h{{ .Level }}>

Fuer die Icons neben den Links sieht der Code in layouts/_markup/render-link.html wie folgt aus:

<a href="{{ .Destination | safeURL }}"{{ with .Title}} title="{{ . }}"{{ end }}>{{ .Text | safeHTML }}{{ if strings.HasPrefix .Destination "http" }} <i class='fa-solid fa-up-right-from-square fa-sm'></i>{{ end }}</a>

Durch die Abfrage mit dem Prefix http werden nur externe Links mit dem Icon versehen, interne Links sind relativ und werden daher nicht mit gekennzeichnet.

CSS Icons #

Eine Alternative (?) ist die Nutzung von CSS , z.B. um Links zu einem RSS-Feed mit dem RSS-Icon () zu kennzeichnen.

Dazu reicht folgendes im CSS (mit der Nutzung von Fonts):

a[href$="rss.xml"]::before {
  content: '\f143';
  color: #f26522;
  font-family: "Font Awesome 5 Free";
  margin-right: 5px;
}

Verlinke ich jetzt einen RSS-Feed, erscheint das Icon: Link zu einem RSS-Feed. Das funktioniert auch mit externen Feeds, dann wird zusaetzlich noch der render-link.html angehaengt: Link zu externem “RSS-Feed” .

Back to top #

Gerade bei langen Texten und/oder auf dem Mobilgeraet ist ein Button, mit dem zum Anfang “gescrollt” werden kann recht sinnvoll.

Daher nun jetzt auch auf dieser Website, dank vanilla-back-to-top . Installiert habe ich es ganz einfach ueber npm :

npm i vanilla-back-to-top

Danach hab ich es in die config/_default/module.toml mit aufgenommen:

[[mounts]]
  source = "node_modules/vanilla-back-to-top/dist/vanilla-back-to-top.min.js"
  target = "assets/js/b2t/b2t.min.js"

Zum Schluss noch eine kleine Datei layouts/assets/js/b2t.js erstellt (was dem 2. Beispiel entspricht ):

 assets/js/b2t.js

1
2
3
4
5
addBackToTop({
  diameter: 56,
  backgroundColor: 'rgb(255, 82, 82)',
  textColor: '#fff'
})

Und diese dann mit der gemounteten layouts/assets/js/b2t/b2t.min.js in die Minifizierungspipeline mit aufgenommen:

{{ $libs := slice "js/jquery/jquery.min.js" "js/lightbox2/lightbox.min.js" "js/b2t/b2t.min.js" "js/b2t.js" -}}

LaTeX #

Update (2024-01-28): Eine Integration von \(\LaTeX\) wollte ich schon seit langer Zeit haben (einfach weil ich es kann, schoen und einfach finde (git !), damit meine Bachelor Arbeit erstellt habe und immer noch zum Briefe schreiben nutze) und hatte dazu auch schon mehrere Vorgehensweisen entdeckt:

Seit Hugo v0.122.0 gibt es allerdings dazu eine offizielle Anleitung und das ist doch Grund genug, das endlich mal zu integrieren, wa?

Zuerst habe ich meine markup.toml und params.toml um die jeweiligen Konfigurationen ergaenzt, allerdings ist der Default fuer math bei mir auf false gesetzt (muss dann also im Frontmatter auf true gesetzt werden, wenn LaTeX aktiviert sein soll). Zudem habe ich nur jeweils eine Konfiguration fuer “block” und “inline”, da ich die eine “block”-Syntax auch in einem anderen Kontext nutze:

[markup]
  [markup.goldmark]
    [markup.goldmark.extensions]
      [markup.goldmark.extensions.passthrough]
        enable = true
        [markup.goldmark.extensions.passthrough.delimiters]
          block = [['$$', '$$']]
          inline = [['\(', '\)']]
[params]
  math = false

Allen Anleitungen gemein ist die blinde Einbindung von JavaScript ueber ein Content Delivery Network . Da ich das aus Datenschutzgruenden nicht nutzen will, werde ich auch hier die benoetigten Dateien via npm und Hugo Mounts einbinden.

Ich habe mich fuer MathJax und die tex-chtml.js-Variante (es gibt noch andere ) entschieden, die auch in der Hugo Doku verwendet wird.

Also installieren wir erstmal das Paket:

npm i mathjax --save

Und binden 2 Dinge via Modules ein:

[[mounts]]
  source = "node_modules/mathjax/es5/tex-chtml.js"
  target = "assets/js/mathjax/mathjax.js"
[[mounts]]
  source = "node_modules/mathjax/es5/output/chtml/fonts/woff-v2"
  target = "static/js/output/chtml/fonts/woff-v2"

Zur Konfiguration legen wir noch eine weitere JavaScript Datei an:

 assets/js/mathjax.js

1
2
3
4
5
6
MathJax = {
  tex: {
    displayMath: [['$$', '$$']], // block
    inlineMath: [['\\(', '\\)']] // inline
  }
};

Zu guter Letzt werden die beiden Dateien moeglichst umstaendlich™ in layouts/baseof.html im

eingebunden, analog zu den anderen JavaScript-Dateien (ich habe hier noch keine bessere Moeglichkeit gefunden, abhaengig von der Konfiguration im Frontmatter unterschiedliche JavaScript Dateien usw. einzubinden, daher wird es nun als weiteres Bundle nur nachgeladen, wenn der Parameter math gesetzt ist):

<footer>
  ...
  {{- if .Param "math" -}}
    {{ $js_mathjax_scratch := newScratch -}}
    {{ $js_mathjax_scratch.Set "jsm" slice -}}
    {{ $js_mathjax_scratch.Add "jsm" (resources.Get "js/mathjax/mathjax.js") -}}
    {{ $js_mathjax_scratch.Add "jsm" (resources.Get "js/mathjax.js") -}}
    {{ $jsm := $js_mathjax_scratch.Get "jsm" | resources.Concat "js/bundle-mathjax.js" -}}
    {{ if hugo.IsServer -}}
    <script async defer src="{{ $jsm.Permalink }}"></script>
    {{ else -}}
    {{ $jsm := $jsm | minify | fingerprint }}
    <script async defer src="{{ $jsm.Permalink }}" integrity="{{ $jsm.Data.Integrity }}" crossorigin="anonymous"></script>
    {{ end -}}
  {{- end -}}
  ...
</footer>

Und damit kann ich jetzt endlich \(\LaTeX\) nutzen, woop woop (an das math: true im Frontmatter denken) 🎉

So zum Beispiel den Satz vom Igel , an den ich mich noch aus meinem Studium erinnern kann:

Für jedes \(m\in \mathbb {N}\) und für jede stetige Abbildung \(f\colon \mathbb {S} ^{2m}\to \mathbb {R} ^{2m+1}\) existiert ein \(x_{0}\in \mathbb {S} ^{2m}\) und ein \(\lambda \in \mathbb {R}\) mit \(f(x_{0})=\lambda \cdot x_{0}\).

Table Of Contents / Inhaltsverzeichnis #

Der Code in der Doku reicht leider nicht fuer meine Beduerfnisse, denn ich will eigentlich immer ein Inhaltsverzeichnis haben, wenn der Text 400 oder mehr Woerter enthaelt und nicht der Flag toc: false gesetzt ist.
Ansonsten soll das Inhaltsverzeichnis nur angezeigt werden, wenn es 2 oder mehr Header gibt (egal welche).

Mein Code in _partials/block/toc.html sieht daher wie folgt aus (mit Nutzung von

):

 layouts/_partials/block/toc.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{{- /*
  https://gohugo.io/content-management/toc/#template-example-toc-partial
  https://github.com/gohugoio/hugo/issues/1778#issuecomment-522222658
  https://gist.github.com/percygrunwald/043e577beb90db72e09727a3ed3053c3
  https://gist.github.com/pyrrho/1d77cdb98ba58c7547f2cdb3fb325c62
*/ -}}
{{- if and (gt .WordCount 400 ) (ne .Params.toc false) -}}
  {{- $headers := findRE "(.|\n])+?" .Content -}}
  {{- $has_headers := ge (len $headers) 2 -}}
  {{- if $has_headers -}}
    <details>
      <summary>Inhaltsverzeichnis</summary>
      {{- .TableOfContents -}}
    </details>
  {{- end -}}
{{- end -}}

Ein Problem bestand noch darin, dass das erstellte Inhaltsverzeichnis auch den ersten Header (

) mit dargestellt hat. Dank Goldmark kann das nun via startLevel = 2 eingestellt werden (das ging vorher mit Blackfriday naemlich nicht ).

Sitemap #

Hugo liefert schon von Haus aus eine Sitemap mit (hier ist meine), der Code dafuer ist auf GitHub . Die Einbindung in den Header ist in diesem Thread beschrieben .

Soll eine Seite/Beitrag oder was auch immer nicht in der Sitemap erscheinen, kann die Loesung von Fryboyter verwendet werden. Eine Alternative Loesung bei Mert Bakır . Meine Loesung in layouts/sitemap.xml sieht jetzt einfach so aus:

{{ printf "" | safeHTML }}
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
  xmlns:xhtml="http://www.w3.org/1999/xhtml">
  {{ range where .Data.Pages ".Params.exclude" "!=" true }} // statt {{ range .Data.Pages }}
    {{- if .Permalink -}}
  <url>
    <loc>{{ .Permalink }}</loc>{{ if not .Lastmod.IsZero }}
    <lastmod>{{ safeHTML ( .Lastmod.Format "2006-01-02T15:04:05-07:00" ) }}</lastmod>{{ end }}{{ with .Sitemap.ChangeFreq }}
    <changefreq>{{ . }}</changefreq>{{ end }}{{ if ge .Sitemap.Priority 0.0 }}
    <priority>{{ .Sitemap.Priority }}</priority>{{ end }}{{ if .IsTranslated }}{{ range .Translations }}
    <xhtml:link
                rel="alternate"
                hreflang="{{ .Language.Lang }}"
                href="{{ .Permalink }}"
                />{{ end }}
    <xhtml:link
                rel="alternate"
                hreflang="{{ .Language.Lang }}"
                href="{{ .Permalink }}"
                />{{ end }}
  </url>
    {{- end -}}
  {{ end }}
</urlset>

Nun kann im Frontmatter eine Seite via exclude: true in der Seite ausgeschlossen werden.

Konfigurieren lassen sich ansonsten ein paar Einstellungen via Config:

 config/_default/sitemap.toml

1
2
3
4
# https://gohugo.io/templates/sitemap-template/
changeFreq = ''
priority = -1
filename = 'sitemap.xml'

Das XML-Schema einer Sitemap ist hier spezifiziert und kann mit einem Validator ueberprueft werden.

Warnung

Dieser Fehler hat mich mein halbes Leben gekostet … 🙄

Wird in config/_default/outputs.toml bei home die Sitemap inkludiert, wird einfach das Default-Template genutzt:

home     = [ "html", "rss", "manifest", "sitemap" ]

Somit greifen jegliche Aenderungen in layouts/sitemap.xml nicht.

robots.txt / humans.txt #

Eine robots.txt ist ja ueblich, aber auf der anderen Seite gibt es auch humans.txt (mehr dazu ). Ein lustiges Projekt, fuer mich aber nicht notwendig, da ich hier ja alles selbst mache, daher leite ich es einfach auf meine Kontakt-Seite weiter.

Natuerlich laesst sich die robots.txt auch anpassen . Ein Forumsbeitrag enthaelt z.B. die Moeglichkeit, einzelne Seiten/Beitraege nicht zu erlauben (aehnlich zur Sitemap). Meine sieht z.B. so aus (layouts/robots.txt), indem ich einach den gleichen Parameter wie bei der Sitemap nutze:

 layouts/robots.txt

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# https://github.com/ai-robots-txt/ai.robots.txt
{{- $url := "https://raw.githubusercontent.com/ai-robots-txt/ai.robots.txt/refs/heads/main/robots.txt" -}}
{{- with try (resources.GetRemote $url) -}}
  {{- with .Err -}}
    {{- errorf "%s" . -}}
  {{- else with .Value }}
{{ .Content -}}
  {{- else -}}
    {{- errorf "Unable to get remote resource %q" $url -}}
  {{- end -}}
{{- end }}
# https://en.wikipedia.org/wiki/Robots.txt
User-agent: *
{{ range where .Data.Pages "Params.exclude" true -}}
Disallow: {{ .RelPermalink }}
{{ end }}
# https://en.wikipedia.org/wiki/Robots.txt#Sitemap
Sitemap: {{ "sitemap.xml" | absLangURL }}

Wichtig bei einer eigenen robots.txt ist folgende Einstellung in config/_default/config.toml:

enableRobotsTXT = true

Werden bestimmte Seiten ausgeschlossen , sollten sie auch im ausgeschlossen werden. Daher sieht meine layouts/baseof.html so aus:

<head>
  -- [..] -->
  <meta name="robots" content="{{ if .Params.exclude }}noindex,nofollow{{ else }}index,follow{{ end }}">
</head>

Update (2025-03-16): Dank Jan-Lukas Else blocke ich jetzt auch AI Bots , dank ai.robots.txt . Gutes Projekt und wieder einmal geil, wie einfach die Integration in Hugo ist via resources.GetRemote . Love it! 🤖

Errorcodes #

Normalerweise sehen die Errorcodes-Seiten ziemlich langweilig aus. Mit Hugo ist es moeglich, diese auch wie eine normale Webseite auszugeben und mit z.B. nginx zu nutzen (content/403/index.md):

 content/403/index.md

1
2
3
4
5
6
7
8
---
title: "403 - Forbidden"
draft: false
description: "403 - Forbidden"
url: 403.html
exclude: true
---
{{< youtube RfiQYRn7fBg >}}

Wichtig ist hier die Zeile mit url: 403.html, denn ansonsten wuerde Hugo dies als /403/ ausgeben, womit nginx Schwierigkeiten hat, daher reicht nun einfach:

error_page 403 /403.html;

Ebenso wichtig ist die Zeile mit exclude: true, denn diese Fehlerseiten sollen weder in der Sitemap auftauchen, noch in der robots.txt bzw. “erlaubt” sein .

Und auch wichtig ist, dass CSS-, JavaScript und sonstige Dateien absolut eingebunden werden. Bisher hatte ich in meinem alles auf relURL und relPermalink stehen (siehe Minifizierung), doch dann koennen Resourcen nicht geladen werden, wenn z.B. https://uxg.ch/bewusst/falscher/pfad aufgerufen wird. Daher habe ich nun alles einiges auf absURL und Permalink umgestellt.

Meine Fehlerseiten: 403 und 404. Weitere schoene gibt es bei http.cat 😸

RSS #

Da ich in mehreren Planeten gelistet bin, brauche ich natuerlich auch einen ordentlichen RSS-Feed , wenn nicht sogar mehrere 😉

Allerdings ist das Default-Template nicht ausreichend genug, denn ich will im Feed nicht nur ein Summary eines Beitrags drin haben, sondern eben den ganzen Beitrag . Zudem sollen nicht alle Beitraege im Feed sein, sondern nur die neuesten 10, was mit einem limit = 10 gesetzt werden kann.

Update (2024-03-30): Dazu habe ich das Template in layouts/rss.xml mit einer Zeile modifiziert (mit Einhaltung der Standards ). Es gibt uebrigens auch ein Issue und schon einen Pull request :

<description>{{ .Content | transform.XMLEscape | safeHTML }}</description> // .Content statt .Summary

Das funktioniert schonmal prima fuer die Tags 🎉

Allerdings soll die Tags-Seite kein RSS ausspucken (wozu auch?). Dazu ist eine Anpassung der Outputs in config/_default/outputs.toml notwendig:

page     = [ "html" ]
home     = [ "html", "rss", "manifest" ]
section  = [ "html", "rss" ]
# taxonomy soll kein rss beinhalten
taxonomy = [ "html" ]
term     = [ "html", "rss" ]

Hugo wirft jetzt erstmal einen Fehler bei dieser Aenderung aus:

ERROR 2021/12/13 17:12:50 You have configured output formats for 'taxonomy' in your site configuration. In Hugo 0.73.0 we fixed these to be what most people expect (taxonomy and term).
But this also means that your site configuration may not do what you expect. If it is correct, you can suppress this message by following the instructions below.
If you feel that this should not be logged as an ERROR, you can ignore it by adding this to your site config:
ignoreErrors = ["error-output-taxonomy"]

Da ich mir sicher bin, dass ich kein RSS-Feed fuer meine Tags-Seite haben will, kann ich den Fehler in der config/_default/config.toml ignorieren:

ignoreErrors = ["error-output-taxonomy"]

Falls ihr nicht sicher seid, was alles noch einen RSS-Feed ausspuckt, und ob das wirklich notwendig ist, koennt ihr eure Seite erstmal bauen und dann nach rss.xml suchen. Bei mir hatten sich ein paar Dateien eingeschlichen, die durch Sektionen generiert wurden, obwohl das gar keine Section sein muss. Also mal schnell ein git mv _index.md index.md gemacht in den entsprechenden Ordnern und bis auf eine Sektion war alles gut (steht noch auf meiner TODO-Liste das zu fixen) 😉

Update (2024-04-05)

Das ist mittlerweile gefixt, indem ich einfach im Frontmatter outputs ueberschreibe (und rss einfach weglasse):

outputs:
  - html

Eingebunden werden koennen die Feeds (und ggf. andere Formate) in den in layouts/baseof.html so:

{{ with .OutputFormats.Get "rss" -}}
  {{ printf `` .Rel .MediaType.Type .Permalink site.Title | safeHTML }}
{{ end }}

Uebrigens gibt es auch Validatoren fuer RSS-Feeds .

Alternativen: Atom und JSON Feed #

Zusaetzlich zu RSS gibt es auch noch Atom und JSON Feed , was wir natuerlich auch in Hugo realisieren koennen.

Darauf habe ich aber (erst einmal) verzichtet, liste aber hier ein paar Quellen auf, wie das realisiert werden kann.

Related Posts / Aehnliche Beitraege #

Wie in jeder Standardwordpressinstallation gibt es auch bei Hugo die Moeglichkeit, aehnliche Beitraege z.B. nach einem Blockpost anzuzeigen.

Meine Config (config/_default/related.toml) sieht so aus:

 config/_default/related.toml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# https://gohugo.io/content-management/related/

threshold = 1
includeNewer = false
toLower = false

[[indices]]
name = "tags"
weight = 100

[[indices]]
name = "keywords"
weight = 50

[[indices]]
name = "date"
weight = 1
pattern = "2006"

# use fragments?
#[[indices]]
#applyFilter = false
#name = 'fragmentrefs'
#type = 'fragments'
#weight = 80

Und mein Partial (layouts/_partials/block/related.html), welcher in layouts/block/single.html eingebunden wird, so:

 layouts/_partials/block/related.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{{ $related := .Site.RegularPages.Related . | first 5 }}
{{ with $related }}
<h3>Ähnliche Beiträge</h3>
<ul>
  {{ range . }}
    {{ if not (in .Params.Tags "onlyfeed") }}
      <li><a href="{{ .Permalink }}">{{ .Title }}</a></li>
    {{ end }}
  {{ end }}
</ul>
{{ end }}

Naechste / Vorherige Seite #

Auch das wollte ich schon immer mal haben, eine einfache Navigation zum vorherigen bzw. naechsten Beitrag oder Rezept. Und auch das ist mit Hugo recht einfach moeglich:

Hierzu nutzen wir einfach die Variablen , die Hugo uns fuer eine Seite zur Verfuegung stellt. Meine Datei layouts/_partials/page/prev_next.html, die z.B. in layouts/block/single.html eingebunden wird, sieht so aus (unter Nutzung von Icons):

 layouts/_partials/page/prev_next.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<hr class="my-3 bg-gradient-to-r from-emerald-500 dark:from-emerald-300 h-0.5 border-0">
<div class="grid grid-cols-2">
  <div>
    {{ partial "page/prevpage" . }}
  </div>
  <div class="text-right">
    {{ partial "page/nextpage" . }}
  </div>
</div>
<hr class="my-3 bg-gradient-to-l from-emerald-500 dark:from-emerald-300 h-0.5 border-0">

Tagcloud #

Wie in der alten Version, generiert durch ein paar Bashskripte, wollte ich wieder eine Tagcloud . Das ist natuerlich auch mit Hugo moeglich (Taxonomy als Stichwort ).

Diesmal allerdings nicht direkt nativ, d.h. ich habe mir den Code von verschiedenen Quellen angeguckt und schliesslich von Artem Sidorenko uebernommen . Danke!

Die Seite an sich ist die Uebersichtsseite der Tags, die via layouts/tags/list.html modifiziert werden kann. Dort habe ich einfach ein Partial namens layouts/_partials/term_cloud.html angelegt mit folgendem Inhalt:

 layouts/_partials/term_cloud.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{{ if not (eq (len $.Site.Taxonomies.tags) 0) }}
    {{ $fontUnit := "em" }}
    {{ $largestFontSize := 2.0 }}
    {{ $largestFontSize := 2.5 }}
    {{ $smallestFontSize := 1.0 }}
    {{ $fontSpread := sub $largestFontSize $smallestFontSize }}
    {{ $max := add (len (index $.Site.Taxonomies.tags.ByCount 0).Pages) 1 }}
    {{ $min := len (index $.Site.Taxonomies.tags.ByCount.Reverse 0).Pages }}
    {{ $spread := sub $max $min }}
    {{ $fontStep := div $fontSpread $spread }}

    <div id="tag-cloud" style="padding: 5px 15px">
        {{ range $name, $taxonomy := $.Site.Taxonomies.tags }}
            {{ $currentTagCount := len $taxonomy.Pages }}
            {{ $currentFontSize := (add $smallestFontSize (mul (sub $currentTagCount $min) $fontStep) ) }}
            {{ $count := len $taxonomy.Pages }}
            {{ $weight := div (sub (math.Log $count) (math.Log $min)) (sub (math.Log $max) (math.Log $min)) }}
            {{ $currentFontSize := (add $smallestFontSize (mul (sub $largestFontSize $smallestFontSize) $weight) ) }}
            --Current font size: {{$currentFontSize}}-->
            <a href="{{ "/tags/" | relLangURL }}{{ $name | urlize }}" style="font-size:{{$currentFontSize}}{{$fontUnit}}">{{ $name }}<sup>{{ $count }}</sup></a>
        {{ end }}
    </div>
{{ end }}

Mit ausgelagertem CSS waere das natuerlich ebenso moeglich gewesen …

Weitere Inspirationen fuer Tagclouds .

Update (2024-07-07): Da ich letztens ueber Kategorien vs Schlagwörter gestolpert bin: Ich habe den Einsatz von Kategorien in Hugo noch nie verstanden und habe deshalb auch keine im Einsatz. Tags reichen mir vollkommen aus.

URL-Schema #

Dank Hugo ist das sehr einfach moeglich. Generell nutze ich nun keine Endung (genannt Ugly URLs ) mehr bei den Eintraegen/Seiten (also z.B. kein $timestamp.htm bei den Blockeintraegen), was in Hugo der Default ist. Allerdings sollten die Blockposts anders dargestellt werden. Das geht in der Config so :

[permalinks]
  block = "/block/:year/:month/:day/:slug/"

Suche #

Update (2022-08-06): Seit Jahren habe ich mich auf externe Suchmaschinen verlassen, wollte aber schon immer eine “eigene” Suche haben. Ich hatte auch schon des oefteren angefangen zu recherchieren, was ich denn nun eigentlich einsetzen will, habe mich aber nicht zu einer Loesung durchringen koennen. Im Raum standen z.B. eine Suche mit Lunr oder mit einem JSON Feed .

Der Nachteil dabei wurde in den letzten Tagen auf Brain Baking beschrieben (dieser Feed oder Index kann je nach Umfang der Website dann natuerlich auch selbst sehr gross sein und muss komplett client-seitig heruntergeladen werden).

Und daher setze ich nun, wie auch viele andere in den letzten Wochen auf das noch recht junge Projekt Pagefind , was die Suche auf allen Seiten vereinfacht, da hier mit Fragmenten gearbeitet wird und der Index erstellt wird, nachdem die Website gebaut wurde 😊

Der Einbau war sehr einfach, ich habe einfach ein Partial layouts/_partials/search-pagefind.html erstellt und es auf einer Seite eingebunden:

 layouts/_partials/search-pagefind.html

1
2
3
4
5
6
7
8
<link href="/pagefind/pagefind-ui.css" rel="stylesheet">
<script src="/pagefind/pagefind-ui.js"></script>
<div id="search"></div>
<script>
    window.addEventListener('DOMContentLoaded', (event) => {
        new PagefindUI({ element: "#search", showImages: false, resetStyles: true, autofocus: true });
    });
</script>

Zusaetzlich habe ich in meinem GitHub Workflow die folgenden Zeilen ergaenzt (der gesamte Workflow steht weiter unten):

  - name: Run Pagefind
    run: npx -y pagefind --site "public"

Und das wars auch schon, die Suche war nach wenigen Stunden live 🎉

Update (2024-04-02): Ein lang ersehntes Feature war der Autofocus in der Suchleiste (siehe Issue dazu ), was ich nun dank Pagefind v1.1.0 auch endlich einbauen konnte.

Update (2024-10-06): Um Seiten von der Suche auszuschliessen habe ich folgendes in layouts/baseof.html drin stehen (mehr zu Onlyfeed):

<main{{ if and (not (in .Params.Tags "onlyfeed")) (ne .Kind "term") }} data-pagefind-body{{ end }}>

Statistik #

Bisher wurde die Seite Statistik mit einem Bashscript gebaut (via Cronjob).

alte Statistikseite
alte Statistikseite

Dabei wurden nicht nur Eintraege, Linkdumps, Rezepte usw. gezaehlt, sondern auch die Haeufigkeit von Suchmaschineneingaben (ausgelesen ueber die Webserverlogs). Das funktioniert mit der Nutzung von GitHub Actions jetzt nicht mehr so einfach (auch die vorherigen Zaehlungen koennen mit Hugo nur schwierig abgebildet werden). Daher halt eben nur eine kleinere Statistik. 😊

Allerdings wuerde ich gerne noch ein paar kleine Dinge mit einbauen:

  • die Hugo Statistik, die beim Bauen angezeigt wird
  • eine Auflistung aller Dateiendungen sortiert nach der Haeufigkeit

Ersteres laesst sich generieren ueber einen normalen Durchlauf von Hugo, wobei der Output in eine Datei geschrieben wird.

Zweiteres funktioniert mit einem find-Befehl , der ebenfalls in eine Datei geschrieben wird.

Beide Dateien werden anschliessend bei einem (zweiten) Durchlauf mit Hilfe von readfile eingebunden.

# Hugo Statistik
$ hugo | grep -v "^INFO" | sed '1d'
hugo v0.90.1+extended darwin/amd64 BuildDate=unknown
INFO 2021/12/11 20:14:31 syncing static files to /

                   |  DE
-------------------+-------
  Pages            | 1099
  Paginator pages  |    0
  Non-page files   |  907
  Static files     |  125
  Processed images |  805
  Aliases          |  287
  Sitemaps         |    1
  Cleaned          |    0

Built in 1370 ms
# Auflistung aller Dateiendungen sortiert nach der Haeufigkeit (erstmal nur die letzten 10)
$ find . -type f | grep -oE '\.(\w+)$' | sort | uniq -c | sort | tail
  26 .woff
  26 .woff2
 195 .png
 272 .xml
 278 .js
 512 .md
1057 .html
1663 .svg
1786 .webp
2215 .jpg

Beides habe ich noch nicht umgesetzt, das steht noch auf meiner TODO-Liste.

Update (2024-04-05): Beides habe ich mittlerweile umgesetzt. Hierbei kommt das Update v0.124.0 mit Segments zum Einsatz. Die genauen Befehle sind im GitHub Workflow ersichtlich. In meiner hugo.toml habe ich lediglich folgendes definiert:

[segments]
  [segments.statistics]
[[segments.statistics.includes]]
      path = '{/statistics}'

Changelog / Version #

Bisher habe ich Aenderungen in einer Datei namens changelog.txt festgehalten, diese wurde nun durch eine regulaere Seite ersetzt. Zusaetzlich gab es noch eine Datei namens version.txt, die ist nun aber auch im Changelog aufgegangen.

Lange Zeit hatte ich nur Major- und Minor-Versionen festgehalten, nun kehre ich wieder zu Semantic Versioning zurueck, also zusaetzlich mit Patch-Version.

Schoen waere noch ein automatischer Changelog, zusammengebaut via Git Log und/oder Tags. Aber mal gucken, steht als Low Prio auf meiner TODO-Liste.

Update (2024-04-05): Diese Idee habe ich mittlerweile komplett verworfen, siehe dazu den Eintrag zum Changelog.

DSGVO #

Es gibt zwar ein paar Moeglichkeiten, die Standard-Shortcodes etwas datenschutzfreundlicher zu gestalten (Bountysource ), allerdings gehen diese mir nicht weit genug. So wird bei einem Seitenaufruf immer noch Zeugs von anderen Websites nachgeladen, was ich natuerlich nicht will (und ihr wohl auch nicht).

Daher habe ich mir die Muehe gemacht, selber was zu basteln. gist und twitter kommen ohne sonstige Skripte aus, bei youtube und vimeo ist noch ein Skript notwendig, was das Thumbnail runterlaedt und auf die passende Groesse von 560x560 (siehe geometry bei imagemagick ) umwandelt. instagram habe ich bisher nicht gebraucht und github (als Alternative zu gist) sowie osm habe ich neu gebaut. spotify wollte ich auch noch bauen, aber die API ist doof 😉

Ansonsten kann noch durch interne Templates Google Analytics sowie Disqus verwendet werden. Ersteres ist bei mir nicht im Einsatz und selbst wenn sollte lieber Matomo , umami , Plausible Analytics oder GoatCounter verwendet werden. Auf Zweiteres gehe ich unter Kommentare naeher drauf ein.

Gist #

Die Einbindung von Gists ist mir nicht datensparsam genug (es werden Sachen von GitHub nachgeladen). Zum Glueck gibt es die Moeglichkeit, per API u.a. den Code abzufragen. Im Gegensatz zu dem offiziellen Hugo Shortcode benoetigt mein Shortcode (layouts/_shortcodes/gist.html) mit “Positional Parameters” nicht den Namen, sondern nur die Gist ID und ggf. einen Dateinamen, wenn der Gist mehrere Dateien enthaelt, aber nur eine angezeigt werden soll:

 layouts/_shortcodes/gist.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{{- /*
  https://github.com/gohugoio/hugo/blob/master/tpl/tplimpl/embedded/templates/shortcodes/gist.html
  https://docs.github.com/en/rest/gists/gists?apiVersion=2022-11-28#get-a-gist
*/ -}}
{{- $id := .Get 0 | string -}}
{{- $file := .Get 1 | string -}}
{{- $opts := .Get "opts" | default (printf "%s%s" "linenos=table,anchorlinenos=true,lineanchors=" $file) | string -}}
{{- $url := printf "%s/%s" "https://api.github.com/gists" $id -}}
{{ $url_opts := dict
  "headers" (dict "Accept" "application/vnd.github+json" "Authorization" (printf "Bearer %s" (getenv "HUGO_GITHUB_TOKEN")) "X-GitHub-Api-Version" "2022-11-28")
}}
{{- with try (resources.GetRemote $url $url_opts) -}}
  {{- with .Err -}}
    {{- warnf "%s" . -}}
  {{- else with .Value -}}
    {{- $json := transform.Unmarshal .Content -}}
    {{- range $json.files -}}
    {{- if or (eq $file "") (eq .filename $file) -}}
    <div class="relative">
    <p class="absolute text-[90%] top-[-30px] bg-[#eee8d5] dark:bg-[#002b36] text-[#899c9c] m-0 pt-[5px] pb-2.5 px-[15px] rounded-[5px] right-0"><i class="fa-brands fa-github"></i>&nbsp;
    {{ with print "[" .filename "](" $json.html_url ")" }}{{ $.Page.RenderString . }}{{ end }}
    </p>
    {{ transform.Highlight .content .language $opts }}
    </div>
    {{- end -}}
    {{- end -}}
  {{- else -}}
    {{- warnf "Unable to get remote resource %q" $url -}}
  {{- end -}}
{{- end -}}

Die Magie passiert via getJSON , das Styling passiert via CSS bzw. Tailwind CSS und ist in Codebloecke naeher beschrieben.

Ein Beispiel Gist mit mehreren Dateien , wovon nur eine angezeigt werden soll, sieht dann so aus:

{{< gist a900acbf7140c18217dc7a1679c52114 "example-bower.json" >}}

  example-bower.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
{
  "name": "gist-embed",
  "authors": [
    "Blair Vanderhoof"
  ],
  "description": "Ultra powered gist embedding for your website http://blairvanderhoof.com/gist-embed/",
  "main": "gist-embed.js",
  "keywords": [
    "gist",
    "embed",
    "github",
    "ajax"
  ],
  "license": "BSD-2-Clause",
  "homepage": "https://github.com/blairvanderhoof/gist-embed",
  "repository": {
    "type": "git",
    "url": "https://github.com/blairvanderhoof/gist-embed.git"
  }
}

GitHub #

Ganz aehnlich zu dem Shortcode fuer ein Gist ist layouts/_shortcodes/github.html mit “Named Parameters” aufgebaut:

 layouts/_shortcodes/github.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{{- /*
  https://github.com/haideralipunjabi/hugo-shortcodes/tree/master/github
  https://docs.github.com/en/rest/repos/contents
*/ -}}
{{- $repo := .Get "repo" | string -}}
{{- $file := .Get "file" | string -}}
{{- $lang := .Get "lang" | default "txt" | string -}}
{{- $opts := .Get "opts" | default (printf "%s%s" "linenos=table,anchorlinenos=true,lineanchors=" $file) | string -}}
{{- $url := printf "%s/%s/%s/%s" "https://api.github.com/repos" $repo "contents" $file -}}
{{ $url_opts := dict
  "headers" (dict "Accept" "application/vnd.github.object" "Authorization" (printf "Bearer %s" (getenv "HUGO_GITHUB_TOKEN")) "X-GitHub-Api-Version" "2022-11-28")
}}
{{- with try (resources.GetRemote $url $url_opts) -}}
  {{- with .Err -}}
    {{- warnf "%s" . -}}
  {{- else with .Value -}}
    {{- $json := transform.Unmarshal .Content -}}
    {{- $data := encoding.Base64Decode $json.content -}}
    <div class="relative">
    <p class="absolute text-[90%] top-[-30px] bg-[#eee8d5] dark:bg-[#002b36] text-[#899c9c] m-0 pt-[5px] pb-2.5 px-[15px] rounded-[5px] right-0"><i class="fa-brands fa-github"></i>&nbsp;
    {{ with print "[" $json.name "](" $json.html_url ")" }}{{ $.Page.RenderString . }}{{ end }}
    </p>
    {{ transform.Highlight $data $lang $opts }}
    </div>
  {{- else  -}}
    {{- warnf "Unable to get remote resource %q" $url -}}
  {{- end -}}
{{- end -}}

In diesem Shortcode muss die Sprache mit angegeben werden, da die API diese leider nicht mit ausgibt. Zusaetzlich gibt es noch die Moeglichkeit, dem Shortcode highlight Optionen mitzugeben:

{{< github repo="tohn/aa3d-tools" file="dm2txt.py" lang="py3" >}}

  dm2txt.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#!/usr/bin/env python
"""convert a depth map image to a 3d txt map to be used by `aa3d`"""

# inspiration:
# https://github.com/RameshAditya/asciify/blob/master/asciify.py

import sys
try:
    from PIL import Image
except ImportError:
    print("Unable to import pillow")
    sys.exit(1)

# main
#   - takes as parameters the image path [width and intensity]
#   - converts an image to a txt map
#   - prints result to console
if __name__ == '__main__':
    # try to load image
    try:
        image = Image.open(sys.argv[1])
    except IndexError:
        print("No image given")
        sys.exit(1)
    except IOError:
        print("Unable to load image")
        sys.exit(1)
    # set width (default: 80), in the range of 1-500
    try:
        WIDTH = max(1, min(abs(int(sys.argv[2])), 500))
    except (IndexError, ValueError):
        WIDTH = 80
    # set layers (default: 9), in the range of 1-9
    try:
        LAYERS = max(1, min(abs(int(sys.argv[3])), 9))
    except (IndexError, ValueError):
        LAYERS = 9
    # resize image
    image.thumbnail((WIDTH, WIDTH))
    # greyscale (8-bit pixels, black and white) since the input should
    # be like this anyways (this will also provide just one integer in
    # the range of 0-255 instead of a tuple)
    image = image.convert('L')
    # convert every pixel to a value 0-9 corresponding to their intensity
    pixels = [list(map(str, range(0, 9)))[p//(256//LAYERS)] for p in list(image.getdata())]
    # and join the result
    PIXELS_RES = ''.join(pixels)
    # construct the image from the character list
    new_image = [PIXELS_RES[i:i+WIDTH] for i in range(0, len(PIXELS_RES), WIDTH)]
    # and print the resulting lines
    print('\n'.join(new_image))

Eine Alternative kommt vom Hugo Entwickler Joe Mooring : Syntax highlighting for GitHub files .

Instagram #

Der Standard-Shortcode von Instagram will ich nicht einsetzen, da er direkt Daten von Instagram nachlaedt. Allerdings braucht die API viel zu viele Daten von mir und daher ganz ehrlich: war mir zuviel Arbeit, kommt vielleicht irgendwann™.

Kommentare #

Vor Jahren habe ich mich ja eigentlich gegen Kommentare entschieden. Allerdings bringt Hugo schon Support fuer Disqus mit , was die Einbindung natuerlich recht einfach macht.

Wobei Disqus aus Datenschutzgruenden natuerlich nicht die beste Wahl ist. Auf der Seite im Wiki werden aber Alternativen aufgelistet .

Isso kenne ich schon sehr lange und hatte es probeweise auch schon im Einsatz, allerdings habe ich es noch nicht mit Docker am Laufen. Und die Python-Dependency-Hell will ich mir nicht nativ antun.

tl;dr Kommentare will ich haben, allerdings habe ich mich noch nicht fuer eine Loesung entschieden, daher ist das noch ein TODO auf meiner Liste 😉

OpenStreetMaps #

Statt Google Maps sollte OpenStreetMaps verwendet werden. Doch wie datenschutzfreundlich einbinden? Denn der Standard Embed-Code laedt $Dinge direkt von OpenStreetMaps …

<iframe width="425" height="350" frameborder="0" scrolling="no" marginheight="0" marginwidth="0" src="https://www.openstreetmap.org/export/embed.html?bbox=-10.315990447998049%2C51.53373644524688%2C-10.150337219238283%2C51.62846065474101&layer=mapnik&marker=51.5810798959352%2C-10.233279168605804" style="border: 1px solid black">iframe><br/><small><a href="https://www.openstreetmap.org/?mlat=51.5811&mlon=-10.2333#map=13/51.5811/-10.2332">View Larger Mapa>small>

Daher habe ich folgenden Shortcode layout/_shortcodes/osm.html gebaut:

 layouts/_shortcodes/osm.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{{- $bbox := index .Params 0 -}}
{{- $marker := index .Params 1 | default "" -}}
{{- $mlat := index .Params 2 | default "" -}}
{{- $mlon := index .Params 3 | default "" -}}
{{- $z := index .Params 4 | default "" -}}
{{- $code_safejs := safeJS (md5 (printf "%s_%s" $bbox $marker)) -}}
<div id="wrapper-{{ $code_safejs }}" class="relative w-[560px] h-[315px] overflow-hidden mx-auto my-2.5">
  <div id="container-{{ $code_safejs }}" class="h-full">
    <script>
    function loadIframe_{{ $code_safejs }}() {
      var e1 = document.getElementById('info-{{ $code_safejs }}');
      e1.parentNode.removeChild(e1);
      var e2 = document.getElementById('image-{{ $code_safejs }}');
      e2.parentNode.removeChild(e2);
      document.getElementById('container-{{ $code_safejs }}').innerHTML = '<iframe style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" src="https://www.openstreetmap.org/export/embed.html?bbox={{ $bbox }}&layer=mapnik&marker={{ $marker }}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen"></iframe>';
    }
    </script>
    <div id="info-{{ $code_safejs }}" class="text-center absolute h-full w-full z-10 bg-[rgba(255,255,255,0.75)] opacity-[75%] overflow-auto">
      <p class="pt-[20%]"><i class="fa-solid fa-map cursor-pointer text-6xl text-black opacity-60 hover:text-[#89c261] hover:opacity-100" onclick="loadIframe_{{ $code_safejs }}();"></i></p>
      <p class="text-[#242424] dark:text-zinc-800">
        {{ with print "Die Karte wird von [OpenStreetMap](https://www.openstreetmap.org) eingebettet." }}{{ $.Page.RenderString . }}{{ end }}
        <br>
        {{ with print "Es gelten die [Datenschutzerklaerungen von OpenStreetMap](https://wiki.osmfoundation.org/wiki/Privacy_Policy)." }}{{ $.Page.RenderString . }}{{ end }}
      </p>
    </div>
    <div id="image-{{ $code_safejs }}" class="blur-[5px] h-full dark:brightness-[0.8] dark:contrast-[1.2]" style="background:url('{{ .Site.BaseURL | absURL }}/iframe/osm/osm.png');">
    </div>
  </div>
</div>
<div id="small-{{ $code_safejs }}" class="text-sm ml-[75px] -mt-2.5 pb-2.5"><i class="fa-solid fa-magnifying-glass-location"></i>
  {{ with print "[Karte auf OpenStreetMap ansehen](https://www.openstreetmap.org/?mlat=" $mlat "&mlon=" $mlon "#map=" $z "/" $mlat "/" $mlon ")." }}{{ $.Page.RenderString . }}{{ end }}
</div>

Hierbei wird erstmal nur ein Bild mit Text im Vordergrund angezeigt. Erst bei Klick auf das Icon () in der Mitte wird die Karte in einem Iframe nachgeladen. Alternativ kann der Link unter dem

genutzt werden, der direkt auf die OpenStreetMap-Website verlinkt.

Den Code dafuer habe ich fuer youtube uebernommen aus einem Beitrag von Florian Meier (Code auf GitLab ) mit einigen Anpassungen und ihn dann fuer OpenStreetMap nochmals angepasst:

  • Nutzung von getJSON bzw. der YouTube-API , um Informationen ueber das Video zu bekommen und in dem Container darzustellen (in diesem Fall nicht notwendig) und damit auch Anpassung des Textes

    Info

    Laut einem Issue ist .RenderString besser in solchen Situationen geeignet als markdownify.
  • Nutzung eines Icons von Font Awesome als Nachlade-Button

  • Nutzung eines Scriptes, um das Vorschaubild im Hintergrund runterzuladen (mehr dazu in vimeo); in diesem Fall wird ein statisches Bild genutzt, siehe unten

Der notwendige CSS-Code zum Stylen ist dank Tailwind CSS direkt im Shortcode enthalten.

Und hier noch das Bild, was im Hintergrund angezeigt wird (abgewandelt vom Original ):

OpenStreetMap Logo
OpenStreetMap Logo

Abgeleitet vom Embed-Code sieht mein Shortcode dann wie folgt aus:

{{< osm "-10.315990447998049%2C51.53373644524688%2C-10.150337219238283%2C51.62846065474101" "51.5810798959352%2C-10.233279168605804" "51.5811" "-10.2333" "13" >}}

Die Karte wird von OpenStreetMap eingebettet.
Es gelten die Datenschutzerklaerungen von OpenStreetMap .

Spotify #

Aehnlich zu osm, vimeo und youtube habe ich mir auch das einfach selbst gebastelt bzw. es erstmal nur versucht.

Denn das Problem war: Hugo konnte noch nicht mit POST-Requests umgehen. Zuerst hatte ich mir daher mit curl einen Access Token besorgt und den dann mit Hugo genutzt. Allerdings ist all das jetzt mit resources.GetRemote (seit Version 0.91.0 ) moeglich 🎉

Dank der kurzen Haltbarkeit des access_tokens von nur einer Stunde ist die lokale Entwicklung aber sehr nervig. Daher im Folgenden nur ein Proof of Concept .

Nach etwas Hilfe aus der Community hatte ich dann auch meinen Shortcode layouts/_shortcodes/spotify.html zusammengebaut:

 layouts/_shortcodes/spotify.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
{{/* spotify.html
  https://developer.spotify.com/documentation/web-api/reference/
  https://developer.spotify.com/documentation/general/guides/authorization-guide/#client-credentials-flow
  https://github.com/Flowm/spotify-api-bash/blob/master/create_playlist_from_artists_list.sh

  spotify album 1KAg47NePhjsKC4Y8ZC9z3
  spotify show 1O9vyJwNvUcdq1d9vFblQw 0
  spotify playlist 37i9dQZF1DXc51TI5dx7RC
  spotify track 2TpxZ7JUBn3uw46aR7qd6V
*/}}
{{/* - $code_safejs := "" - */}}
{{- $_time := "" -}}
{{- $market := "DE" -}}
{{- $type := index .Params 0 -}}
{{- $code := index .Params 1 -}}
{{- $time := index .Params 2 | default "" -}}
{{/* - if eq $type "show" - */}}
{{/* - $code_safejs := safeJS (md5 (printf "%s_%s_%s" $type $code $time)) - */}}
{{/* - $_time := printf "%s%s" "&t=" $time - */}}
{{/* - else - */}}
{{- $code_safejs := safeJS (md5 (printf "%s_%s" $type $code)) -}}
{{/* - end - */}}
{{- $base64 := base64Encode (printf "%s:%s" (getenv "HUGO_SECRET_SPOTIFY_ID") (getenv "HUGO_SECRET_SPOTIFY_SECRET")) -}}
{{- $base64_2 := printf "%s %s" "Basic" $base64 -}}
{{- $opts := dict
  "method" "post"
  "headers" (dict
    "Authorization" $base64_2
    "Content-Type" "application/x-www-form-urlencoded"
  )
  "body" "grant_type=client_credentials"
-}}
{{- $postResponse := resources.GetRemote "https://accounts.spotify.com/api/token" $opts | transform.Unmarshal -}}
{{- $headers := dict "Authorization" (printf "Bearer %s" $postResponse.access_token) -}}
{{/* - TODO fixme | $json := getJSON "https://api.spotify.com/v1/" $type "s/" $code "?market=" $market $headers - */}}
<div id="wrapper-{{ $code_safejs }}" class="relative w-[560px] h-[380px] overflow-hidden mx-auto my-2.5">
  <div id="container-{{ $code_safejs }}" class="h-full">
    <script>
    function loadIframe_{{ $code_safejs }}() {
      var e1 = document.getElementById('info-{{ $code_safejs }}');
      e1.parentNode.removeChild(e1);
      var e2 = document.getElementById('image-{{ $code_safejs }}');
      e2.parentNode.removeChild(e2);
      document.getElementById('container-{{ $code_safejs }}').innerHTML = '<iframe src="https://open.spotify.com/embed/{{ $type }}/{{ $code }}{{ $_time }}" width="100%" height="380" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen"></iframe>';
    }
    </script>
    <div id="info-{{ $code_safejs }}" class="text-center absolute h-full w-full z-10 bg-[rgba(255,255,255,0.75)] overflow-auto">
      <p class="pt-[20%]"><i class="fa-brands fa-spotify cursor-pointer text-6xl text-black opacity-60 hover:text-[#1db954] hover:opacity-100" onclick="loadIframe_{{ $code_safejs }}();"></i></p>
      <p class="p-[3px] dark:text-zinc-800">
        {{/* with print "[" $json.name "](" $json.external_urls.spotify ") wird von [Spotify](https://www.spotify.com) eingebettet." }}{{ $.Page.RenderString . }}{{ end */}}
        <br>
        {{ with print "Es gelten die [Datenschutzerklaerungen von Spotify](https://www.spotify.com/de/legal/privacy-policy/)." }}{{ $.Page.RenderString . }}{{ end }}
      </p>
    </div>
    <div id="image-{{ $code_safejs }}" class="blur-[5px] h-full dark:brightness-[0.8] dark:contrast-[1.2]" style="background:url('{{ .Site.BaseURL | absURL }}/iframe/spotify/{{ $code }}.webp');">
    </div>
  </div>
</div>

Um mit der Spotify-API zu reden, brauchen wir eine ID und ein Secret . Die koennen wir in Hugo mit Environment Variablen einbinden. Ich habe dafuer eine gute Loesung gefunden, diese Variablen abhaengig vom Ordner einzubinden, naemlich mit direnv 🎉

Nach der Installation und der Einbindung in die Shell , erstellen wir im aktuellen Hugo-Verzeichnis eine Datei .envrc:

export HUGO_SECRET_SPOTIFY_ID="012"
export HUGO_SECRET_SPOTIFY_SECRET="789"

Diese Datei erlauben wir jetzt mit direnv allow . und koennen danach Hugo starten:

hugo server -v -D

Nun werden die Variablen genutzt und die API kann angesprochen werden 🎉

Um jetzt noch die Hintergrundbilder runterladen zu koennen, habe ich folgendes Script in bin/sm.sh gebaut, was rg (ripgrep, einfach viel schneller als grep), youtube-dl (nur fuer Vimeo und YouTube) und convert (imagemagick ) benoetigt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/bin/bash

set -e -o pipefail

# Script to download thumbnails from spotify, vimeo or youtube

for exe in rg youtube-dl convert ; do
  command -v "$exe" >/dev/null 2>&1 || { echo >&2 "I require \"$exe\" but it's not installed. Aborting."; exit 1; }
done

function spotify() {
  # get access-token
  _id=$(env | grep HUGO_SECRET_SPOTIFY_ID | cut -d= -f2)
  _secret=$(env | grep HUGO_SECRET_SPOTIFY_SECRET | cut -d= -f2)
  _base64=$(echo -n "$_id:$_secret" | base64)
  _at=$(curl -s -X "POST" -H "Authorization: Basic $_base64" -d grant_type=client_credentials https://accounts.spotify.com/api/token | jq .access_token | tr -d '"')

  rg -I "\{\{< spotify" .. | sort | uniq | \
    while read -r _ _ type code _ ; do
      _code=$(echo "$code" | tr -d '"')
      _site="../static/iframe/spotify"
      if [[ ! -d $_site ]] ; then mkdir -p "$_site" ; fi
      dst="$_site/$_code"
      if [[ ! -e "$dst.webp" && ! -e "$dst.jpg" ]] ; then
        echo "spotify: $code"
        _img=$(curl -s -X "GET" "https://api.spotify.com/v1/${type}s/$code?market=DE" -H "Accept: application/json" -H "Content-Type: application/json" -H "Authorization: Bearer $_at" | jq .images[].url | sed -n '1p' | tr -d '"')
        curl -s -o "../static/iframe/spotify/$code.jpg" "$_img"
        convert "$dst.*" -resize "560x560" "$dst.webp"
        rm "$dst.jpg"
      fi
    done
}

spotify

Das Styling ist nun auch dank Tailwind CSS direkt im Shortcode enthalten. Als Hover Farbe des Buttons habe ich #1db954 im Einsatz (um die genaue Farbe von Logos/Brands herauszufinden, kann z.B. BrandColors genutzt werden).

Insgesamt sieht der Shortcode dann wie folgt aus:

{{< spotify album 25r7pEf31viAbsoVHC6bQ4 >}}

Da wie oben beschrieben der access_token nur fuer eine Stunde gueltig ist, muesste ich lokal entweder jede Stunde Hugo neu starten oder den Parameter --ignoreCache=true anhaengen:

hugo server -v -D --ignoreCache=true

Da beides mega nervig ist, gibt es den Spotify Shortcode nur als Bilder 😉

Spotify Einbindung mit Bild im Hintergrund
Spotify Einbindung mit Bild im Hintergrund
Spotify Einbindung nach Klick auf das Logo
Spotify Einbindung nach Klick auf das Logo

Update (2024-07-07): Vom Hugo Entwickler Joe Mooring gibt es Spotify widgets , die ich aber so nicht nutzen wuerde (DSGVO und so).

Twitter #

Twitter habe ich dank Elon Musk nicht mehr im Einsatz.

Vimeo #

Wie bei osm schon beschrieben, habe ich den Code fuer youtube auch fuer Vimeo verwendet, da mir auch hier der Standard-Shortcode nicht weit genug geht. Angepasst fuer Vimeo sieht layouts/_shortcodes/vimeo.html so aus:

 layouts/_shortcodes/vimeo.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
{{- /*
  https://developer.vimeo.com/api/guides/start
  https://developer.vimeo.com/api/reference/videos
  https://github.com/gohugoio/hugo/blob/master/tpl/tplimpl/embedded/templates/shortcodes/vimeo.html
  https://github.com/jmooring/hugo/blob/master/tpl/tplimpl/embedded/templates/shortcodes/vimeo_simple.html
*/ -}}
{{- $code := index .Params 0 | string -}}
{{- $time := index .Params 1 | default 0 | int -}}
{{- $code_safejs := safeJS (md5 (printf "%s_%s" $code $time)) -}}
{{- $url := urls.JoinPath "https://vimeo.com" $code -}}
{{- $query := querify "url" $url "dnt" 1 -}}
{{- $request := printf "https://vimeo.com/api/oembed.json?%s" $query -}}
{{- with resources.GetRemote $request -}}
{{- with . | transform.Unmarshal -}}
<div id="wrapper-{{ $code_safejs }}" class="relative w-[560px] h-[315px] overflow-hidden mx-auto my-2.5">
  <div id="container-{{ $code_safejs }}" class="h-full">
    <script>
    function loadIframe_{{ $code_safejs }}() {
      var e1 = document.getElementById('info-{{ $code_safejs }}');
      e1.parentNode.removeChild(e1);
      var e2 = document.getElementById('image-{{ $code_safejs }}');
      e2.parentNode.removeChild(e2);
      document.getElementById('container-{{ $code_safejs }}').innerHTML = '<iframe style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" src="https://player.vimeo.com/video/{{ $code }}?autoplay=1&dnt=1#t={{ $time }}" width="560" height="315" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" title="{{ .title }}"></iframe>';
    }
    </script>
    <div id="info-{{ $code_safejs }}" class="text-center absolute h-full w-full z-10 bg-[rgba(255,255,255,0.75)] opacity-[75%] overflow-auto">
      <p class="pt-[20%]"><i class="fa-brands fa-vimeo cursor-pointer text-6xl text-black opacity-60 hover:text-[#1ab7ea] hover:opacity-100" onclick="loadIframe_{{ $code_safejs }}();"></i></p>
      <p class="p-[3px] dark:text-zinc-800">
        {{ with print "Das Video [" .title "](https://vimeo.com/" $code "#t=" $time ") (ID: " $code ") wird von [Vimeo](https://vimeo.com) eingebettet." }}{{ $.Page.RenderString . }}{{ end }}
        <br>
        {{ with print "Es gelten die [Datenschutzerklaerungen von Vimeo](https://vimeo.com/de/features/video-privacy)." }}{{ $.Page.RenderString . }}{{ end }}
      </p>
    </div>
    <div id="image-{{ $code_safejs }}" class="blur-[5px] h-full dark:brightness-[0.8] dark:contrast-[1.2]" style="background:url('{{ .Site.BaseURL | absURL }}/iframe/vimeo/{{ $code }}.webp');">
    </div>
  </div>
</div>
{{- end -}}
{{- end -}}

Das Styling ist wieder direkt im Shortcode, als Hover Farbe des Buttons habe ich #1ab7ea genutzt.

Um mit der Vimeo-API zu reden, brauchen wir ein Secret . Das koennen wir in Hugo mit Environment Variablen einbinden. Hierzu kann die Loesung, die ich bei Spotify beschrieben habe ebenfalls verwendet werden. Die .envrc habe ich einfach mit der folgenden Zeile erweitert:

export HUGO_SECRET_VIMEO="abc"

Um jetzt noch die Hintergrundbilder runterladen zu koennen, habe ich das Script bin/sm.sh von Spotify erweitert:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# siehe bei Spotify

function sm() {
  site="$1"
  url="$2"
  rg -I "\{\{< $site" .. | sort | uniq | \
    while read -r _ _ code _ ; do
      _code=$(echo "$code" | tr -d '"')
      _site="../static/iframe/$site"
      if [[ ! -d $_site ]] ; then mkdir -p "$_site" ; fi
      dst="$_site/$_code"
      if [[ ! -e "$dst.webp" && ! -e "$dst.jpg" ]] ; then
        echo "$site: $code"
        youtube-dl --abort-on-error -w --no-warnings \
          --skip-download -o "../static/iframe/$site/%(id)s" \
          --write-thumbnail --playlist-items 1 \
          "$url$code"
        convert "$dst.*" -resize "560x560" "$dst.webp"
      fi
    done
}

sm "vimeo" "https://vimeo.com/"
sm "youtube" "https://www.youtube.com/watch?v="

Insgesamt sieht der Shortcode dann wie folgt aus:

{{< vimeo 22439234 >}}

Das Video The Mountain (ID: 22439234) wird von Vimeo eingebettet.
Es gelten die Datenschutzerklaerungen von Vimeo .

Optional kann noch die Startzeit in Sekunden mit angegeben werden:

{{< vimeo 22439234 83 >}}

Das Video The Mountain (ID: 22439234) wird von Vimeo eingebettet.
Es gelten die Datenschutzerklaerungen von Vimeo .

YouTube #

Im Prinzip wurde ja alles schon weiter oben beschrieben. Der Standard-Shortcode gefaellt mir nicht , daher habe ich selbst was gebastelt in layouts/_shortcodes/youtube.html, mit den hier folgenden Influenzen , wobei ich mich mehr an dem Beitrag von Florian Meier (siehe osm) orientiert habe:

 layouts/_shortcodes/youtube.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{{- /*
  https://www.flomei.de/blog/2019/01/04/youtube-datenschutzkonform-einbinden/
  https://developers.google.com/youtube/v3/docs/videos
  https://console.developers.google.com/apis/credentials
*/ -}}
{{- $code := index .Params 0 | string -}}
{{- $time := index .Params 1 | default 0 | int -}}
{{- $code_safejs := safeJS (md5 (printf "%s_%s" $code $time)) -}}
{{- $url := printf "%s%s%s%s" "https://www.googleapis.com/youtube/v3/videos?part=snippet&id=" $code "&key=" (getenv "HUGO_SECRET_YOUTUBE") -}}
{{- with try (resources.GetRemote $url) -}}
  {{- with .Err -}}
    {{- warnf "%s" . -}}
  {{- else with .Value -}}
    {{- $json := unmarshal .Content -}}
    {{- range $json.items -}}
    <div id="wrapper-{{ $code_safejs }}" class="relative w-[560px] h-[315px] overflow-hidden mx-auto my-2.5">
      <div id="container-{{ $code_safejs }}" class="h-full">
        <script>
        function loadIframe_{{ $code_safejs }}() {
          var e1 = document.getElementById('info-{{ $code_safejs }}');
          e1.parentNode.removeChild(e1);
          var e2 = document.getElementById('image-{{ $code_safejs }}');
          e2.parentNode.removeChild(e2);
          document.getElementById('container-{{ $code_safejs }}').innerHTML = '<iframe style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" src="https://www.youtube-nocookie.com/embed/{{ $code }}?autoplay=1&start={{ $time }}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" title="{{ .snippet.title }}"></iframe>';
        }
        </script>
        <div id="info-{{ $code_safejs }}" class="text-center absolute h-full w-full z-10 bg-[rgba(255,255,255,0.75)] overflow-auto">
          <p class="pt-[20%]"><i class="fa-brands fa-youtube cursor-pointer text-6xl text-black opacity-60 hover:text-[#ff0000] hover:opacity-100" onclick="loadIframe_{{ $code_safejs }}();"></i></p>
          <p class="p-[3px] dark:text-zinc-800">
            {{ with print "Das Video [" .snippet.title "](https://www.youtube.com/watch?v=" .id "&t=" $time ") (ID: " $code ") wird von [YouTube](https://www.youtube.com) eingebettet." }}{{ $.Page.RenderString . }}{{ end }}
            <br>
            {{ with print "Es gelten die [Datenschutzerklaerungen von Google](https://www.google.de/intl/de/policies/privacy/)." }}{{ $.Page.RenderString . }}{{ end }}
          </p>
        </div>
        <div id="image-{{ $code_safejs }}" class="blur-[5px] h-full dark:brightness-[0.8] dark:contrast-[1.2]" style="background:url('{{ .Site.BaseURL | absURL }}/iframe/youtube/{{ $code }}.webp');">
        </div>
      </div>
    </div>
    {{- end -}}
  {{- else -}}
    {{- warnf "Unable to get remote resource %q" $url -}}
  {{- end -}}
{{- end -}}

Und auch hier ist das Styling dank Tailwind CSS direkt im Shortcode drin. Als Hover Farbe des Buttons habe ich #ff0000 genutzt.

Um mit der YouTube-API zu reden, brauchen wir ein Secret . Auch dieses Mal koennen wir mit Environment Variablen und direnv (siehe vimeo) das Secret mit Hugo nutzen und erweitern die .envrc um folgende Zeile:

export HUGO_SECRET_YOUTUBE="xyz"

Und ebenfalls wie bei vimeo kann auch das Shellscript zum Runterladen der Hintergruende genutzt werden. Das waere noch was fuer meine TODO-Liste, dass das via Pipeline passieren kann (der Durchlauf wuerde dann aber laenger dauern).

Und so sieht dann der Shortcode aus:

{{< youtube beTqiiV5zhI >}}

Das Video Kinicles (ID: beTqiiV5zhI) wird von YouTube eingebettet.
Es gelten die Datenschutzerklaerungen von Google .

Optional kann noch die Startzeit in Sekunden mit angegeben werden:

{{< youtube beTqiiV5zhI 83 >}}

Das Video Kinicles (ID: beTqiiV5zhI) wird von YouTube eingebettet.
Es gelten die Datenschutzerklaerungen von Google .

Mehrsprachigkeit / i18n #

Mehrsprachigkeit ist mit Hugo recht leicht moeglich . Eine Loesung fuer einzelne Artikel/Seiten etc. wird bei Fryboyter beschrieben . Noch bin ich unsicher, ob ich das ueberhaupt will, daher lasse ich das erstmal offen.
Zu ueberlegen waere dann noch, wie die URL aufgebaut sein soll, z.B. /en/artikel/ oder /artikel-en/ oder /artikel/en/ oder …

Structured Data / Schema.org #

Weil ich meine Rezepte bei Tandoor importieren wollte, ist mir aufgefallen, dass es dafuer eine Loesung namens structured data gibt, damit das auch einfach gelingt und schoen aussieht.

Also habe ich mal mit ein wenig Hilfe meine Rezepte umgebaut, sodass sie auch valides structured data ausspucken. Die Kategorien usw. habe ich mir ein bisschen bei der Rezepteingabe bei Chefkoch abgeguckt. Weitere Quellen:

Testen laesst sich das dann mit dem Test fuer Rich-Suchergebnisse .
Bei der weiteren Beschaeftigung damit, habe ich es mal zumindest noch fuer die Startseite eingebaut. Weitere Seiten werden vermutlich folgen.

Die Einbindung ist recht simpel und habe ich mir in diesem hervorragenden Repository etwas abgeguckt. Ich habe einfach in meiner layouts/baseof.html folgendes vor dem schliessenden hinzugefuegt:

  {{ partial "schema/schema.html" . }}
head>

Das Partial layouts/_partials/schema/schema.html sieht dann so aus:

 layouts/_partials/schema/schema.html

1
2
3
4
5
6
7
{{ if .IsHome -}}
  {{ partial "schema/schema_website.html" . }}
{{- else if .IsPage -}}
  {{ if eq .Section "recipes" }}
    {{ partial "schema/schema_recipe.html" . }}
  {{ end }}
{{ end }}

Das Schema fuer Website sieht dann z.B. so aus:

{{ $author :=  or (.Params.author) (.Site.Params.Author.name) }}
{{ $description := .Site.Params.description }}
 type="application/ld+json">
{
    "@context": "http://schema.org",
    "@type": "WebSite",
    "name": "{{ .Site.Title }}",
    "url": {{ .Site.BaseURL }},
    "description": "{{ $description }}",
    "thumbnailUrl": {{ .Site.Params.logo | absURL }},
    "license": "{{ .Site.Params.copyright }}"
}

Hier noch weitere Quellen:

Neue Domain #

Im Laufe der Jahre fielen mir immer wieder neue und vor allem kuerzere Domains ein. Schliesslich habe ich mich fuer uxg.ch entschieden.

Warum? Auf vielen Geraeten lautet mein Username seit langem benjo, was (abgeleitet von der Caesar-Verschluesselung ) ein ROT1 von admin ist. Da es keine TLD .jo gibt, habe ich einfach mal durchrotiert und bin bei “ROT20” haengen geblieben. Die Domain war verfuegbar, ist kurz (gerade im Vergleich zu yhaupenthal.org), unterstuetzt DNSSEC und war nicht mega teuer, also habe ich sie gekauft 🎉

Workflow #

So, und wie funktioniert nun das alles im Zusammenspiel?

GitHub Action #

Zuerst legen wir einen GitHub Workflow an (im Repo in .github/workflows/test_build_deploy.yml):

 .github/workflows/test_build_deploy.yml

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
---
name: test_build_deploy
on: push  # yamllint disable-line rule:truthy
jobs:
  linting:
    uses: tohn/gh-workflows/.github/workflows/linting.yml@main
    with:
      config-file: .markdownlintrc
  # codespell:
  #   name: Check for spelling errors
  #   runs-on: ubuntu-latest
  #   steps:
  #     - name: Checkout
  #       uses: actions/checkout@v6
  #     - name: Annotate locations with typos
  #       uses: codespell-project/codespell-problem-matcher@v1
  #     - name: Codespell
  #       uses: codespell-project/actions-codespell@v2
  # check-spelling:
  #   runs-on: ubuntu-latest
  #   steps:
  #     - name: Checkout
  #       uses: actions/checkout@v6
  #     - name: Check Spelling
  #       uses: rojopolis/spellcheck-github-actions@0.40.0
  #       with:
  #         config_path: .spellcheck.yml
  #         task_name: Markdown
  build_and_deploy:
    if: github.ref == 'refs/heads/main'
    needs: linting
    runs-on: ubuntu-latest
    steps:
      - name: Prepare
        uses: tohn/gh-workflows@main
      - name: Generate Statistics
        run: find ./content ./static -type f -printf '%f\n' | sed -r -n 's/.+(\..*)$/\1/p' | sort | uniq -c | sort -bn >./statistics.txt
      - name: Build uxg.ch
        env:
          HUGO_SECRET_YOUTUBE: ${{ secrets.HUGO_SECRET_YOUTUBE }}
          HUGO_SECRET_VIMEO: ${{ secrets.HUGO_SECRET_VIMEO }}
          HUGO_GITHUB_TOKEN: ${{ secrets.HUGO_GITHUB_TOKEN }}
        run: hugo --logLevel info --minify --panicOnWarning --printPathWarnings | tee ./build.txt && sed -i '/^INFO/d;1d' ./build.txt
      - name: Segment build of uxg.ch
        env:
          HUGO_SECRET_YOUTUBE: ${{ secrets.HUGO_SECRET_YOUTUBE }}
          HUGO_SECRET_VIMEO: ${{ secrets.HUGO_SECRET_VIMEO }}
          HUGO_GITHUB_TOKEN: ${{ secrets.HUGO_GITHUB_TOKEN }}
        run: hugo --logLevel info --minify --panicOnWarning --printPathWarnings --renderSegments statistics
      - name: Build uxgch42.onion
        if: startsWith(github.ref, 'refs/tags/')
        env:
          HUGO_SECRET_YOUTUBE: ${{ secrets.HUGO_SECRET_YOUTUBE }}
          HUGO_SECRET_VIMEO: ${{ secrets.HUGO_SECRET_VIMEO }}
          HUGO_GITHUB_TOKEN: ${{ secrets.HUGO_GITHUB_TOKEN }}
        run: hugo --logLevel info -e tor --minify --panicOnWarning --printPathWarnings
      - name: Run Pagefind on uxg.ch
        run: npx -y pagefind@latest --verbose
      - name: Run Pagefind on uxgch42.onion
        if: startsWith(github.ref, 'refs/tags/')
        run: npx -y pagefind@latest --site tor --verbose
      - name: Deploy uxg.ch
        uses: tohn/rsync-deploy@v1
        env:
          DEPLOY_KEY: ${{ secrets.SERVER_SSH_KEY }}
          ARGS: "-at --quiet --delete --delete-delay --delay-updates --exclude=_"
          SERVER_PORT: ${{ secrets.SERVER_PORT }}
          FOLDER: "public/"
          SERVER_IP: ${{ secrets.SERVER_IP }}
          USERNAME: ${{ secrets.SERVER_USERNAME }}
          SERVER_DESTINATION: ${{ secrets.SERVER_DESTINATION }}
      - name: Deploy uxgch42.onion
        if: startsWith(github.ref, 'refs/tags/')
        uses: tohn/rsync-deploy@v1
        env:
          DEPLOY_KEY: ${{ secrets.SERVER_SSH_KEY_TOR }}
          ARGS: "-at --quiet --delete --delete-delay --delay-updates --exclude=_"
          SERVER_PORT: ${{ secrets.SERVER_PORT }}
          FOLDER: "tor/"
          SERVER_IP: ${{ secrets.SERVER_IP }}
          USERNAME: ${{ secrets.SERVER_USERNAME }}
          SERVER_DESTINATION: ${{ secrets.SERVER_DESTINATION_TOR }}
  ping:
    # https://safjan.com/quick-ways-to-disable-github-actions-workflows-without-deletion/
    if: github.ref == 'refs/heads/main'
    # if: false
    needs: build_and_deploy
    runs-on: ubuntu-latest
    steps:
      - name: curl
        uses: wei/curl@v1
        with:
          args: ${{ secrets.UBERBLOGR }}
  build_and_deploy_stg:
    if: github.ref == 'refs/heads/stg'
    needs: linting
    runs-on: ubuntu-latest
    steps:
      - name: Prepare
        uses: tohn/gh-workflows@main
      - name: Generate Statistics
        run: find ./content ./static -type f -printf '%f\n' | sed -r -n 's/.+(\..*)$/\1/p' | sort | uniq -c | sort -bn >./statistics.txt
      - name: Build stage
        env:
          HUGO_SECRET_YOUTUBE: ${{ secrets.HUGO_SECRET_YOUTUBE }}
          HUGO_SECRET_VIMEO: ${{ secrets.HUGO_SECRET_VIMEO }}
          HUGO_GITHUB_TOKEN: ${{ secrets.HUGO_GITHUB_TOKEN }}
        run: hugo --logLevel info -e stg --minify --panicOnWarning --printPathWarnings | tee ./build.txt && sed -i '/^INFO/d;1d' ./build.txt
      - name: Segment build of stage
        env:
          HUGO_SECRET_YOUTUBE: ${{ secrets.HUGO_SECRET_YOUTUBE }}
          HUGO_SECRET_VIMEO: ${{ secrets.HUGO_SECRET_VIMEO }}
          HUGO_GITHUB_TOKEN: ${{ secrets.HUGO_GITHUB_TOKEN }}
        run: hugo --logLevel info -e stg --minify --panicOnWarning --printPathWarnings --renderSegments statistics
      - name: Run Pagefind
        run: npx -y pagefind@latest --verbose
      - name: Deploy
        uses: tohn/rsync-deploy@v1
        env:
          DEPLOY_KEY: ${{ secrets.SERVER_SSH_KEY_STG }}
          ARGS: "-at --quiet --delete --delete-delay --delay-updates --exclude=_"
          SERVER_PORT: ${{ secrets.SERVER_PORT }}
          FOLDER: "public/"
          SERVER_IP: ${{ secrets.SERVER_IP }}
          USERNAME: ${{ secrets.SERVER_USERNAME }}
          SERVER_DESTINATION: ${{ secrets.SERVER_DESTINATION_STG }}

Im Repo selbst muessen wir nun noch Secrets anlegen , die die entsprechenden Daten enthalten. Also z.B. das Secret fuer die YouTube API oder die SSH-Daten zum Server.

Update (2024-03-30): Bei jedem Push werden erst 2 Testjobs ausgefuehrt (die ich jetzt via reusable Workflow einbinde) und bei erfolgreichem Status der 3. Job (der ein bisschen von Composite Actions Gebrauch macht):

  1. markdownlint

    Fortlaufend den Code mit Lintern etc. zu ueberpruefen sollte eigtl. Standard sein. Ich nutze in neovim dazu ALE , was Support fuer Markdown via markdownlint mitbringt.

    Meine .markdownlintrc sieht so aus:

    {
      "comment": "https://github.com/DavidAnson/markdownlint / https://github.com/noqqe/noqqe.de/blob/master/.markdownlintrc",
    
      "MD013": false,
      "MD026": true,
      "MD033": true
    }
    
  2. shellcheck

    Auch meine Shellscripts will ich fortlaufend ueberpruefen und nutze hierzu wieder ALE mit dem Tool shellcheck .

  3. Build & Deploy

    Waren beide vorherigen Jobs erfolgreich, folgt der dritte. Dieser checkt erstmal das Repo aus (mit History fuer Git Info Variablen ), installiert dann node.js , Hugo und die npm-Pakete , die in der packages.json angegeben sind. Schliesslich wird unsere Website mit Minifizierung von Hugo gebaut und in einem weiteren Schritt auf unseren Webserver hochgeladen. Auf entsprechende Vorkehrungen auf dem Server gehe ich jetzt nicht ein, aber ein Artikel dazu (siehe Hosting) ist in Arbeit. Ebenso das Ersetzen von npm durch Hugo Modules .

  4. Profit 🎉

GitLab CI #

Als Alternative hier die Vorgehensweise mit der GitLab CI (trotz Repo bei GitHub ). Wie auch bei GitHub muessen wir unsere Secrets im Repo hinterlegen .

Ansonsten funktioniert die ganze Magie ueber die .gitlab-ci.yml, was im Prinzip die gleichen Steps durchlaeuft wie die Action bei GitHub (nur mit evtl. alten Paketen, weil ich die Docker Images schon laenger nicht mehr aktualisiert habe):

 .gitlab-ci.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
---
stages:
  - test
  - build
  - deploy

test:markdownlint:
  image: benjo2342/markdownlint-cli
  stage: test
  script:
    - "find . -name '*.md' | grep -v '^\\.\\/themes' | xargs markdownlint"
  variables:
    GIT_LFS_SKIP_SMUDGE: "1"

test:shellcheck:
  image: koalaman/shellcheck-alpine
  stage: test
  script:
    - "find . -name '*.sh' -print0 | xargs -0 shellcheck"
  variables:
    GIT_LFS_SKIP_SMUDGE: "1"

build:hugo:
  image: benjo2342/hugo
  stage: build
  script:
    - "apk --no-cache add npm~=12.16"
    - "npm i"
    - "hugo --minify"
  variables:
    GIT_SUBMODULE_STRATEGY: normal
  artifacts:
    paths:
      - public
  only:
    - master

deploy:live:
  variables:
    GIT_STRATEGY: none
  image: benjo2342/deploy
  stage: deploy
  script:
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - >/dev/null
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
    - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" >~/.ssh/config
    - rsync -at --quiet --delete --delete-delay --delay-updates --exclude=_ -e "ssh -p $SSH_LIVE_PORT" public/ "${SSH_LIVE_USER_HOST_LOCATION}"
  only:
    - main
  dependencies:
    - build:hugo

Lokal #

Dank Archetypes und Frontmatter legen wir anschliessend eine Datei namens archetypes/block.md an:

 archetypes/block.md

1
2
3
4
5
6
7
8
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
tags:
- default
description: Summary
---

Es reicht dann folgender Befehl, um eine Datei anzulegen:

$ hugo new block/beispiel.md
/path/to/website/content/block/beispiel.md created

Diese Datei koennen wir nun oeffnen und beliebig aendern. Die Datei wird anschliessend zum Git Index hinzugefuegt (git add content/block/beispiel.md), commited (git commit -va) und die Aenderungen gepusht (git push origin main).

Der Push loest dann auf GitHub die oben beschriebene Action aus und wir sollten nach kurzer Zeit eine neue Version auf der Website sehen 🎉

Die ganzen technischen Dinge im Hintergrund (also Server an sich, ssh, nginx usw.) versuche ich noch in einem anderen Beitrag zu beschreiben.

Und … das wars! Ueber 3 Jahre Arbeit, um meine Website auf einen neuen technischen Stand zu heben 🙈 😅 🎉

Influenzen / Nuetzliches #


  1. Laeuft 🎉 ↩︎

  2. Beispiel
    Yeah 🎉
     ↩︎

Danke, dass du das hier via RSS liest! :)

Fr, 18. Februar 2022, Ralf Hersel

Ubuntu Touch OTA-22 ist heute erschienen und führt Unterstützung von FM-Radio ein, um echten analogen Radioempfang auf unterstützten Geräten zu ermöglichen. Im Moment ist nur die Daemon-Implementierung in der OTA-22-Version enthalten, während eine FM-Radio-Anwendung in den nächsten Wochen im App Store verfügbar sein sollte.


Ubuntu Touch OTA-22 bringt auch gute Nachrichten für Besitzer anderer Geräte dank der Implementierung von WebGL-Unterstützung, die ein schnelleres 3D-Rendering ermöglicht. Darüber hinaus folgen die QQC2-Apps nun dem Systemthema und die Kameraunterstützung wurde in den Morph-Webbrowser integriert, sodass die Nutzer nun endlich Videoanrufe tätigen können.

"Dies ist wahrscheinlich die wichtigste Funktion dieses OTAs", so UBports. "Viele Leute haben uns gebeten, Videoanrufe als Option anzubieten. Jetzt läuft noch immer im Browser, aber wir denken, dass es bereits eine grosse Erleichterung sein kann. Und es ist der Türöffner für Videogespräche in Apps."


Es gibt auch gute Nachrichten für Volla Phone X-Besitzer im OTA-22-Update, das Unterstützung für den Fingerabdruckleser hinzufügt und verschiedene Probleme behebt, die Nutzer mit der vorherigen Version hatten. Dies alles ist möglich dank des Wechsels zu einer neueren Basis für das System-Image, Halium 10.

Darüber hinaus können OnePlus 5 und OnePlus 5T-Besitzer jetzt einen vollständigen Ubuntu Touch-Port geniessen, der sofort funktioniert, und Google Pixel 3a und Google Pixel 3a XL-Besitzer erhalten mit diesem Update eine bessere Klangqualität und Lautstärkeregelung sowie einen sogenannten "Booster-Modus" aktiviert, der die Anzahl der CPUs begrenzt und andere Batteriesparoptionen konfiguriert, wenn der Bildschirm ausgeschaltet ist.

Zu guter Letzt fügt das OTA-22-Update dem Begrüssungsbildschirm (Sperrbildschirm) Unterstützung für die Rotation sowie ein neues Layout für die Notrufleiste am unteren Rand hinzu, und die Dialer-App erhielt eine automatische Vervollständigung des Wähltastenfelds, damit man schneller auf Kontakte zugreifen kann.

Ausserdem wurden einige Bugs beseitigt, um das Beschneiden von MMS-Bildern in der Messaging-App zu verhindern, den Shader-Cache für QML zu verbessern, den Fingerabdruckleser auf OnePlus 5T-Geräten funktionsfähig zu machen und zu ermöglichen, dass sie in den Ruhezustand zurückkehren, wenn eine Push-Benachrichtigung eintrifft, sowie die Kamerarechte auf einigen Geräten durchzusetzen.

Das Ubuntu Touch OTA-22-Update wird jetzt auf alle unterstützten Geräte ausgerollt.

Quelle: https://ubports.com/de/blog/ubports-blogs-nachrichten-1/post/ubuntu-touch-ota-22-release-3835

Fr, 18. Februar 2022, Lioh Möller

Es gibt eine Vielzahl von grafische Markdown Editor für Linux, mit unterschiedlichen Schwerpunkten. Zettlr vereint die Fähigkeiten eines mächtigen Bearbeitungsprogramms mit einer Verschlagwortungsfunktion und vielen weiteren Extras. Insbesondere bei komplexen Texten, helfen diese dabei, die Übersicht zu behalten.

Tags lassen sich pro Dokument hinzufügen. Die zentrale Suche ermöglicht das schnelle Auffinden zuvor definierter Schlagwörter.

Eine weitere Möglichkeit stellt der sogenannte Zettelkasten dar. Mit dessen Hilfe lassen sich interne Dokumente verlinken, ähnlich wie bei einem Wiki. Bei einem Klick auf einen solchen Link mit gedrückter Ctrl-Taste wird das Zieldokument angezeigt und gleichzeitig in der Seitenleiste eine Volltextsuche nach dem Wort durchgeführt.

Zettlr bietet Exportmöglichkeiten in gängige Formate, welche mittels Pandoc/LaTeX realisiert wurden. Die Anwendung integriert eine vollwertige Rechtschreibkorrektur und bietet einen Dark-Mode, welcher sich bei Bedarf auch zeitgesteuert aktivieren lässt.

Der integrierte und optional nutzbare Pomodoro-Timer hilft dabei, die Bildschirmzeit zu reduzieren und Pausen einzuhalten.

Es stehen Pakete für Debian und Fedora, sowie AppImages zum Download zur Verfügung.

Download: https://www.zettlr.com/download
Git-Repository: https://github.com/Zettlr/Zettlr

17. Februar 2022

Mozilla hat mit Firefox 97.0.1 ein Update außer der Reihe für seinen Desktop-Browser veröffentlicht.

Download Mozilla Firefox 97.0.1

Mit dem Update auf Firefox 97.0.1 blockiert Mozilla die aktuelle Version einer von der Sicherheits-Software WebRoot SecureAnywhere injizierten DLL-Datei unter Windows, da diese zur völligen Unbenutzbarkeit von Firefox führen konnte. WebRoot wird das Problem in einem zukünfigten Update seiner Sicherheits-Software beheben.

Außerdem hat Mozilla einen Fehler behoben, der für manche Nutzer mit deaktivierter Sitzungswiederherstellung fälschlicherweise zu einem Bildschirm beim Firefox-Start führte, der aussagte, dass Firefox die Tabs nicht wiederherstellen konnte.

Behoben wurde auch ein Fehler, der auf der Video-Plattform Hulu dafür sorgte, dass der Bild-im-Bild-Modus nicht aktiviert werden konnte und das Video stattdessen pausierte. Ein anderes behobenes Problem betrifft die Video-Plattform TikTok, wo es sein konnte, dass Videos, welche von der Profilseite eines Nutzers geöffnet werden sollten, nicht öffneten.

Schließlich wurde noch eine Absturzursache behoben, welche ausschließlich Windows 7 betroffen hat, sowie eine weitere Absturzursache, welche ausschließlich Linux betroffen hat.

Der Beitrag Mozilla veröffentlicht Firefox 97.0.1 erschien zuerst auf soeren-hentzschel.at.

16. Februar 2022

Zu einem Smart Home gehört es fast selbstverständlich, dass der Verbrauch von Energie aufgezeichnet wird. Der Stromzähler ist eine der wesentlichen Energiezählern im Haushalt, auch der Gaszähler ist sehr wichtig. Vor allem in Haushalten, in denen der primäre Energieträger Erdgas ist, ist der Gasverbrauch essenziell und wird im Smart Home auch optimiert. Das ist, zumindest für meine Begriffe, eine der wesentlichen Aufgaben des Smart Homes.

Das Schöne an den Gaszähler ist es, dass sie sich in der Regel sehr einfach auslesen lassen. Es ist keine komplizierten und teuren Geräte dafür notwendig. Bei den meisten Gaszählern reicht nämlich ein einfacher Reed-Kontakt aus, die es für einige Cent bei Ebay oder anderen Shops zu kaufen gibt. Als Intelligenz wird der sehr vielseitige ESP8266 verwendet. Dieser ist sehr energiesparend, hat integriertes WLAN, lässt sich verhältnismäßig einfach einrichten und kostet ebenfalls nur sehr wenig Geld.

Wenn der Gaszähler eine Aufschrift wie „1 im = 0,01m³“ trägt, lässt er sich mit wenig Aufwand digitalisieren. Ein Reed-Schalter könnte bereits genügen, um den Gaszähler ins Smart Home einzubinden.

Hardware: ESP8266 und Reed-Kontakt verlöten

Die Einkaufsliste für diesen Sensor:

  • ESP8266, beispielsweise den Wemos D1 Mini
  • Reed-Kontakt
  • optional: 5V Netzteil
  • optional: Schrumpfschlauchsortiment

Für einen geübten Maker ist diese Aufgabe im Handumdrehen erledigt. Für nicht geübte Maker ist es das perfekte Einsteigerprojekt. Es sind nur sehr wenige Lötstellen zu setzen und man kann kaum etwas falsch machen. Beim ESP8266, in meinem Fall ein Wemos D1 mini, müssen nur die beiden Pinleisten angelötet werden. Für Minimalisten würde sogar je ein Pin bei G (Ground) und D1 (GPIO5) reichen. An diese Pins gehören jeweils die Kabel, idealerweise Litzen 0,15mm², an deren Ende der Reed-Kontakt gehört. Bei diesem Schalter muss man keine Richtung beachten, man kann ich nicht falsch herum anlöten. Das fertige Produkt sieht dann so aus. Ich habe noch versucht, die Lötstellen mit Schrumpfschlauch zu verschönern. Das ist optional.

Update (02.05.2022)  Weiterhin habe ich eine optionale LED zur Visualisierung des Signals eingebracht. Diese blinkt immer dann, wenn der Reed-Kontakt schaltet. Das ist vor allem dann sehr nützlich, wenn man den Reed-Schalter am Gaszähler anbringt. Die LED ist eine Hilfe, sie ist aber nicht zwingend nötig. Ihren Vorwiderstand kann man über Online-Tools berechnen, bei mir waren es 220 Ohm.

Der Schaltplan mit optionaler LED
ESP8266 an der Pinleiste mit einem Reed-Schalter verlötet. Der Reed-Schalter geht auf Pin G und Pin D1

ESPHome auf ESP8266 installieren unter Windows 10

Unter Windows 10 lässt sich über den „Microsoft Store“ Ubuntu installieren. Das ist der kleine Umweg, den ich häufig gehe, um ein fast vollständiges Linux unter Windows 10 zum laufen zu bringen. Dass es leider nicht vollständig ist, sieht man den nun folgenden, etwas umständlichen Herangehensweise, wie man ESPHome auf dem ESP8266 installiert.

Wer ein vollständiges hass.io bzw. Home Assistant hat, kann das übrigens über den Addon-Store mit dem Add-on „ESPHome“ deutlich beschleunigen. Da ich aber, wie bereits beschrieben, Home Assistant als Container laufen habe, ist für mich der Umweg notwendig.

Man startet Ubuntu unter Windows und gelangt in das Terminal. Dort installiert man sich (falls noch nicht geschehen) Python 3 und das nötige Paket „esphome“ aus dem Python-Repsitory. Anschließend prüft man, ob die Installation geklappt hat, indem man sich die Versionsnummer ausgeben lässt.

$ sudo apt-get install python3 python3-pip
$ pip install --user esphome
$ esphome version 

Der Einfachheit halber empfehle ich, den Wizard von ESPHome zu verwenden. Er wird benutzt, um die *.yaml zu erstellen. Genau wie bei Home Assistant ist sie dafür da, den Controller zu konfigurieren. Der Wizard zeigt einem glücklicherweise gleich alle möglichen Alternativen auf, die man eingeben kann. Beantwortet also wahrheitsgemäß die 4 Fragen den Wizards und wir erhalten eine Konfigurationsdatei mit dem angegebenen Namen. Ich habe hier willkürlich gaszaehler.yaml gewählt.

$ esphome wizard gaszaehler.yaml
$ nano gaszaehler.yaml

Die nun folgende Datei sieht dann beispielsweise so aus:

esphome:
  name: gaszaehler

esp8266:
  board: d1_mini

# Enable logging
logger:

# Enable Home Assistant API
api:
  password: "1234"

ota:
  password: "1234"

wifi:
  ssid: "hier die Wifi-SSID eintragen"
  password: "hier das Wifi Passwort eintragen"
  manual_ip:
    static_ip: "auf Wunsch"
    gateway: "IP-Adresse des Gateways"
    subnet: "Subnet Maske"

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Gaszaehler Fallback Hotspot"
    password: "hier steht automatisch ein Passwort"

captive_portal:

Das Fallback-Wifi wird gebraucht, falls der Sensor das eigentliche WLAN nicht erreichen kann. Dann baut der ESP8266 eigenständig ein WLAN auf, über das er sich konfigurieren lässt.

Update (02.05.2022)  Vor diesem Update habe ich den Pulse_counter von ESPHome verwendet. Dieser liefert leider keine zuverlässigen Werte. Gleiches gilt für den pulse_meter, der eigentlich besser sein sollte. Stattdessen bin ich nach einiger Tüftelei wieder bei dem binary_sensor herausgekommen, der wunderbar funktioniert. Zusätzlich ist eine LED auf GPIO0 angebracht, die schaltet, sobald der Reed-Schalter zieht. Damit könnt ihr live am Gerät sehen, ob ein Puls anliegt. Sie ist nur eine Hilfe und nicht zwingend erforderlich.

globals:
  - id: total_pulses
    type: int
    restore_value: false
    initial_value: '0'  # hier kann der Gaszählerstand initialisiert werden

binary_sensor:
  - platform: gpio
    id: internal_pulse_counter
    pin:
      number: GPIO5
      mode: INPUT_PULLUP
    name: "Live-Impuls"
    filters:
      - delayed_on: 10ms
    on_press:
      then:
        - lambda: id(total_pulses) += 1;
        - output.turn_off: led  # optional: für eine LED, die den Gaszählerpuls visualisiert
    on_release:
      then:
        - output.turn_on: led  # optional: für eine LED, die den Gaszählerpuls visualisiert

sensor:
  - platform: template
    name: "Gasverbrauch"
    device_class: gas
    unit_of_measurement: "m³"
    state_class: "total_increasing"
    icon: "mdi:fire"
    accuracy_decimals: 2
    lambda: |-
      return id(total_pulses) * 0.01;

# Optional: Diese LED soll blinken, sobald ein Signal vom Gaszähler erkannt wird
output:
  - platform: gpio
    pin: GPIO0
    id: 'led'

Fertig mit der Konfiguration. Wir speichern mit Strg + O und schließen Nano mit Strg + X

Mit dem folgenden Befehl wird der Code für den ESP8266 kompiliert. Ich habe es leider nicht geschafft, ihn direkt per USB-Kabel auf meinen Controller zu bekommen. Daher bin ich einen Umweg gegangen.

$ esphome run gaszaehler.yaml

Das endet mit einer Fehlermeldung (connection failed bad indicator errno=11), dass der Code nicht auf den Controller gebracht werden konnte. Stattdessen kopieren wir den Code auf das Laufwerk C: unter Windows und arbeiten von dort aus weiter (sorry Leute!!).

$ cp ~/esphome/.esphome/build/gaszaehler/.pioenvs/gaszaehler/firmware.bin /mnt/c/Users/

Mittels ESPHome-Flasher, den es auch für Windows gibt und der nicht installiert werden muss (!!) geht es weiter. Die eben kopierte Datei auswählen, den ESP8266 mit USB am PC anstöpseln und den entsprechenden COM-Port auswählen (bei mir wurde nur einer angezeigt). Bestätigen, und warten bis es fertig ist.

ESPHome in Home Assistant installieren

Mit der Entität „ESPHome“ kann der Sensor in den Home Assistant eingebunden werden. Das geht sehr fix, es muss nur die IP-Adresse und das festgelegte Passwort eingegeben werden. Damit er auch korrekt als Energiequelle erkannt wird, muss man noch folgende Zeilen in die configuration.yaml anfügen, besser noch, in die sensor.yaml

Update (15.04.2022): Manchmal fällt der ESP kurzzeitig aus, was den Gasverbrauch kurzfristig auf 0 m³ setzt. Sobald er wieder da ist, gibt es einen unlogischen Peak in der Statistik. Dieser wird über die kleine IF-Schleife herausgefiltert.

- platform: template
  sensors:
# Gaszähler, kommend von ESPHome, aufbereiten für Energy
    gasincubicmeter:
      value_template:  >
          {% if states('sensor.gasverbrauch') | float == 0 %}
           {{ states('sensor.gasincubicmeter') }}
          {% else %}
           {{ states('sensor.gasverbrauch') | float }}
          {% endif %}
      unit_of_measurement: m³
      device_class: gas
      attribute_templates:
        state_class: total_increasing

Nach einem Neustart des Servers klickt man im Home Assistant auf „Einstellungen“, „Energie“ und klickt auf den Gaszähler. Dort taucht nun die neue Entität gasincubicmeter auf und kann ausgewählt werden.

In Home Assistant kann über Einstellungen → Energie eine neue Gasquelle hinzugefügt werden
Der Gasverbrauch wird auf dem Energie-Dashboard von Home Assistant angezeigt

Reed-Schalter am Gaszähler positionieren

Wer Zugriff auf einen 3D-Drucker hat, sollte sich bei Thingiverse mal umsehen, ob dort ein Halter für seinen Gaszähler vorhanden ist. Die Chance dort, oder woanders, einen zu finden, halte ich für sehr hoch. Andernfalls kann man mit etwas Geschick und gutem Klebeband den Schalter direkt am Gaszähler montieren. Er muss in der vorgesehenen Kerbe möglichst genau unter der letzten Ziffer positioniert werden. Wer darauf achtet, wird bemerken, dass die letzte drehende Ziffer einen kleinen Magneten hat. Genau darunter muss der Reed-Schalter geklebt werden.

The post Home Assistant: Gaszähler mit ESPHome auslesen, flashen unter WSL first appeared on bejonet - Linux | Smart Home | Technik.

Mi, 16. Februar 2022, Lioh Möller

Kinoite gehören zur Mineralklasse der 'Silikate und Germanate'. Der bläulich-silbern schimmernde Glanz hat wohl die Entwickler des Fedora-Abkömmlings zur Namensgebung inspiriert.

Ähnlich wie Fedora Silverblue gehört Kinoite zu den sogenannten immutable Systemen, das heisst das Basissystem ist grundsätzlich unveränderlich. Die Distributionen unterscheiden sich vorwiegend in der Wahl der Standard-Desktopumgebung. Im Gegensatz zu Silverblue, kommt bei Kinoite KDE Plasma zum Einsatz. Grundlegende Mechanismen wie rpm-ostree sind auch dort gültig.

Die Installation erfolgt mit dem von Fedora bekannten Anaconda Installationsprogramms, wobei die Einrichtung der Benutzerkonten von Silverblue abweicht. Der Root-Account ist standardmässig deaktiviert, der anzulegende Benutzer erhält allerdings erst nach Auswahl der entsprechenden Option, die Möglichkeit Administratoren-Rechte zu erlangen. Wurde dies nicht aktiv konfiguriert, lässt sich die Installation nicht starten.

Wurde diese Hürde erst einmal genommen, präsentiert sich Kinoite mit einem leicht angepassten KDE Plasma Desktop. Damit einher geht die grafische Softwareverwaltung mit dem Programm Discover. Wie auch bei Silverblue ist das Flathub Repository standardmässig nicht aktiviert. Die Möglichkeit 3rd Party Repositories über einen Assistenten zu integrieren, wie es bei Silverblue der Fall ist, gibt es nicht. Sobald Flathub anhand der offiziellen Anleitung des Projektes eingebunden wurde, lassen sich Applikationen aus dem Repository über Discover installieren und das System so erweitern.

Damit stellt Kinoite eine verlässliche Alternative für Nutzer des KDE Plasma Desktops dar und hebt sich aufgrund der neuen Systemarchitektur von klassischen Linux-Distributionen ab.

Quelle: https://kinoite.fedoraproject.org/

Mi, 16. Februar 2022, Lioh Möller

Fedora Silverblue und Kinoite sind primär auf die Nutzung von Flatpaks ausgelegt. Dennoch kann es in einigen Fällen notwendig sein, RPM Pakete zu installieren. Möglich macht dies rpm-ostree, welches die benötigten Anwendungen als Overlay einbinden kann.


Bisher bietet rpm-ostree keine integrierte Suche, wie beispielsweise dnf. Sollte der Paketname unbekannt sein, kann dnf in einer toolbox zur Hilfe genommen werden.

Falls bisher noch keine toolbox erzeugt wurde, lässt sich dies mit dem folgenden Befehl nachholen:

toolbox create -y

Die dnf Suche lässt sich daraufhin innerhalb der toolbox wie folgt ausführen:

toolbox run dnf search nano

Nachdem der passende Paketname ermittelt wurde, erfolgt die eigentliche Installation mit rpm-ostree:

rpm-ostree install nano

Die Änderungen werden nach einem Neustart des Systems appliziert.

Alternativ lässt sich die experimentelle Funktion apply-live nutzen:

sudo rpm-ostree ex apply-live

15. Februar 2022

Di, 15. Februar 2022, Ralf Hersel

In den letzten Wochen haben wir einige Beiträge zum Thema 'Linux für Einsteiger' geschrieben und in diesem Artikel ein Fazit gezogen. Es wurden 9 verschiedene Distros mit unterschiedlichen Konzepten und Desktop-Umgebungen betrachtet. Bei der Vorauswahl haben wir nur solche Systeme in Erwägung gezogen, die wir für Einsteiger:innen geeignet hielten. Deshalb gab es keine Gewinner und Verlierer, sondern eine Palette an Distributionen, die wir empfohlen haben.

Nach einigem Nachdenken, bin ich zum Schluss gekommen, dass die Desktop-Umgebung für Anfänger:innen wichtiger ist, als die darunter liegende Distribution. Dem mag man widersprechen, weil es nicht lange dauert, bis die Anwender die Unterschiede entdecken. Dennoch, der erste Eindruck zählt, und dieser wird von der Oberfläche und nicht vom Unterbau geprägt.

'Seeing is believing', sagt der Volksmund. Man muss etwas sehen, um es zu glauben; sichtbare Fakten lassen sich nicht leugnen. Die Einsteigerin sieht den Installationsprozess, die Einführung und den Desktop. Bei der aktuellen Aufmerksamkeitsspanne, die bei wenigen Minuten liegt, müssen diese Erfahrungen ausreichen, um einen Einsteiger von der gewählten Distro/Desktop-Kombination zu überzeugen.

'The Linux Experiment' zu diesem Thema

Der grosse Vorteil von GNU/Linux-Distributionen und den Desktop-Umgebungen, ist deren Vielfalt. Das muss man nicht als Verwirrung, sondern als freie Auswahl ansehen. Auch beim Wocheneinkauf schätzt man die Wahl zwischen den Barbecue-Sossen, Joghurts und Pasta-Sorten.

Daher empfehle ich in diesem Nachtrag, sich an den grossen und etablierten Desktop-Umgebungen zu orientieren. Diese sind:

  • KDE-Plasma : Windows-ähnlich mit vielen Einstellungsmöglichkeiten
  • GNOME-Shell : Modern und auf das Wesentliche reduziert
  • Xfce : Solide, klassisch zu bedienen und für ältere Computer geeignet

Selbstverständlich gibt es weitere Desktops, die für spezielle Anwendungsfälle besser geeignet sind. Das geht jedoch über das hinaus, was eine Einsteigerin interessieren sollte. Denn für Euch, die in die GNU/Linux-Welt einsteigen möchtet, reicht die Auswahl zwischen diesen drei Desktops vollkommen aus. Alles andere kommt später.

Mein Tipp: schau Dir die drei Desktops an, und wähle das Erscheinungsbild, welches Dir am besten gefällt. Alle relevanten Distributionen bieten diese drei Desktop-Umgebungen an. Wenn es später um die beste Distro für Deine Zwecke geht, wendest Du Dich an die Community und lässt Dich beraten.

14. Februar 2022

Unter macOS hatte ich mit LibreOffice seit einiger Zeit das Problem, dass es nur weiße Seiten ausdruckte. Auch die entsprechende Druckvorschau unter macOS war leer. Das Erzeugen von PDFs und der anschließende Druck hingegen funktionierten ohne Probleme. Die Druckvorschau zeigt nur leere Seiten Verursacht wird der Fehler wohl durch die Bibliothek Skia, bei welcher es sich um eine freie 2D…

Quelle

Dies ist der Beginn meines zweiten Container-Projekts. Nach Kanboard im Container möchte ich diesmal eine Nextcloud-Instanz als Container, zusammen mit einem Datenbank-Container, in einem Podman-Pod betreiben.

Da ein einzelner Artikel vermutlich zu lang wird, teile ich das Projekt in mehrere Artikel auf. Wie viele es genau werden, kann ich jetzt noch nicht sagen. Am Ende der Reihe werde ich hier eine Übersicht einführen und die einzelnen Teilen entsprechend miteinander verbinden.

In diesem ersten Teil geht es um meine Motivation, das eigentliche Ziel und den groben Plan.

Was Leser dieser Reihe erwartet

Ihr könnt mich durch diese Reihe begleiten und euch von meinen Erlebnissen und Erkenntnissen unterhalten lassen. Dabei dürft ihr nicht annehmen, dass es sich bei dem von mir beschriebenen Vorgehen um eine gute Praxis handelt. Hier gilt eher: Der Weg ist das Ziel.

Ihr seid herzlich eingeladen, die Artikel zu kommentieren und über das Vorgehen und Alternativen dazu zu diskutieren. Gern in der Kommentarsektion unter den jeweiligen Beiträgen oder als Artikel in euren eigenen Blogs.

Ich plane die Artikel im Wochenrhythmus, wenigstens monatlich, zu veröffentlichen. Bitte verzeiht, wenn es etwas unregelmäßig wird. Dies ist ein Hobby, dem nur begrenzt Zeit zur Verfügung steht.

Motivation

Bei Linux-Containern handelt es sich um eine Technologie, die gekommen ist, um zu bleiben. Sie hat bereits in vielen Branchen Fuß gefasst und immer mehr Projekte bieten ihre Anwendungen zusätzlich oder ausschließlich in Form von Containern an.

Als Sysadmin mittleren Alters werden mich Linux-Container sicher noch viele Jahre begleiten. Um praktische Erfahrungen mit dem Betrieb zu sammeln, möchte ich einige private Projekte in Containern betreiben.

Beruflich arbeite ich überwiegend mit RHEL. Red Hat engagiert sich stark in den Projekten Ansible und Podman, welche ich auch unter anderen Distributionen, wie z.B. Debian, einsetze. Ich möchte das Projekt als Chance nutzen, mein Wissen auch in diesen Werkzeugen zu festigen und auszubauen.

Ich spiele schon seit einiger Zeit mit dem Gedanken, wieder eine eigene Nextcloud-Instanz zu betreiben. Da auf dem zur Verfügung stehenden Server bereits eine Nextcloud-Instanz läuft und ich meine Anwendung von der bestehenden Instanz getrennt und möglichst losgelöst vom Betriebssystem betreiben möchte, habe ich mich entschieden, Nextcloud im Container zu betreiben.

Ziele

Ziel dieses Projekts sind das Deployment und der Betrieb einer Nextcloud-Instanz als Podman-Pod. Im Einzelnen sollen folgende Ziele erreicht werden:

  1. Entwicklung eines wiederverwendbaren Verfahrens zum Deployment einer Nextcloud im Container
  2. Persistente Speicherung von Konfigurations- und inhaltlichen Daten im Dateisystem des Hosts
  3. Konfiguration eines Reverse-Proxies (NGINX) für den Zugriff auf die Nextcloud-Instanz
  4. Konfiguration von Backup und Restore für Konfiguration und Inhalte der Nextcloud-Instanz
  5. Konfiguration und Test automatischer durch Ansible gesteuerter Updates

Umgebung

Für die Umsetzung des Projekts steht mir ein Virtual Private Server (VPS) mit genügend Ressourcen zur Verfügung. Dieser wird in einem Rechenzentrum in Deutschland betrieben. Auf diesem sind Debian Bullseye, NGINX, ein OpenSSH-Server, Podman 3.0.1 (rootless) und Python 3.9.2 installiert. Damit erfüllt dieses System die Voraussetzungen, um mit Ansible konfiguriert zu werden und Container ausführen zu können.

Ansible selbst läuft in meiner privaten Arbeitsumgebung auf meinem Debian-PC und einem Fedora-35-Notebook.

Methodik und verwendete Werkzeuge

Zu Beginn habe ich mich etwas in der Nextcloud-Dokumentation und den verfügbaren Nextcloud-Images belesen. Besagte Dokumentation sowie die der verwendeten Werkzeuge sind im folgenden Abschnitt verlinkt.

Um die oben formulierten Ziele zu erreichen, werde ich in einem Python Virtual Environment eine Ansible-Version installieren, mit der ich die Collection containers.podman nutzen kann. Hiermit werde ich eine Ansible-Rolle entwickeln, die ich wiederverwenden kann, um Nextcloud-Instanzen in einer rootless-Podman-Umgebung zu deployen. Die Ansible-Rolle wird anschließend auf meinem GitHub-Account veröffentlicht.

Die Konfiguration von NGINX und acme.sh für die TLS-Zertifikate erfolgt manuell.

Quellen und weiterführende Links

In diesem Abschnitt liste ich Links zu Artikeln und Dokumentationen auf, welche ich im Vorfeld gelesen habe und deren Kenntnis ich für die Umsetzung als nützlich erachte. Zur besseren Übersicht gliedere ich diese in die Unterabschnitte Hintergrundwissen, Dokumentation und Eigene Artikel.

Die weiteren Artikel dieser Reihe

Hintergrundwissen

Dokumentation

Eigene Artikel

13. Februar 2022

Der Sinn eines verifizierten Systemstart (engl. „verified boot“) ist sicherzustellen, dass beim Start Komponenten aus vertrauenswürdigen Quellen geladen werden und es Angreifern nicht gelingt, manipulativ in den Prozess einzugreifen. Grundsätzlich ist vieles davon Linux nicht fremd, aber wird zum Teil aktiv unterlaufen.

Einordnung und Kontext

Die Themen verifizierte Systemstart, Secure Boot und TPM beschäftigen mich momentan. Befasst man sich mit vollständig verschlüsselten Systemen, der zusätzlichen Bindung an TPM bzw. Secure Boot, zusätzlich gegeneinander durch Verschlüsselung abgeschirmten Home-Verzeichnissen und weiterführenden Fragen, wie der Sicherheit biometrischer Verfahren und welche zusätzlichen Schutzmaßnahmen durch zusätzliche Hardware-Tokes wie FIDO2 (YubiKey) möglich sind, dann ist der verifizierte Systemstart letztlich ein logisches weiteres Puzzlestück.

Natürlich handelt es sich dabei um „advanced“ Sachen. Wer sein System nicht verschlüsselt, andauernd irgendwelche Programme von Dritten herrunter lädt und sich Kernel aus irgendwelchen PPAs installiert, der hat dadurch überhaupt keinen Sicherheitsgewinn. Ich respektiere es auch, wenn man solche „Trusted Computing“-Konzepte ablehnt oder ihnen zumindest misstraut. Ich ermuntere dennoch dazu, hier nicht einfach irgendwas nachzuplappern, weil (insbesondere in der deutschen Community?) in dem Bereich viele urbane Legenden kursieren. Neben abstrakten Informationsquellen ist meine konkrete Arbeitsgrundlage für viele Experimente das Arch Linux-Wiki und das ist nun wirklich kein Vertreter von „Corporate Linux“.

Der Blick über den Tellerrand

Der verifizierte Systemstart ist dabei keinesfalls irgendwie exotisch. Systeme wie iOS oder Android haben das ebenso implementiert, wie Windows 11 oder macOS. Das Prinzip ist einfach: Es wird eine vollständige Vertrauenskette aufgebaut. Angefangen von einer vertrauenswürdigen (Hardware-)Wurzel, bei der jeweils eine geladene Stufe die nächste authentifiziert. Also von der Hardware, über den Bootloader zu den entsprechenden Systempartitionen bis hin zu den Benutzerdaten. Das entsprechende Verfahren kann z. B. hier für Android nachgelesen oder ist hier sehr anschaulich viel Windows 11 (vor allem Abbildung 1 für Leser ohne Lust auf langwierige technische Inhalte) dargestellt. Solche Verfahren können noch mit einem Rollback-Schutz und anderen Sachen kombiniert werden, aber das führt jetzt hier zu weit.

Was kann Linux theoretisch?

Folgende Boot-Abfolge kann Linux bzw professionelle Linux-Distributionen momentan theoretisch bieten:

  • UEFI startet shim (was letztlich nur eine Zertifikatesammlung mit Signatur von Microsoft ist). Das kann in das s. g. „TPM boot measure“ (keine Ahnung, wie man das gut übersetzt?) einbezogen werden.
  • Shim ruft den Bootloader (i. d. R. GRUB) auf, der mit einem Schlüssel des Distributors signiert ist. GRUB kann theoretisch in das „TPM boot measure“ einbezogen werden.
  • Alternativ ist theoretisch der direkte Start über ein Unified Kernel Image möglich.
  • Der Bootloader ruft den Kernel auf und übergibt initrd. Der Kernel ist durch den Distributor signiert, initrd ist nicht signiert oder validiert. Kernel und initrd können theoretisch in „TPM boot measure“ einbezogen werden.
  • Bei verschlüsselten Systemen fragt initrd nun nach dem Passwort für die verschlüsselte Root-Partition. Es erfolgt keine Verifikation, ob initrd manipuliert oder ähnliches wurde. Es erfolgt der Übergang ins Root-Dateisystem. Die per LUKS verschlüsselte root-Partition kann an den TPM Secure Boot Status gebunden werden.
  • Abschließend erfolgt eine Anmeldung des Benutzers in der Loginmaske der Desktopumgebung. Die Daten sind bereits vorher entschlüsselt, sofern der Administrator nicht eine individuelle Konfiguration mit systemd-homed, eCryptFS oder separaten LUKS-Partitionen pro Benutzer eingerichtet hat.

Was macht Linux faktisch?

  • Secure Boot wurde deaktiviert, weil man irgendwo gelesen hat, dass man das tun sollte.
  • TPM ist ebenfalls deaktiviert, auf der Modul-Blacklist oder wird einfach nur nicht beachtet. Selbst wenn TPM nicht deaktiviert ist, macht eine Einbeziehung in GRUB oder initrd momentan wegen des unzureichenden Implementierungsgrades keinen Sinn.
  • GRUB lädt ein nicht signiertes initrd und einen nicht signierten Kernel.
  • Bei verschlüsselten Systemen fragt initrd nun nach dem Passwort für die verschlüsselte Root-Partition. Es erfolgt keine Verifikation, ob initrd manipuliert oder ähnliches wurde. Es erfolgt der Übergang ins Root-Dateisystem.
  • Abschließend erfolgt eine Anmeldung des Benutzers in der Loginmaske der Desktopumgebung. Die Anmeldung ist Makulatur, da die Daten bereits vorher entschlüsselt wurden, daher erfolgt oft ein einfacher Autologin.

Ist das ein Problem?

Das hängt wie bereits oben geschildert, von der Frage ab, welches Sicherungsbedürfnis man hat. Nutzt man sowieso keine Verschlüsselung, braucht man weitere Schutzmaßnahmen eh nicht. Insbesondere auf Entwicklungssystemen ohne schutzwürdige Daten verzichtet man häufiger auf Verschlüsselung. Hier kann man dann also auch nach Belieben mit nicht signierten Kernel, selbst gebauten Modulen etc. arbeiten.

Nutzt man eine Verschlüsselung nur, um Gelegenheitsblicke von neugierigen Kollegen, Freunden oder Familienmitgliedern zu entgehen, dann benötigt man weiterführende Maßnahmen ebenso eher nicht. Es sei denn man hat eine Tochter, die gerade an einer Bewerbung für den Geheimdienst als Hackerin arbeitet.

Hat man aber ein Schutzbedürfnis für sich selbst erkannt und nutzt ein vollständig verschlüsseltes Betriebssystem, um seine Daten sicher vor den Blicken professioneller Angreifer zu schützen, dann hat der aktuelle Zustand erhebliche Schwachstellen und einige zumindest seltsame Aspekte:

  • Man startet ein verschlüsseltes System von einer i.d.R. unverschlüsselten boot-Partition, ohne zu prüfen, was eigentlich mit initrd und Kernel passiert ist und gibt dort das Passwort ein. Dieses Problem wird aktuell gelöst durch eine verschlüsselte Boot-Partition, was aber GRUB immer mehr aufbläht und noch unwartbarer macht als es eh schon ist.
  • Das System lässt sich einfach kopieren und danach mit Brute Force so lange „quälen“ bis man drin ist. Die Verschlüsselung mit LUKS kennt dagegen keine Schutzmaßnahmen und verhindert das auch nicht.
  • Man gibt ein Passwort beim Start ein, um das Betriebssystem zu entschlüsseln und entschlüsselt nebenbei die Benutzerdaten. Der eigentliche Benutzerlogin ist nur Makulatur und weil das den meisten Anwendern klar ist, überspringen sie oft auch den Benutzerlogin per Autologin.
  • Benutzerdaten sind bei Multiuser-Systemen nur mit den UNIX-Rechten gegeneinander abgeschirmt und je nach Distribution nicht mal das.

Schlussbemerkung

Das Thema ist perspektivisch kein „Entweder-Oder“, sondern ein „Und“. So wie in der Vergangenheit immer die Möglichkeit bestand, nicht verschlüsselte Systeme zu nutzen, obwohl die Distributionen die LUKS-Verschlüsselung zunehmend prominenter in den Installationsroutinen platzierten, so wird auch der verifizierte Startvorgang Einzug halten für jene, die es benötigen oder dies zumindest glauben bzw. wohl eher fürchten. Alle anderen können, aber müssen dies nicht nutzen. Unstrittig ist jedoch, dass man aktuell ein bisschen hinter den anderen Betriebssystemen her rennt und nicht mal die Möglichkeiten ausreizt, die man schon hat. Spannend dürfte sein, was Enterprise-Distributionen wie SUSE Linux oder Red Hat Enterprise Linux hier in den nächsten Jahren umsetzen. Bei beiden Firmen ist man an den Themen dran.

Das war (voraussichtlich) der letzte Artikel zu diesem Thema für die nächste Zeit. Alle, die von Secure Boot, TPM, GRUB, verified Boot & Co schon genervt sind, können also entspannt in die Zukunft schauen 😉

Der Artikel Verifizierter Systemstart (Verified Boot) – Der Stand bei Linux erschien zuerst auf [Mer]Curius

Die Linux-Community, ihre Eigenheiten und komischen Ausprägungen beschäftigen mich schon länger. Das dürfte für viele Leser hier nichts Neues sein. Momentan frage ich mich aber, ob das Problem nicht sehr spezifisch die deutschsprachige Linux-Community betrifft?

Meine Beobachtung zu den Unterschieden bei Wikipedia hatte ich kürzlich schon mal hier dargelegt. Nun bin ich aufgrund eines anderen Themas auf die systemd-Artikel bei Wikipedia gestoßen. Schauen wir uns mal drei Artikel an, deren Sprache ich beherrsche:

Die Unterschiede sind eklatant. Das ist keineswegs ein „Wikipedia-Phänomen“, denn diese Artikel schreiben Prinzip-bedingt Autoren mit Affinität zum Thema Linux bzw. Mitglieder einer weiter gefassten „Linux-Community“.

Der englischsprachige Artikel ist sehr umfassend, was vermutlich an der größeren Autoren- und Leserschaft liegt. Es geht um Geschichte, technisches Design, die zugehörigen Komponenten, die Verbreitung unter den verschiedenen Linux-Distributionen und ganz am Schluss kommt die Rezeption bzw. Kritik an systemd, sowie alternative Implementierungen. Die spanischsprachige Wikipedia orientiert sich an der englischsprachigen Wikipedia, hat aber einen längeren Teil mit Anleitungscharakter und nur einen kurzen Kritikteil.

Bei der deutschen Wikipedia geht es ganz kurz um Geschichte und Funktion und dann auf knapp 7000 Zeichen um die Kritik an systemd. Dazu ist die Auswahl der Meldungen verglichen mit der englischsprachigen Version höchst tendenziös, da immer nur die Kritik und selten die Entscheidungen und Problemlösungen thematisiert werden. Repliken auf diese Kritik (Lennart Poetterings „The biggest Myths“), die in der englischsprachigen Wikipedia erwähnt werden, finden in der deutschsprachigen Wikipedia keinen Widerhall.

Der englischsprachige Leser hat am Ende der Lektüre einen guten Überblick über Geschichte, Design, Funktionen und Kritik an systemd. Dem deutschsprachigen Leser bleibt der Eindruck, eine katastrophale Technologie hätte sich irgendwie durchgesetzt.

Läuft da was falsch bei der deutschsprachigen Linux-Community? Ist das ein spezifisches Wikipedia-Problem? Mich würden da auch Meinungen bzw. Kommentare von Lesern interessieren, die vielleicht auch in anderssprachigen Linux-Communitys unterwegs sind.

Der Artikel Sonderwege der deutschsprachigen Linux-Community? erschien zuerst auf [Mer]Curius

Verschlüsselung für mobile Betriebssystem wie Android ist trivial und mit wenigen Einstellungen aktiviert. Wegen einiger Änderungen in der Vergangenheit gibt es für Android jedoch viele veraltete Informationen.

Verschlüsselung ist bei Desktop-Betriebssystemen wie Windows, Linux oder macOS eine peinliche Leerstelle. Obwohl es für alle drei Systeme leistungsfähige Lösungen gibt, verzichten alle Hersteller auf eine standardmäßige Aktivierung und Bewerben diese auch nicht aktiv. Während Windows und macOS sich wenigstens noch nachträglich verschlüsseln lassen, schauen Linux-Nutzer in die Röhre, wenn sie bei der Installation die entsprechenden Optionen nicht gesetzt haben.

Nicht so bei mobilen Betriebssystemen. Google und Apple haben in ihre Betriebssysteme für Tablets und Smartphones selbstverständlich leistungsstarke Verschlüsselungsmethoden implementiert, die im Hintergrund automatisch aktiviert werden, sobald der Anwender die Codesperre (sowie ggf. biometrische Verfahren) aktiviert. Bei Android hat Google hier in der Vergangenheit einige Änderungen vorgenommen, weshalb viele Berichte dazu veraltet sind und teils unverständliche Kürzel kursieren.

FDE: Full Disk Encryption

Die FDE-Verschlüsselung stand am Anfang. Technisch gesehen handelte es sich um eine Festplattenverschlüsselung auf Basis von dm-crypt. Insbesondere bei diesen tieferen Bereichen von Android merkt man oft die Linux-Basis, denn letztlich ist das nicht viel anderes als die LUKS-Vollverschlüsselung bei Linux.

Technisch war das natürlich etwas aufwendiger. Die Verschlüsselung erfolgte in der üblichen Blockdevice-Methode, bei der die gesamten Userdata-Partition verschlüsselt wurde. Die Verschlüsselung erfolgte mit einem einzigartigen Schlüssel, der gleichermaßen an den Benutzer-PIN gebunden und mittels TEE signiert wurde. Mit diesem Schlüssel wurden jene Bereiche verschlüsselt, in denen Android Benutzerdaten speichert.

Es gab nur ein grundlegendes Problem. Bei einem Neustart des Android-Smartphones waren viele Funktionen nicht verfügbar, bevor der Anwender seine Passphrase eingegeben hatte. Beispielsweise Wecker oder auch Telefonanrufe. Das war natürlich für ein Smartphone-Betriebssystem keine zufriedenstellende Lösung.

Diese Lösung steht nach einer längeren Übergangsphase nicht mehr zur Verfügung. Android ab Version 10 nutzt nur noch die FBE-Methode.

FBE: File Based Encryption

Um diese Beschränkungen zu beheben, hat Google ab Android 7 die dateibasierte Verschlüsselung (FBE) eingeführt. Dabei handelte es sich um eine Neuentwicklung auf Basis des für Android verwendeten Dateisystems ext4. Die Umstellung hat je nach OEM-Hersteller etwas gedauert, weshalb zwischen den Versionen 7 und 9 die Verschlüsselung je nach Konfiguration und Hersteller unterschiedlich funktioniert hat, aber heute ist FBE die Standardverschlüsselung.

Ein netter Nebenaspekt: Diese Erweiterung des ext4-Dateisystems zog mit Kernel 4.1 bzw. 4.6 (fscrypt) direkt upstream ein und kommt dadurch allen Linux-Anwendern zugute. Die Adaption verzögerte sich hier etwas, aber mit systemd-homed steht nun erstmals eine praktikable Umsetzung für den Linux-Desktop zur Verfügung.

Technisch gibt es zwei Bereiche. Den Standarspeicherplatz (Credential Encrypted = CE), der erst nach der Entschlüsselung zur Verfügung steht und den sogenannten Device Encrypted (DE) Speicher, der während des Direct Boot-Verfahrens Anwendungen ermöglicht mit begrenztem Datenzugriff zu funktionieren.

Anfänglich war ein Nachteil der FDE-Methode die fehlende Verschlüsselung der Metadaten (Berechtigungen, Dateigröße, Verzeichnis-Layout etc.). Diese berechtigte Kritik – z. B. in diesem Short Paper – kann man auch heute noch häufiger lesen, wenn es um Android-Verschlüsselung geht. Das Problem hat Google in Android 9 behoben und es ist somit nicht mehr aktuell.

Der wesentliche Unterschied für den Anwender: Mit FBE gesicherte Android-Geräte booten ganz normal in den Sperrbildschirm und bieten dort auch vor der Entschlüsselung durch den Anwender Funktionalitäten wie Anrufe, Wecker etc.

Aktivierung durch den Anwender

Sobald der Anwender einen PIN, eine Passphrase und/oder biometrische Merkmale hinterlegt hat, ist die Verschlüsselung aktiviert. Normalerweise geschieht dies bereits bei der Initialisierungsroutine. Anwender müssen mehrfache Warnungen überspringen, um ihre Geräte ohne entsprechenden Schutz nutzen zu können. Das ist meiner Ansicht nach die richtige Vorgehensweise. Überprüfen lässt sich der Status in den Einstellungen und Sicherheit und dort in Verschlüsselung und Anmeldedaten.

Hier ist wie aus dem folgenden Screenshot bei „Smartphone verschlüsseln“ der Status Verschlüsselt zu sehen. Weitere Einstellungsmöglichkeiten oder Handlungsbedarf bestehen nicht.

Der Artikel Verschlüsselung unter Android – Von FDE zu FBE erschien zuerst auf [Mer]Curius

10. Februar 2022

Do, 10. Februar 2022, Lioh Möller

In den vergangenen Wochen haben wir unterschiedliche Linux-Distributionen auf Einsteigertauglichkeit getestet. Dabei kamen zuvor definierte Bewertungskriterien zum Einsatz.

Folgende Distributionen konnten wir in unsere Tests einbeziehen:

Natürlich stellt die Auswahl nur einen Bruchteil der verfügbaren Linux-Derivate dar. Dennoch vermittelt sie ein gutes Gesamtbild über den aktuellen Stand der Entwicklungen.

Grundsätzlich lassen sich die Distributionen in drei Bereiche aufteilen. Dazu gehören einerseits klassische Ansätze wie bei openSUSE Leap, Ubuntu LTS, Debian GNU/Linux oder ZorinOS, andererseits Rolling-Release Lösungen wie Siduction, Manjaro oder openSUSE Tumbleweed. Fedora Silverblue verfolgt als einzige Immutable Distribution in unseren Tests neue Wege und bietet aufgrund der Stabilität und reibungslosen Updatefähigkeit ein ideales System für Einsteiger.

Davon unabhängig eignen sich alle vorgestellten Lösungen gut für einen ersten Kontakt mit dem Freien Betriebssystem. Teilweise unterstützen Assistenten oder Handbücher bei der Einarbeitung.

Fast alle getesteten Distributionen sind mit den drei grossen Desktop-Umgebungen, KDE-Plasma, GNOME-Shell und Xfce verfügbar. Obwohl wir nur jeweils eine Desktop-Variante getestet haben, sollte diese nicht den Ausschlag für die Wahl eines Einsteigers geben. Alle drei Umgebungen sind gut für Um- oder Einsteiger geeignet. Hier liegt es am persönlichen Geschmack, was einem besser gefällt.

Möchte man ein System haben, welches man einfach nur benutzen kann, ohne sich tiefer mit den Konzepten von Linux auseinanderzusetzen, ist Fedora Silverblue oder ZorinOS eine gute Wahl. Siduction hingegen bietet ausführliche Dokumentationen und Anlaufstellen bei Fragen an. Ambitionierte Einsteiger können zu Manjaro oder openSUSE Tumbleweed greifen. Und wer Linux von der Basis ab kennenlernen möchte, dem sei ein Blick in den Slackware basierten Linux-Kurs empfohlen.

9. Februar 2022

Die MZLA Technologies Corporation hat mit Thunderbird 91.6 ein planmäßige Update für seinen Open Source E-Mail-Client veröffentlicht.

Neuerungen von Thunderbird 91.6

Mit dem Update auf Thunderbird 91.6 hat die MZLA Technologies Corporation ein planmäßiges Update für seinen Open Source E-Mail-Client veröffentlicht und behebt damit aktuelle Sicherheitslücken. Darüber hinaus bringt das Update diverse Fehlerbehebungen der Versionsreihe 91, welche sich in den Release Notes (engl.) nachlesen lassen.

Der Beitrag Thunderbird 91.6 veröffentlicht erschien zuerst auf soeren-hentzschel.at.