staging.inyokaproject.org

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.Name }}"
                href="{{ .Permalink }}"
                />{{ end }}
    <xhtml:link
                rel="alternate"
                hreflang="{{ .Language.Name }}"
                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
{{- /*
  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)) -}}
{{- $txt := os.ReadFile (printf "%s/%s.txt" "static/iframe/vimeo" $code) -}}
<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="{{ $txt }}"></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 [" $txt "](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>

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
{{- /*
  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") -}}
{{- $txt := os.ReadFile (printf "%s/%s.txt" "static/iframe/youtube" $code) -}}
<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="{{ $txt }}"></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 [" $txt "](https://www.youtube.com/watch?v=" $code "&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>

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
127
128
129
---
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
        with:
          history: 0
          hugo_extended: true
      - 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.

Anlässlich des gestrigen Releases von KDE Plasma 5.24, das die Möglichkeit Fingerabdrücke zur Authentifizierung einzusetzen, offiziell integriert möchte ich hier noch mal explizit davor warnen. Erstens weil biometrische Daten nicht zur Authentifizierung geeignet sind und zweitens weil Linux nicht die sichere Infrastruktur dafür bietet.

Ich hatte vor etwas mehr als einem Jahr, als die Bestrebungen Fahrt aufnahmen, bereits vor dieser Methode gewarnt und möchte das hier noch mal aufgreifen. Biometrische Daten sind nicht nur ungeeignet zur Authentifizierung, sondern Linux bietet nicht die notwendige Infrastruktur, um solche Daten sicher abzulegen. Die Implementierung in Linux ist ein einziges Sicherheitsrisiko und Datenverlust eine plausible Möglichkeit. Nur weil etwas freie Software ist, darf man dem nicht blind vertrauen.

Biometrische Daten zur Authentifizierung

Der erste Punkt ist natürlich, dass man besser keine biometrischen Daten zur Authentifizierung nutzt. Sie vermitteln ein falsches Sicherheitsgefühl und beim Verlust lassen sie sich nicht ersetzen. Kursiert der eigene Fingerabdruck irgendwo, kann man sich schlecht einen neuen generieren lassen. Andererseits nutzten vor dem Aufkommen solcher Verfahren viel zu wenige Anwender sichere PINs oder Passwörter für ihre mobilen Geräte. Entweder schützte man diese gar nicht oder mit simplen Wischmustern. Fingerabdruck oder Gesichtserkennung bieten hier durchaus einen Mehrwert. Mehrwert aber nur, wenn dieser Fingerabdruck sicher gespeichert wird.

Auf keinen Fall mit Linux!

Die größte Gefahr besteht darin, dass der Fingerabdruck verloren geht bzw. gestohlen wird. Google, Apple, Microsoft – sie alle sind sich dessen bei biometrischen Daten bewusst. Alle drei Anbieter haben in ihren Geräten sogenannte Sicherheitschips verbaut (Apple: Secure Enclave; Google: Titan M; Microsoft: TPM) mit denen sie solche Daten schützen. Die Daten der Fingerabdrücke können nicht vom Gerät exportiert werden.

Irgendwer kommt jetzt bestimmt wieder mit proprietärem Code und proprietärer Hardware, der man nicht vertrauen kann. Das ist Mist, mit dem man den desolaten Zustand unter Linux schönredet! Jeder Programmierer macht Fehler, kein Code ist perfekt, manchmal muss man durch die Architektur neue Sicherheitsmethoden einbauen.

Google, Apple & Co haben diese Verfahren sehr effizient eingesetzt, bisher sind trotz milliardenfacher Nutzung keine Daten abhandengekommen. Zudem speichern diese Firmen die Fingerabdrücke nicht als Bild, sondern als Hashwert. Die Apps bekommen durch das ausgefeilte Berechtigungssystem keinen direkten Zugriff auf den Fingerabdruck selbst, sondern führt nur einen Abgleich mit dem Hash durch – grob vereinfacht erklärt. Das sind die technischen Standards zur Verarbeitung biometrischer Daten und sie sind gerade so ausreichend (eigentlich sollten wir gar keine biometrischen Daten verarbeiten!)

Linux kann gar nichts davon! Hinter solchen Frontends wie bei Plasma kommt fprint zum Einsatz, die Authentifizierung erfolgt über PAM. Linux kann momentan noch nicht ausgreift und flächendeckend mit Sicherheitschips umgehen. Die Daten werden somit einfach im System gespeichert (wo auch sonst bei den aktuellen Möglichkeiten?) und können von dort auch mit entsprechenden Rechten exportiert werden.

Und da kommt mir bitte keiner mit dem tollen Berechtigungssystem von UNIX bzw. Linux. Das ist uralt und garantiert nicht für so hoch sensible Daten entworfen worden. Ein kritischer Fehler und sudo, PolKit oder wo auch immer und und wir sitzen richtig im Dreck! Berechtigungskonzepte für Apps haben wir jenseits von Flatpak gar nicht und noch immer kommen genug Helden auf die Idee, grafische Programme mit Administratorrechten zu starten. KDE hat das gerade erst wieder offiziell für Dolphin möglich gemacht.

Ich finde es in Ordnung, wenn Linux-Anwender sagen, die misstrauen Sicherheitschips wie TPM, aber dann lassen sich halt manche Funktionen wie ein biometrisches Authentifizierungsverfahren nicht umsetzen. Man kann nicht alles haben!

Schlussbemerkung

Dieser Text ist sehr hart und sehr plakativ geschrieben und man könnte zu vielen Punkten mehr schreiben und manches differenzieren. Aber anlässlich der Veröffentlichung von KDE Plasma 5.24 gestern, sehe ich mich in der Pflicht hier eindrücklich zu warnen. Es ist schön, wenn Entwickler sich mit so etwas befassen und hier grundlegende Methoden entwickeln. Wenn ein System aber nicht die geeignete Hardware-Infrastuktur hat, dann kann man an einem gewissen Punkt nicht weiter arbeiten.

Wenn Microsoft, Apple oder Google biometrische Authentifizierungsmaßnahmen so stümperhaft umgesetzt hätten, wäre jeder IT-Experte in jeder denkbaren Weise über sie hergefallen. Bei Linux ist das wieder okay, weil es ja freie Software ist und so. Nein, einsehbarer Code schützt nicht vor allem! Wenn hier Daten verloren gehen, kommen Heartbleed, Boothole und Debians OpenSSL-Fiasko zusammen. Will sagen: Dann ist der Ruf von Linux endgültig im Eimer!


Nachtrag 09. Februar 2022:

Anscheinend bin ich mit meiner Meinung hier wenigstens nicht ganz alleine. Durch einen Kommentar auf Linuxnews bin ich auf diesen Bugreport bei Debian aufmerksam geworden, der auf eine entsprechende Diskussion im GitLab von fprint verweist. Bitte folgenden Kommentare zur Kenntnis nehmen:

There are no short-term plans to fixing this. Any attempts at encrypting the fingerprints would just be security through obscurity as the decryption would need to be made available to fprintd and would therefore be available to other processes.

The only way to currently safeguard the fingerprints is to run with SELinux, AppArmor or another LSM enabled, and made sure that only the fprintd binary has access to those saved fingerprints.

Mal abgesehen davon, dass ich selbst die dort angedachten Schutzmaßnahmen für unzureichend halte, nutzt nur Fedora / RHEL SELinux in einer ausreichenden Konfiguration.

Folgende Aussage des Entwicklers bitte sich ebenfalls Gemüte führen:

I think we might entertain such ideas if anyone can actually point to proper research that shows it is feasible. Before then, I will continue to assume that anything like that is impossible.

Bei diesem Problembewusstsein frage ich mich, ob Debakel wirklich noch die ausreichende Beschreibung ist?

Der Artikel Warnung: Nutzt keine Fingerabdruck-Authentifizierung mit Linux! erschien zuerst auf [Mer]Curius

Mi, 9. Februar 2022, Ralf Hersel

"Zorin OS ist eine auf Ubuntu basierende Linux-Distribution, die speziell für Linux-Neulinge entwickelt wurde. Sie hat eine Windows-ähnliche grafische Benutzeroberfläche und viele Programme, die denen von Windows ähneln. Zorin OS wird auch mit einer Anwendung geliefert, mit der Windows-Programme ausgeführt werden können. Das Ziel der Distribution ist es, eine Linux-Alternative zu Windows zu bieten und Windows-Benutzern alle Funktionen von Linux ohne Komplikationen zur Verfügung zu stellen", besagt die Selbstbeschreibung von Zorin OS.

Für diesen Test habe ich mich für die Variante 'Zorin OS 16 Core' entschieden. Neben dieser gibt es auch eine Pro- und eine Lite-Version. Die Unterschiede werden auf der Download-Seite erklärt. Kurz gesagt, Core verwendet die GNOME-Shell als Desktop-Umgebung, Lite nimmt Xfce und Pro bringt unter anderem einen Desktop-Layout Switcher mit und kostet 39 Euro.

Die Version 16 stammt von August 2021, hat einen 5.13er Kernel, Gnome 3.38, Python 3.5, LibreOffice 7.1 und basiert auf Ubuntu 20.04.3 LTS. Über Zorin OS haben wir bereits des Öfteren geschrieben; schaut in den Artikelindex, um die bisherigen Berichte zu lesen.

Installation

Die ISO-Datei ist 2.7 GB gross und kann von der Download-Seite bezogen werden. Da Zorin auf Ubuntu beruht, gibt es zum Installationsprozess nicht viel zu sagen. Er funktioniert und sieht so aus, wie man ihn von fast allen Ubuntu-Derivaten kennt.

Eine Sache hat mich dennoch überrascht, die 'Volkszählung'. Diese seltsam übersetzte Option für die Telemetrie stammt auch von Ubuntu und ist als Opt-out ausgelegt. Im Gegensatz zum Original gibt es während des Installationsprozesses keine Zorin-eigene Slide-Show. Stattdessen sieht man nur das Zorin-Logo.

Ich gebe 4 von 5 Punkten, wegen der 'Volkszählung' und der verpassten Chance, die Anwenderin während der Installation zu informieren/unterhalten.

Einführung

Nach dem ersten Start sieht man die stark modifizierte GNOME-Desktop-Umgebung, die (für meinen Geschmack) sehr gut aussieht. Eine Begrüssung oder Einführung gibt es leider nicht. Der Desktop bietet zwar ein modernes Design, folgt aber dem klassischen Bedienkonzept. Es gibt ein Panel mit Menü, Favoriten-Apps und den System-Tray.

Wie man im Screenshot sehen kann, sieht alles sehr klar und aufgeräumt aus. Im Panel befinden sich: Menü, Desktop-Overview, Firefox, Dateien und der Software-Manager. Im System-Tray geht es auch sauber zu: Lautstärke, Netzwerke, Einstellungen, An/Aus sind in einem Menü vereint. Rechts daneben steht der Kalender.

4 von 5 Punkten wegen der fehlenden Begrüssung und Einführung.

Vollständigkeit

Die Software-Ausstattung (im Menü) sieht folgendermassen aus:

  • Büro: LibreOffice, Evolution, GNOME-Kalender und -Kontakte
  • Grafik: Eye of GNOME, GNOME-Fotos, GIMP
  • Hilfsprogramme: diverse, aber nicht zu viele
  • Internet: Firefox, Remmina
  • Multimedia: Brasero, Pitivi, Cheese, Rhythmbox, Sound Recorder, Totem
  • Spiele: Solitaire, Mahjongg, Minen, Quadrapassel, Sudoku
  • Systemwerkzeuge: diverse, aber nicht zu viele
  • Zubehör: Aufgaben, Dateien, Karten, Text-Editor, Uhren, Wetter

Die Wahl der vorinstallierten Apps gefällt mir sehr gut; sie stellt eine gute Balance zwischen Notwendigem und Überfrachtung dar.

Das Software-Center bindet die Repositories: main, universe, restricted und multiverse ein. Ausserdem werden eine ganze Reihe von Zorin-eigenen PPAs geladen. Flatpaks und Snap-Pakete stehen ebenfalls zur Verfügung, wobei man die Einstellungen dafür in den Optionen von GNOME-Software nicht findet.

Bei den Einstellmöglichkeiten findet man die nicht-modifizierten GNOME-Settings, das Werkzeug zur GNOME-Optimierung (Tweak-Tool) fehlt, dafür gibt es die Zorin-Appearance mit Optionen für das Desktop-Konzept, Farb-Themen und weiteren Einstellungen. Das ist sehr Einsteiger-freundlich umgesetzt.

Ich gebe 5 von 5 Punkte.

Stabilität

Machen wir es kurz: Ubuntu 20.04 LTS. 5 von 5 Punkten.

Vorkonfiguration

Aus den vorherigen Kapiteln kann man bereits herauslesen, dass die Konfiguration von Zorin OS sehr gut gelungen ist. Das Design und die Benutzerfreundlichkeit schaffen den schwierigen Spagat zwischen modern, funktional, einfach zu bedienen, nicht überlastet, aber auch nicht zu sehr vernagelt. Meiner Meinung nach gehört das zu den Hauptaufgaben einer GNU/Linux-Distribution, denn, Kernel, Systemsoftware, Pakete und Desktops können alle Distros zusammenstellen; die hohe Kunst besteht in der Ausgewogenheit. Diese gelingt Zorin wie kaum einer anderen Distribution.

Klare 5 von 5 Punkten.

Update-Prozess

Machen wir es nicht ganz so kurz wie bei der Stabilität: der Ubuntu Update-Prozess ist bekannt und etabliert. Die Aktualisierungs-Einstellung, welche aus dem Software-Center heraus erreicht werden können, sind minimalistisch. Hier gibt es nur die Optionen 'Automatische Updates' und 'Benachrichtigungen bei eben diesen'; beide Optionen sind eingeschaltet. In der Aktualisierungsverwaltung können beliebige Quellen hinzugefügt und viele weitere Einstellungen gemacht werden. Es gibt keine Optionen zu Flatpaks und Snaps; für die Einsteigerin mag das aber genau das Richtige sein.

Ein Check im Terminal ergab, dass keine Snap-Pakete vorhanden sind und für Flatpaks nicht einmal die Basis installiert ist. Zu Testzwecken habe ich TextSnatcher aus dem Software-Center installiert; die App ist ein Flatpak. Wie erwartet, werden dabei zuerst einmal alle Flatpak-Voraussetzungen installiert; was in meinem Test sehr lange dauerte. Solange möchte kein Einsteiger warten. Hier wäre es besser gewesen, die Flatpak-Basis in der Distro bereitzustellen, insbesondere, da das Software-Center Flatpaks anbietet.

Dafür gebe ich 4 Punkte.

Bewertung

Kriterium Bewertung (max. 5)
Installation 🏆️🏆️🏆️🏆️
Einführung 🏆️🏆️🏆️🏆️
Vollständigkeit 🏆️🏆️🏆️🏆️🏆️
Stabilität 🏆️🏆️🏆️🏆️🏆️
Vorkonfiguration 🏆️🏆️🏆️🏆️🏆️
Update-Prozess 🏆️🏆️🏆️🏆️
Summe 27

Fazit

'Zorin OS 16 Core' ist einer der Einsteiger-freundlichsten Distributionen, die ich bisher gesehen habe.

Zorin OS mit dunklem Thema

Obwohl ich bei den Kriterien Installation, Einführung und Update-Prozess jeweils einen Punkt vom Maximalwert abgezogen habe, ist Zorin ein Tipp für Einsteiger:innen in die wunderbare GNU/Linux-Welt. Diese Distribution schafft eine Ausgewogenheit, die ich selten gesehen habe. Zorin ist einerseits modern und schick beim Erscheinungsbild, holt den Anwender aber beim Bedienkonzept dort ab, wo er sich auskennt, nämlich bei MS-Windows.

Da Zorin 16 auf Ubuntu 20.04 aufbaut, ist damit zu rechnen, dass es in diesem Jahr eine neue Version geben wird, die auf Ubuntu 22.04 LTS setzt, und damit auch ein paar der Neuerungen von GNOME 42 abbekommen wird.

Download: https://zorin.com/os/download/

8. Februar 2022

Mozilla hat Firefox 97 für Windows, Apple macOS und Linux veröffentlicht. Dieser Artikel fasst die wichtigsten Neuerungen zusammen – wie immer auf diesem Blog weit ausführlicher als auf anderen Websites.

Download Mozilla Firefox für Microsoft Windows, Apple macOS und Linux

Neue Farbgebungen für Firefox

Bereits jetzt nutzen mehr Nutzer ein farbiges Theme als das mit Firefox standardmäßig ausgelieferte dunkle Theme oder auch Alpenglow, das optionale Design, welches seit Firefox 81 zusätzlich ausgeliefert wird. Dafür bietet Mozilla auf seiner Erweiterungs-Plattform addons.mozilla.org eine Auswahl von zig tausenden Themes an.

Um noch mehr Nutzer auf diese Möglichkeit der Anpassung aufmerksam zu machen, hatte Mozilla Firefox 94 und folgende Versionen mit 18 zusätzlichen Themes, sogenannten Farbgebungen, ausgeliefert, welche mit dem heutigen Tag ausgelaufen sind und nicht mehr zur Auswahl stehen. Wer sich für eine dieser Farbgebungen entschieden hatte, kann diese aber noch weiterhin nutzen.

Mit Firefox 97 liefert Mozilla dafür sechs ganz neue Farbgebungen aus – auch diese sind wieder zeitlich limitiert verfügbar.

Firefox 97

Außerdem zeigt die Add-ons-Verwaltung deaktivierte Theme nicht länger blasser an, womit die Vorschaubilder repräsentativer sind, und fügt auch kein „(deaktiviert)“ mehr hinter den Namen an. Dass Themes deaktiviert sind, wird ohnehin durch die entsprechende Überschrift klar.

Firefox 97

Verbesserungen der Entwicklerwerkzeuge

In den Entwicklerwerkzeugen erlaubt der Inspektor bei gedrückter Shift-Taste jetzt auch die Auswahl von Elementen, welche aufgrund einer pointer-events: none-Regel ansonsten nicht auswählbar wären.

Das Farbpipette-Werkzeug funktioniert jetzt auch auf Websites, bei denen eine Content Security Policy (CSP) zuvor verursachen konnte, dass das Werkzeug nicht funktionierte.

Sonstige Neuerungen von Firefox 97

Für die Darstellung von Scroll-Leisten nutzt Firefox unter Windows jetzt den neuen Stil von Windows 11.

Unter macOS wurden Verbesserungen beim Laden von System-Schriften vorgenommen, welche unter bestimmten Voraussetzungen in einem schnelleren Öffnen und Wechseln von Tabs resultieren.

Die Verwendung der „Zum Tab wechseln“-Funktion in der Adressleiste beeinflusst jetzt auch die Gewichtung der Adressleisten-Vorschläge, so dass ein Häufiges Verwenden dieser Funktion für eine bestimmte Seite dafür sorgen kann, dass diese in den Vorschlägen zukünftig höher erscheint.

Die Passwortverwaltung unter about:logins hat eine verbesserte Tastatur-Steuerung erhalten (Pfeil rauf / runter zum Wechseln des Eintrags, Enter zum Auswählen, ESC zum Leeren des Suchfelds). Außerdem wurde die Performance des CSV-Imports von Zugangsdaten (kann in about:config via signon.management.page.fileImport.enabled aktiviert werden) signifikant verbessert.

Hardware-WebRender ist jetzt auch auf Systemen mit Lucienne-Chip von AMD standardmäßig aktiviert.

Die Option print.tab_modal.enabled, um den alten Drucken-Dialog, welcher in Firefox 86 vor knapp einem Jahr ersetzt wurde, wieder zu aktivieren, wurde entfernt. Netzwerk-Drucker, die von Firefox bislang nicht angezeigt worden sind, werden jetzt angezeigt, sofern diese innerhalb einer bestimmten Zeit gefunden werden.

Das Problem, dass auf YouTube bei Verwendung der Textsuche die Positionen der Suchergebnisse neben der Scroll-Leiste nicht angezeigt worden sind, gehört der Vergangenheit an.

Verbesserungen der Webplattform

Die CSS-Einheiten cap und ic werden ab sofort für Längenangaben unterstützt, außerdem die @-Regel @scroll-timeline und die animation-timeline-Eigenschaft. Die Eigenschaft color-adjust wurde umbenannt in print-color-adjust. Neu ist auch die Unterstützung von @layer sowie die Unterstützung für die Eigenschaft scrollbar-gutter.

In SVG-Grafiken kann das d-Attribut jetzt auch via CSS gesteuert werden.

Eine Übersicht über alle Verbesserungen der Webplattform sowie Verbesserungen für WebExtension-Entwickler gibt es wie immer in den MDN web docs.

Geschlossene Sicherheitslücken

Auch in Firefox 97 wurden wieder mehrere Sicherheitslücken geschlossen. Alleine aus Gründen der Sicherheit ist ein Update auf Firefox 97 daher für alle Nutzer dringend empfohlen.

Dazu kommen wie immer weitere Fehlerbehebungen sowie sonstige Verbesserungen unter der Haube dazu.

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

Zu einem guten Smart Home gehört natürlich auch eine schöne „Kommandozentrale“. Obwohl das Haus ja schlau ist und alles selbst entscheiden soll, möchte man ja dennoch wissen, was gerade los ist. Der Home Assistant, eine freie und open source Software für Hausautomatisierung, bietet mit seinen Dashboards genau solche Übersichten an. Mit wenigen Klicks lassen sich die Messdaten aus dem Haus und um das Haus visualisieren.

Einen Regenradar in Home Assistant kann man einbinden, wenn man die Bild-URL als Kachel im Dashboard hinzufügt.

Regenradar und weitere Wetterdaten

Das Wetter spielt natürlich eine entscheidende Rolle für das Haus und die Bewohner. Die erste Anlaufstelle für aktuelle Wetterdaten und Vorhersagen könnte beispielsweise OpenWeatherMap sein. Die API ist gut dokumentiert, die Einbindung in den Home Assistant ist reibungslos, der Dienst ist kostenlos und es fällt keine Werbung an. Einzig eine Registrierung auf der Webseite ist nötig, um den verpflichtenden API-Schlüssel zu erhalten.

Was leider fehlt, ist der Regenradar. Jede Vorhersage ist ja nur so gut, wie die, die man mit eigenen Augen sieht. Darum lohnt es sich, einen Regenradar in das Home Assistant Dashboard einzubinden.

Die meisten Regenradare lassen sich nicht besonders einfach einbinden. Und falls doch, sind sie meistens mit Werbung übersät. Um so schöner ist es, dass die Webseite morgenwirdes.de den Regenradar vom Deutschen Wetterdienst (DWD) aufbereitet und uns als animierte gif-Datei zur Verfügung stellt. Die Einbindung in den Home Assistant ist explizit erlaubt und funktioniert folgendermaßen.

Einbindung in den Home Assistant

Zunächst bastelt man sich die URL zusammen, die man für sein Dashboard braucht. Das geht relativ einfach, wenn man sich die Struktur der URL ansieht.

https://morgenwirdes.de/api/v3/gif4.php?plz=12345&delay=70&type=1&zoomlvl=1&bar=0&map=1&textcol=ffffff&bgcol=03a9f4

Die Flagge „plz“ gibt die Postleitzahl vor. Delay ist die Zeit in Millisekunden, die pro Zeitstufe angezeigt wird. Es gibt zwei Farbschemen der Karte, typ=0 oder 1. Der Zoomlvl gibt den Zoomlevel an (hier 1 für ganz Deutschland). Eine Zeitleiste (bar) und Hintergrundkarte (map) können optional eingeblendet werden. Farblich anpassen kann man die Karte mit den beiden letzten Flaggen.

In der configuration.yaml habe ich das dann als Kamera eingebunden. Das hat den Vorteil, dass die Daten immer aktualisiert werden. Würde man es als Bild einbinden, könnte es sein, dass der Regenradar nur aus dem Cache geladen wird und deshalb nicht aktuell ist.

##### Kameras und Wetterkarten
camera:
  - platform: generic
    still_image_url: 
https://morgenwirdes.de/api/v3/gif4.php?plz=12345&delay=70&type=1&zoomlvl=1&bar=0&map=1&textcol=ffffff&bgcol=03a9f4
    name: regenradar

In Lovelace, dem Dashboard von Home Assistant, fügt man nun ein Picture-Glance hinzu, schon ist das Radar da:

type: picture-glance
title: Regenradar
entities: []
camera_image: camera.regenradar

Update 15.04.2022:

Ich habe den Artikel angepasst, das Bild wird nicht mehr als normales Bild, sondern als Kamera eingebunden. Dadurch wird das Radar zuverlässiger aktualisiert.

The post Home Assistant: Animiertes Regenradar einfügen first appeared on bejonet - Linux | Smart Home | Technik.

Es gibt vor allem in Deutschland kollektive Vorbehalte gegen moderne Techniken wie TPM oder Secure Boot. Sie sind nicht exklusiv für die Linux-Gemeinschaft, aber dort besonders stark verbreitet. Diese sind nicht auf Fakten gegründet, sondern basieren auf langjährig gepflegten Mythen.

Ich beschäftige mich aktuell ja mit Themen wie TPM. Ein Thema, um das ähnlich wie um Secure Boot unfassbar viel Fear, Uncertainty and Doubt (FUD) gestreut wird. Das ist keinesfalls von Nischenphänomen, sondern findet sich an hochkarätiger Stelle.

Die englischsprachige Wikipedia schreibt in ihrem Artikel als Einleitung zu TPM:

Trusted Platform Module (TPM, also known as ISO/IEC 11889) is an international standard for a secure cryptoprocessor, a dedicated microcontroller designed to secure hardware through integrated cryptographic keys. The term can also refer to a chip conforming to the standard.

TPM is used for digital rights management (DRM), Windows Defender, Windows Domain logon, protection and enforcement of software licenses, and prevention of cheating in online games.

One of Windows 11’s system requirements is TPM 2.0. Microsoft has stated that this is to help increase security against firmware and ransomware attacks.

Und weiter:

Overview

Trusted Platform Module provides
A hardware random number generator
Facilities for the secure generation of cryptographic keys for limited uses.
Remote attestation: Creates a nearly unforgeable hash key summary of the hardware and software configuration. One could use the hash to verify that the hardware and software have not been changed. The software in charge of hashing the setup determines the extent of the summary.
Binding: Encrypts data using the TPM bind key, a unique RSA key descended from a storage key[clarification needed].
Sealing: Similar to binding, but in addition, specifies the TPM state for the data to be decrypted (unsealed).
Other Trusted Computing functions for the data to be decrypted (unsealed).

Wikipedia, Artikel „Trusted Platform Module“ unter Creative Commons Attribution-ShareAlike 3.0 Unported License (abgerufen am 08. Februar 2022.

Der deutschsprachige Artikel beginnt mit:

Das Trusted Platform Module (TPM) ist ein Chip nach der TCG-Spezifikation, der einen Computer oder ähnliche Geräte um grundlegende Sicherheitsfunktionen erweitert. Diese Funktionen können beispielsweise dem Lizenz- und Datenschutz oder der nachrichtendienstlichen Kontrolle dienen. Der Chip verhält sich in einigen Punkten wie eine fest eingebaute Smartcard, allerdings mit dem wichtigen Unterschied, dass er nicht an einen konkreten Benutzer, sondern an den lokalen Computer gebunden ist. Außer der Verwendung in PCs und Notebooks kann das TPM in PDAs, Mobiltelefonen und Unterhaltungselektronik integriert werden. Ein Gerät mit TPM, speziell angepasstem Betriebssystem und entsprechender Software bildet zusammen eine Trusted-Computing-Plattform (TC-Plattform). Eine solche „vertrauenswürdige Plattform“ kann nicht mehr entgegen den Interessen des Herstellers genutzt werden, sofern dieser Beschränkungen festgelegt hat. Ein möglicher Vorteil für einen normalen Benutzer eines solchen Systems liegt im Schutz gegen softwareseitige Manipulation durch unbefugte Dritte.

Der Chip ist aktuell überwiegend passiv und kann weder den Bootvorgang noch den Betrieb direkt beeinflussen. Er enthält einen eindeutigen kryptografischen Schlüssel und kann damit zur Identifizierung des Rechners genutzt werden. Das ist jedoch nur möglich, wenn der Eigentümer das Auslesen dieser Information auch gestattet hat. Bei x86-basierten PCs konnte das TPM bisher im BIOS vollständig deaktiviert werden, so dass keine seiner Funktionen zur Verfügung steht. Allerdings gibt es immer mehr Anwendungen, die nur auf einer TC-Plattform mit aktiviertem TPM laufen, wie es z. B. bei Windows seit der Version 11 der Fall ist.

Wikipedia (de.), Artikel „Trusted Platform Module“ unter Creative Commons Attribution-ShareAlike 3.0 Unported

Der Unterschied ist eklatant. Der deutsche Artikel enthält in der Einleitung FUD in seiner Reinform, kaschiert und versteckt im Mantel vorgeblich neutraler Beschreibung plus den Hinweis, wie man es abschalten kann.

Das selbe Spiel kann man mit Secure Boot durchspielen. Einmal der Eintrag in der deutschsprachigen Wikipedia und einmal der Eintrag in der englischsprachigen Wikipedia. Aus Gründen der Redundanz verzichte ich hier auf die Vollzitate, denn der Mechanismus ist der Gleiche. Der Abschnitt in der deutschsprachigen Wikipedia ist tendenziös, verweist auf uralte Sicherheitslücken und hinsichtlich der Zertifikate und Shim auch nicht korrekt. (Vgl. dazu auch das Zitat weiter unten aus dem Debian Wiki)

Da wundert es einen nicht, warum in Deutschland auf die Ankündigung von Microsoft, künftig in Windows 11 TPM 2.0 zur Voraussetzung zu machen, so viel Unsicherheit und Vorbehalte vorherrschten. Dabei ist das langfristig die richtige Entscheidung von Microsoft. Ein etwas überzeichneter Artikel dazu, der aber vielleicht für manchen gewinnbringend sein kann: Packungsbeilage TPM.

In einigen Kreisen – besonders gerne bei Open Source-Anhängern – herrschen bezüglich solcher Techniken wie TPM oder Secure Boot unglaubliche Vorbehalte, die teilweise auf langjährig gepflegten Mythen basieren und bei denen einem abstrakten Risiko erstaunlich selten der konkrete Nutzen gegenüber gestellt wird. Die Ursache dafür liegt meiner Ansicht nach in alten Berichten, die vor vielen Jahren Angst geschürt haben. Das ist irgendwie bei vielen Lesern – vor allem im deutschsprachigen Raum – hängen geblieben. Das hat man inzwischen selbst bei den ursächlichen Medien eingesehen, die heute kritisch auf die eigene damalige Berichterstattung schauen und festhalten, dass daran nichts dran ist.

Noch erstaunlicher ist, dass die FUD-Verbreiter in der Linux-Community sogar gegen die eigenen Knowledge-Bases – die offiziellen Wikis der Distributionen – argumentieren. Dazu kann man beispielsweise auf die tollen Artikel zu TPM und Secure Boot im Arch Linux Wiki verweisen, aber auch konservativere und auf Freiheit bedachte Distributionen wie Debian haben das längst sauber dokumentiert:

What is UEFI Secure Boot NOT?

UEFI Secure Boot is not an attempt by Microsoft to lock Linux out of the PC market here; SB is a security measure to protect against malware during early system boot. Microsoft act as a Certification Authority (CA) for SB, and they will sign programs on behalf of other trusted organisations so that their programs will also run. There are certain identification requirements that organisations have to meet here, and code has to be audited for safety. But these are not too difficult to achieve.

SB is also not meant to lock users out of controlling their own systems. Users can enrol extra keys into the system, allowing them to sign programs for their own systems. Many SB-enabled systems also allow users to remove the platform-provided keys altogether, forcing the firmware to only trust user-signed binaries.

Debian Wiki Artikel Secure Boot (zuletzt abgerufen am 08.02.2022)

Aus diesem Grund hat man viel zu lange in der Linux-Community Bausteine wie Secure Boot oder TPM links liegen gelassen und es kursieren abenteuerliche Anleitungen für Einsteiger, wie böse Secure Boot ist und was sie alles in UEFI-Settings deaktivieren sollten, bevor sie Linux installieren können.

Die Zukunft gehört solchen Technologien, denn die Sicherheitskonzepte von vor 20 Jahren sind halt auf die Bedrohungen von vor 20 Jahren gemünzt. Heute speichern wir andere Daten auf den Geräten, nutzen sie für andere Zwecke und sind anderen Bedrohungsszenarien ausgesetzt. „Mein System ist Open Source und ich gebe ein Passwort beim Start ein“ ist kein alleinig ausreichendes Sicherheitskonzept mehr. Passend zum Thema sind kürzlich die neuen Sicherheitsrichtlinien der US-Regierung herausgekommen (die natürlich weit darüber hinausgehen). Das BSI wird sicher irgendwann nachziehen und wenn man nicht wünscht, dass Linux eines Tages offiziell vor der Tür bleiben muss, weil es elementare Sicherheitsstandards nicht erfüllt, dann ist jetzt die Zeit proaktiv mit der Implementierung zu beginnen.

Mobile Plattformen und macOS haben vorgemacht, wohin die Reise geht und Windows zieht hier mit der neuen Version Windows 11 konsequent nach. Dabei schließen sie dankenswerterweise die PC-Plattform nicht ab, sondern ermöglichen es Linux ebenfalls von diesen Entwicklungen zu profitieren.

Also hört auf jedem zu raten, Secure Boot und TPM zu deaktivieren und blockiert nicht TPM über die Modul-Blacklist – es sei denn, ihr versteht wirklich, warum ihr was tut.

Der Artikel Kollektive Vorbehalte gegen TPM und Secure Boot – Ängste, Unsicherheit und Zweifel erschien zuerst auf [Mer]Curius

7. Februar 2022

LUKS (dm-crypt) ist die Standardverschlüsselungsmethode. Dank systemd ist es seit Kurzem ein vollständig mit LUKS verschlüsseltes Linux-Betriebssystem mittels FIDO2 oder TPM zu entsperren. Das kann in Kombination mit anderen Schutzmechanismen Sinn ergeben.

TPM und ein modernes Sicherheitskonzept

Die Sicherheit von Linux (Sicherheit im Sinne des Schutzes der Daten vor unbefugte Einsichtnahme) ist ziemlich einfach aufgestellt. Mach eine Vollverschlüsselung und entsperre beim Hochfahren den Container und gut ist. Was mit den Daten im Standby oder bei der Benutzerabmeldung passiert, ist damit ebenso wenig klar wie Themen wie eine vollständige verifizierte Bootkette. Linux hinkt hier hinter Systemen wie Android oder Windows 11 hinterher.

Das Thema ist aber inzwischen auf der Agenda und ein wichtiger Baustein hier ist das Trusted Platform Module (TPM). Natürlich gehört dazu viel mehr, als hier jetzt aufgezeigt werden kann, aber man profitiert bereits von wichtigen Vorarbeiten wie systemd-cryptenroll.

Eine automatische Entsperrung des LUKS-Systemcontainers mittels TPM macht natürlich keinen Sinn, wenn es der einzige Schutzmechanismus ist. Ein Angreifer benötigt dann nur das Gerät, um an alle Daten zu kommen. Wenn man aber z. B. seine Benutzerdaten mittels systemd-homed verschlüsselt hat und diese bereits geschützt sind, geht es um andere Angriffsszenarien. Zum Beispiel ein sogenannter „Evil Maid“-Angriff, bei dem das Speichermedium entwendet oder ein Systemabbild erstellt wurde. Angriffe auf ein verschlüsseltes System lassen sich schließlich viel leichter mit einem solchen Abbild durchführen.

Hier kommt TPM ins Spiel, denn während die Vollverschlüsselung im Normalbetrieb nicht weiter auffällt, scheitert der Startvorgang, sobald das Speichermedium in einer anderen Hardware steckt oder – und das ist der zweite relevante Punkte – sofern jemand am Secure Boot zugange war. Was natürlich voraussetzt, dass Secure Boot aktiviert ist. Das ist leider dank einer verbreiteten Unart bei Linux oft nicht der Fall, obwohl alle ernst zunehmenden Linux-Distributionen Secure Boot unterstützen.

LUKS mit TPM entsperren am Beispiel von openSUSE

LUKS mit TPM zu entsperren ist gegenwärtig noch experimentell und sollte nur von Personen mit tiefer gehenden Linux-Kenntnissen umgesetzt werden.

Die folgende Anleitung basiert auf openSUSE Tumbleweed, weil die SUSE-Entwickler hier bereits erste relevante Vorarbeiten geleistet haben. Sie lässt sich mit Sicherheit auch auf andere moderne Linux-Distributionen übertragen.

Das relevante Werkzeug systemd-cryptenroll kann nur mit LUKS2-Containern umgehen. OpenSUSE legt bei der Installation aber immer noch einen LUKS1-Container an. Deshalb muss in einem ersten Schritt dieser LUKS konvertiert werden. Das ist einfach und klappt zuverlässig. Einfach mit einem Live-Medium starten und mit folgendem Befehl den LUKS-Container konvertieren:

# cryptsetup convert /dev/<laufwerk> --type luks2 

Konzeptionell bedingt funktioniert die Methode aktuell nur, wenn eine unverschlüsselte Boot-Partition zum Einsatz kommt. Das war bis vor Kurzem noch völliger Standard, inzwischen kann man aber LUKS-Container auch in GRUB entschlüsseln. Wegen der erheblichen Verzögerung nutze ich das jedoch nicht, weshalb das für mich keine Einschränkung ist.

Mein Partitionsszenario sieht wie folgt aus:

:~> sudo lsblk 
NAME                                                MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS
nvme0n1                                             259:0    0 476,9G  0 disk  
├─nvme0n1p1                                         259:1    0   300M  0 part  /boot/efi
├─nvme0n1p2                                         259:2    0     1G  0 part  /boot
└─nvme0n1p3                                         259:3    0 475,6G  0 part  
  └─cr_nvme-SAMSUNG_MZVLQ512HALU-000H1_S4UHNF0NA02912-part3
                                                    254:0    0 475,6G  0 crypt 
    ├─vg--system-lv--system                         254:1    0    20G  0 lvm   /var
    │                                                                          /usr/local
    │                                                                          /srv
    │                                                                          /root
    │                                                                          /opt
    │                                                                          /
    └─vg--system-lv--benutzerdaten                  254:2    0 455,6G  0 lvm   /home/gheim
                                                                               /home

Nun installiert man noch die TPM-Tools:

$ sudo zypper install tpm2.0-tools

Anschließend rollt man mittels systemd-cryptennroll TPM aus:

Die Verwendung von PCR 7 schützt nicht vor Änderungen an der /boot-Partition. Dadurch kann ein Angreifer eine modifizierte initrd booten und auf die entschlüsselten Daten zugreifen! Um auch diese in den Schutz einzubeziehen, benötigt man PCRs 2, 4, 8 und 9. Dabei müsste man nach jeder Kernel- und Bootloader-Aktualisierung sowie nach jedem Neuaufbau der initrd den Schlüssel neu hinterlegen. Das ist aktuell noch nicht praktikabel und deshalb empfehle ich hier PCR 7. Zukünftig wird das hoffentlich besser werden, das ganze Konzept ist momentan noch work in progress.

Folgende PCR Definitionen stehen theoretisch zur Verfügung:

PCRErklärung
0Core system firmware executable code; changes on firmware updates             
1Core system firmware data/host platform configuration; typically contains serial and model numbers, changes on basic hardware/CPU/RAM replacements   
2Extended or pluggable executable code; includes option ROMs on pluggable hardware
3Extended or pluggable firmware data; includes information about pluggable hardware   
4Boot loader; changes on boot loader updates  
5GPT/Partition table; changes when the partitions are added, modified or removed  
6Power state events; changes on system suspend/sleep  
7Secure boot state; changes when UEFI SecureBoot mode is enabled/disabled   
8sd-boot measures the kernel command line in this PCR.   
$ sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 /dev/nvme0n1p3 ##Laufwerknamen anpassen

Danach muss man noch die /etc/crypttab modifizieren. Dort dürfte nur eine Zeile hinterlegt sein (ansonsten die entsprechende Systempartition wählen). Dort am Ende Folgendes eintragen: tpm2-device=auto. Das Ergebnis müsste in etwa wie folgt aussehen:

cr_nvme-...  UUID=b7d...  none  x-initrd.attach,tpm2-device=auto

Nun muss noch initrd neu generiert werden:

§ sudo dracut -f

Nach einem Neustart sollte nun keine Passwortabfrage beim Bootvorgang erscheinen und man direkt beim Loginbildschirm landen, wo man den verschlüsselten systemd-homed-Account anmeldet, um seine Daten zu entsperren.

Nacharbeiten und alternative Möglichkeiten

Das LUKS-Volume kann bei einem Problem mit TPM (was in diesem experimentellen Stadium nicht ausgeschlossen werden kann) immer noch mit den zusätzlich hinterlegten Passwörtern entsperrt werden. TPM belegt einfach einen Keyslot in LUKS. Wenn man sich vollständig auf TPM verlassen möchte, muss man die entsprechenden anderen Keys entfernen. Alternativ kann man auch einen langen und besonders sicheren Schlüssel als Wiederherstellungsschlüssel hinterlegen, was ich sowieso empfehle.

Alternativ lässt sich das Prozedere auch mit einem FIDO2-Stick anstelle von TPM durchführen, was vor allem Sinn macht, wenn man kein separat verschlüsseltes Home-Verzeichnis hat und stattdessen einen zweiten Faktor zur Authentifizierung des LUKS-Containers einziehen möchte. Dadurch verschiebt sich das Schutzkonzept, weshalb das nicht hier im Artikel behandelt wird. In dem hier behandelten Kontext wäre ein FIDO2-Stick eher als Schutzmaßnahme für systemd-homed geeignet – was selbstredend auch unterstützt wird. Das Vorgehen unterscheidet sich nur in wenigen Punkten vom hier geschilderten Szenario und kann dem openSUSE-Wiki entnommen werden.

Zusammengefasst

Angesichts der noch sehr jungen Werkzeuge und eher rudimentären Vorarbeiten der Distribution, funktioniert das Ganze bereits sehr gut. Die systemd-Entwickler haben einmal mehr bewiesen, was für gute Werkzeuge sie den Linux-Anwendern zur Verfügung stellen und damit alte, unzureichende und unnötig komplexe Lösungen überflüssig machen.

Die aktuelle Entwicklung ist ein Übergangsstadium hin zu einer verifizierten Kette vom Systemstart bis zum Login des Anwenders, bei der Signierung und Verschlüsselung ineinandergreifen und nur jene Teile verschlüsselt sind, die es auch sein müssen.

Der Artikel Verschlüsseltes Linux mit LUKS mittels TPM 2.0 entsperren erschien zuerst auf [Mer]Curius