<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Pascua Theus</title><link>https://theus.name/</link><description>Softwareentwicklung, IT-Sicherheit, Tooling</description><generator>Hugo</generator><language>de-DE</language><lastBuildDate>Sat, 03 Oct 2026 13:09:01 +0200</lastBuildDate><atom:link href="https://theus.name/index.xml" rel="self" type="application/rss+xml"/><item><title>Nie wieder Merge-Konflikte im Changelog</title><link>https://theus.name/post/changelog-konflikte-vermeiden/</link><pubDate>Sat, 03 Oct 2026 13:09:01 +0200</pubDate><guid>https://theus.name/post/changelog-konflikte-vermeiden/</guid><description>&lt;p&gt;Wer schon einmal im Team an einem Projekt gearbeitet hat, kennt das Problem:&#10;Hat ein Kollege einen richtig guten Tag, schließt er gleich fünf Merge Requests ab.&#10;Jeder davon bringt, wie es sich gehört, einen Eintrag im &lt;code&gt;CHANGELOG.md&lt;/code&gt; mit – am besten nach &lt;a href="https://theus.name/post/changelog-fuer-menschen-keep-a-changelog/"&gt;Keep a Changelog&lt;/a&gt;.&#10;Und im Zweifel landet jeder dieser Einträge unter derselben Überschrift.&#10;Sobald der erste Merge Request gemergt ist, stehen die anderen vier vor einem Konflikt.&lt;/p&gt;&#10;&lt;p&gt;Dabei gibt es hier überhaupt nichts zu entscheiden.&#10;Beide Einträge sollen rein und selbst die Reihenfolge hat hier keine Bedeutung.&#10;Trotzdem müssen die Konfliktmarkierungen jedes Mal von Hand entfernt werden.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Wer schon einmal im Team an einem Projekt gearbeitet hat, kennt das Problem:
Hat ein Kollege einen richtig guten Tag, schließt er gleich fünf Merge Requests ab.
Jeder davon bringt, wie es sich gehört, einen Eintrag im <code>CHANGELOG.md</code> mit – am besten nach <a href="https://theus.name/post/changelog-fuer-menschen-keep-a-changelog/">Keep a Changelog</a>.
Und im Zweifel landet jeder dieser Einträge unter derselben Überschrift.
Sobald der erste Merge Request gemergt ist, stehen die anderen vier vor einem Konflikt.</p>
<p>Dabei gibt es hier überhaupt nichts zu entscheiden.
Beide Einträge sollen rein und selbst die Reihenfolge hat hier keine Bedeutung.
Trotzdem müssen die Konfliktmarkierungen jedes Mal von Hand entfernt werden.</p>
<p>Die Lösung ist eine einzige Zeile.</p>
<h2 id="wie-der-konflikt-entsteht">Wie der Konflikt entsteht</h2>
<p>Nach <a href="https://theus.name/post/changelog-fuer-menschen-keep-a-changelog/">Keep a Changelog</a> landen neue Einträge im Abschnitt <code>## [Unreleased]</code>, und zwar meist ganz am Ende einer Gruppe:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl"><span class="gu">## [Unreleased]
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Added
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Dark mode.
</span></span><span class="line"><span class="cl">  Follows the system setting and can be switched manually in the profile.
</span></span></code></pre></div><p>Branch A hängt darunter einen Eintrag zum CSV-Export an, Branch B einen zur Volltextsuche.
Git jedoch vergleicht Zeilenblöcke.
Es sieht, dass beide Seiten an derselben Stelle etwas eingefügt haben, und kann nicht wissen, ob die Reihenfolge eine Rolle spielt oder ob überhaupt nur eine der beiden Zeilen übernommen werden soll.
Also stellt es dir diese Frage:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">- Dark mode.
</span></span><span class="line"><span class="cl">  Follows the system setting and can be switched manually in the profile.
</span></span><span class="line"><span class="cl">&lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD
</span></span><span class="line"><span class="cl">- Export search results as CSV.
</span></span><span class="line"><span class="cl">  The new button downloads the current list, including the active filters.
</span></span><span class="line"><span class="cl">=======
</span></span><span class="line"><span class="cl">- Full-text search.
</span></span><span class="line"><span class="cl">  The search field is available in the header on every page.
</span></span><span class="line"><span class="cl">&gt;&gt;&gt;&gt;&gt;&gt;&gt; feature/search
</span></span></code></pre></div><p>Gelöst wird das, indem du beide Einträge stehen lässt und die Markierungen löschst.
Das ist keine Denkarbeit, sondern reine Routine und die lässt sich zum Glück automatisieren.</p>
<h2 id="der-union-merge-treiber">Der Union-Merge-Treiber</h2>
<p>Git hat für genau diesen Fall einen eingebauten Merge-Treiber namens <code>union</code>.
Statt Konfliktmarkierungen zu schreiben, übernimmt er die Zeilen beider Seiten.
Aktiviert wird er über die Datei <code>.gitattributes</code> im Wurzelverzeichnis des Repositorys:</p>
<pre tabindex="0"><code class="language-gitattributes" data-lang="gitattributes">CHANGELOG.md merge=union
</code></pre><p>Das war&rsquo;s.
Die Datei gehört, wie jede andere, ins Repository, damit sie für das ganze Team gilt.</p>
<p>Ich habe es mit dem Beispiel von oben nachgestellt.
Beide Branches ergänzen einen Eintrag, und der Merge läuft ohne Rückfrage durch:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl"><span class="gu">### Added
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Dark mode.
</span></span><span class="line"><span class="cl">  Follows the system setting and can be switched manually in the profile.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Export search results as CSV.
</span></span><span class="line"><span class="cl">  The new button downloads the current list, including the active filters.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Full-text search.
</span></span><span class="line"><span class="cl">  The search field is available in the header on every page.
</span></span></code></pre></div><p>Keine Markierungen, kein manuelles Aufräumen.
Auch Einträge, die über mehrere Zeilen gehen, bleiben als Block zusammen und werden nicht ineinander verschachtelt.</p>
<p>Ob das Attribut greift, prüfst du mit:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">git check-attr merge CHANGELOG.md
</span></span><span class="line"><span class="cl"><span class="c1"># CHANGELOG.md: merge: union</span>
</span></span></code></pre></div><p>Das Muster <code>CHANGELOG.md</code> ohne Schrägstrich passt übrigens auf jede Datei dieses Namens in jedem Unterverzeichnis.
Das ist praktisch in Monorepos, in dem jedes Projekt sein eigenes Changelog führt.
Brauchst du es nur für eine spzeielle Datei, schreibst du den Pfad aus, zum Beispiel <code>/CHANGELOG.md merge=union</code>.</p>
<p>Der Treiber wirkt nicht nur bei <code>git merge</code>, sondern bei allem, was intern auf einen Dreiwege-Merge zurückgreift, also auch bei <code>git rebase</code>, <code>git cherry-pick</code> und <code>git stash pop</code>.</p>
<h2 id="wo-die-grenzen-liegen">Wo die Grenzen liegen</h2>
<p>Der Union-Treiber ist bewusst simpel.
Er kennt weder Markdown noch die Struktur eines Changelogs, sondern fügt Zeilen zusammen.
Das hat ein paar Folgen, die du kennen solltest.</p>
<h3 id="die-reihenfolge-ist-nicht-garantiert-sinnvoll">Die Reihenfolge ist nicht garantiert sinnvoll</h3>
<p>Bei zwei Einträgen an derselben Stelle steht die Zeile des aktuellen Branches zuerst, die des gemergten danach.
Für ein Changelog ist das in aller Regel egal.
Wenn du die Einträge sortiert haben willst, musst du das selbst tun.</p>
<h3 id="geänderte-zeilen-verdoppeln-sich">Geänderte Zeilen verdoppeln sich</h3>
<p>Der Treiber löst nur das Problem <em>zweier Ergänzungen</em>.
Ändern beide Branches dieselbe vorhandene Zeile unterschiedlich, behält er beide Varianten.
Die Folge ist kein Konflikt, sondern ein doppelter Eintrag.</p>
<p>Passieren könnte das zum Beispiel am Dateiende.
Keep a Changelog sammelt dort die Vergleichslinks:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">[Unreleased]: https://github.com/example/project/compare/v1.2.0...HEAD
</span></span></code></pre></div><p>Wird beim Release des einen Branches diese Zeile angepasst und gleichzeitig vom anderen Branch, stehen danach zwei <code>[Unreleased]:</code>-Zeilen in der Datei.
Das merkst du nur, wenn du hinschaust.</p>
<h3 id="konflikte-fallen-nicht-mehr-auf">Konflikte fallen nicht mehr auf</h3>
<p>Das ist die größte Kehrseite.
Ein Konflikt ist lästig, aber er zwingt dich, die Stelle anzusehen.
Mit <code>merge=union</code> geht der Merge still durch, auch wenn das Ergebnis nicht stimmt.</p>
<p>Vor einem Release solltest du also immer das Changelog gut prüfen.
Auch CI-Jobs können dabei helfen, Fehler im Changelog zu erkennen.</p>
<h2 id="und-andere-dateien">Und andere Dateien?</h2>
<p>Weniger Konflikte klingt verlockend, aber der Union-Treiber ist für die wenigsten anderen Dateien geeignet.
Beide Varianten einer Zeile im Quellcode zu behalten, ist fast immer falsch und das Ergebnis lässt sich nicht mehr bauen oder parsen.</p>
<p>Der Union-Merge taugt für Dateien, die <em>wachsen</em>, bei denen jede Zeile für sich steht und die Reihenfolge nebensächlich ist.
Neben dem Changelog sind das etwa <code>.gitignore</code> Dateien.
Aber ehrlicherweise gibt es da auch selten Konflikte.</p>
<h2 id="fazit">Fazit</h2>
<p>Eine Zeile in der <code>.gitattributes</code> kann dir viel Mühe sparen und deinen Entwicklungsprozess beschleunigen.
Du musst nur bedenken, dass Git ab jetzt nicht mehr nachfragt und gelegentlich prüfen, ob sich doppelte Zeilen eingeschlichen haben.
Dafür bleiben die Merge Requests, in denen der Changelog-Eintrag direkt mitgeliefert wird, von nun an konfliktfrei.</p>
]]></content:encoded></item><item><title>Changelog für Menschen: Keep a Changelog</title><link>https://theus.name/post/changelog-fuer-menschen-keep-a-changelog/</link><pubDate>Fri, 02 Oct 2026 09:00:00 +0200</pubDate><guid>https://theus.name/post/changelog-fuer-menschen-keep-a-changelog/</guid><description>&lt;p&gt;Wer schon einmal wissen wollte, was sich in der neuen Version einer Bibliothek geändert hat, kennt zwei Enttäuschungen.&#10;Die erste: Es gibt gar kein Changelog.&#10;Die zweite: Es gibt eins, aber es hilft nicht weiter.&#10;Der letzte Eintrag ist zwei Jahre alt, mal steht die neueste Version oben und mal unten, Daten fehlen, und hinter „Various bug fixes and improvements“ kann sich alles verbergen.&lt;/p&gt;&#10;&lt;h2 id="wozu-ein-changelog"&gt;Wozu ein Changelog?&lt;/h2&gt;&#10;&lt;p&gt;Ein Changelog ist eine Datei, in der du die Änderungen an deinem Projekt festhältst, geordnet nach Versionen.&#10;Das klingt nach Pflichtübung, aber der Nutzen ist größer als der Aufwand.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Wer schon einmal wissen wollte, was sich in der neuen Version einer Bibliothek geändert hat, kennt zwei Enttäuschungen.
Die erste: Es gibt gar kein Changelog.
Die zweite: Es gibt eins, aber es hilft nicht weiter.
Der letzte Eintrag ist zwei Jahre alt, mal steht die neueste Version oben und mal unten, Daten fehlen, und hinter „Various bug fixes and improvements“ kann sich alles verbergen.</p>
<h2 id="wozu-ein-changelog">Wozu ein Changelog?</h2>
<p>Ein Changelog ist eine Datei, in der du die Änderungen an deinem Projekt festhältst, geordnet nach Versionen.
Das klingt nach Pflichtübung, aber der Nutzen ist größer als der Aufwand.</p>
<h3 id="anwender-können-updates-einschätzen">Anwender können Updates einschätzen</h3>
<p>Bevor jemand eine neue Version einspielt, will er wissen, was ihn erwartet.
Gibt es neue Funktionen?
Wurde ein Fehler behoben, der ihn betrifft?
Bricht etwas, das bisher funktioniert hat?
Ohne Changelog bleibt nur, den Code zu vergleichen oder es auszuprobieren.
Mit Changelog dauert die Antwort eine Minute.</p>
<h3 id="du-selbst-profitierst-auch">Du selbst profitierst auch</h3>
<p>Ein Changelog ist das Gedächtnis des Projekts.
Wann habe ich dieses Verhalten geändert und warum?
Seit welcher Version gibt es die Funktion?
Nach einem halben Jahr weiß das niemand mehr auswendig, auch du als Entwickler:in nicht.
Und wenn es an den Release geht, hast du die Release Notes praktisch schon geschrieben.</p>
<h3 id="das-team-bleibt-auf-demselben-stand">Das Team bleibt auf demselben Stand</h3>
<p>Wer im Team arbeitet, bekommt nicht jede Änderung mit.
Das Changelog zeigt auf einen Blick, was in den letzten Wochen passiert ist, ohne dass jemand durch Merge Requests blättern muss.
Wird er im selben Merge Request gepflegt, der die Änderung einführt, kann das Review gleich auch eine zweite Frage klären:
Ist die Änderung für Anwender verständlich beschrieben?</p>
<h3 id="probleme-lassen-sich-eingrenzen">Probleme lassen sich eingrenzen</h3>
<p>Wenn nach einem Update etwas nicht mehr funktioniert, hilft das Changelog bei der Suche.
Was wurde zwischen der letzten funktionierenden und der aktuellen Version geändert?</p>
<h2 id="warum-nicht-einfach-git-log">Warum nicht einfach <code>git log</code>?</h2>
<p>Der naheliegende Gedanke: Die Historie steht doch schon in Git.
Das stimmt, aber sie beantwortet eine andere Frage.
Die Commit-Historie erzählt, <em>wie der Code entstanden ist</em>.
Ein Changelog beantwortet, <em>was die Änderung für den Leser bedeutet</em>.</p>
<p>Ein Commit wie <code>Refactor user lookup</code> ist für Mitentwickler nützlich, für Anwender aber ohne Aussage.
Dazu kommt viel Rauschen:
Tippfehler-Korrekturen, Zwischenstände und Merge-Commits.
Ein ungefilterter Dump des Git-Logs zählt deshalb kaum als Changelog.</p>
<h2 id="der-nächste-schritt-ein-gemeinsames-format">Der nächste Schritt: ein gemeinsames Format</h2>
<p>Damit wäre die Frage nach dem <em>ob</em> geklärt, aber es bleibt die nach dem <em>wie</em>.
Fängst du ohne Vorgabe an, wirst du schnell Entscheidungen treffen müssen, die nichts mit dem Projekt zu tun haben:
Neueste Version oben oder unten?
Welches Datumsformat?
Wo kommen noch nicht veröffentlichte Änderungen hin?
Wie trennst du neue Funktionen von Fehlerkorrekturen?</p>
<p>Jedes Projekt beantwortet das anders und der Leser muss sich jedes Mal neu zurechtfinden.
Auch ein Team, das das Changelog gemeinsam pflegt, driftet ohne gemeinsame Regeln auseinander.
Der eine schreibt Prosa, der andere Stichpunkte, der dritte vergisst das Datum.
Mit den Jahren habe ich gelernt, wie angenehm es sein kann auf bestehende Konventionen aufzusetzen.</p>
<p>Genau hier setzt <a href="https://keepachangelog.com/">Keep a Changelog</a> an.
Die Konvention nimmt dir diese Entscheidungen ab und ersetzt sie durch ein paar klare Regeln.
Das bringt drei Vorteile:</p>
<ul>
<li><em>Leser finden sich überall zurecht:</em> Wer das Format kennt, weiß in jedem Projekt, wo er suchen muss.</li>
<li><em>Du musst nichts erfinden:</em> Die Struktur ist fertig, du füllst sie nur.</li>
<li><em>Tools kommen damit klar:</em> Weil das Format fest ist, lässt es sich auch maschinell auswerten, etwa für automatische Release Notes.</li>
</ul>
<p>Die Konvention sagt dazu selbst:
Changelogs sind für Menschen gemacht, nicht für Maschinen.</p>
<blockquote >
  <p>Changelogs are for humans, not machines.</p>

  
</blockquote>
<p>Die Maschinenlesbarkeit ist ein Nebeneffekt der klaren Struktur, aber nicht ihr Zweck.</p>
<h2 id="die-grundsätze">Die Grundsätze</h2>
<p>Die Konvention fasst sich in wenigen Regeln zusammen (hier aus dem Englischen übersetzt):</p>
<ul>
<li>Changelogs sind für Menschen gemacht, nicht für Maschinen.</li>
<li>Es gibt einen Eintrag für jede Version.</li>
<li>Gleichartige Änderungen werden gruppiert.</li>
<li>Versionen und Abschnitte sind verlinkbar.</li>
<li>Die neueste Version steht oben.</li>
<li>Das Veröffentlichungsdatum jeder Version steht dabei.</li>
<li>Es ist angegeben, ob das Projekt <a href="https://semver.org/lang/de/">Semantic Versioning</a> folgt.</li>
</ul>
<p>Datumsangaben folgen ISO 8601, also <code>2026-10-02</code> statt <code>02.10.26</code> oder <code>10/02/26</code>.
So ist das Datum eindeutig, egal woher der Leser kommt.</p>
<h2 id="die-sechs-gruppen">Die sechs Gruppen</h2>
<p>Innerhalb einer Version werden die Änderungen nach Art sortiert.
Dafür gibt es genau sechs Überschriften:</p>
<table>
	<thead>
			<tr>
					<th>Gruppe</th>
					<th>Wofür</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>Added</code></td>
					<td>neue Funktionen</td>
			</tr>
			<tr>
					<td><code>Changed</code></td>
					<td>Änderungen an bestehendem Verhalten</td>
			</tr>
			<tr>
					<td><code>Deprecated</code></td>
					<td>Funktionen, die bald entfernt werden</td>
			</tr>
			<tr>
					<td><code>Removed</code></td>
					<td>Funktionen, die jetzt entfernt wurden</td>
			</tr>
			<tr>
					<td><code>Fixed</code></td>
					<td>behobene Fehler</td>
			</tr>
			<tr>
					<td><code>Security</code></td>
					<td>Korrekturen bei Schwachstellen</td>
			</tr>
	</tbody>
</table>
<p>Du verwendest nur die Gruppen, zu denen es Einträge gibt.
Die feste Auswahl hat einen Nutzen:
Wer ein Update plant, sucht zuerst nach <code>Removed</code> und <code>Changed</code>, weil dort die Dinge stehen, die etwas kaputt machen können.
<code>Security</code> ist die Gruppe, bei der Betreiber sofort hellhörig werden sollten.</p>
<h2 id="ein-beispiel">Ein Beispiel</h2>
<p>So sieht das Ganze in der Praxis aus.
Die Einträge sind bewusst ausführlicher als eine Commit-Nachricht und erklären, was sich für den Leser ändert:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl"><span class="gh"># Changelog
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">All notable changes to this project will be documented in this file.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The format is based on [<span class="nt">Keep a Changelog</span>](<span class="na">https://keepachangelog.com/en/1.1.0/</span>),
</span></span><span class="line"><span class="cl">and this project adheres to [<span class="nt">Semantic Versioning</span>](<span class="na">https://semver.org/spec/v2.0.0.html</span>).
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## [Unreleased]
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Added
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Export search results as CSV.
</span></span><span class="line"><span class="cl">  The new &#34;Export&#34; button in the results view downloads the current result list, including the active filters.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## [1.1.0] - 2026-09-15
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">This release adds full-text search.
</span></span><span class="line"><span class="cl">Existing installations need no manual steps, but the first start after the update rebuilds the search index and may take a few minutes on large databases.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Added
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Full-text search across titles and descriptions.
</span></span><span class="line"><span class="cl">  The search field is available in the header on every page.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Changed
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Uploads larger than 10 MB are now rejected with a clear error message instead of timing out after 30 seconds.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Fixed
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Opening an empty file no longer crashes the application.
</span></span><span class="line"><span class="cl">  Empty files are now shown as a blank document.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## [1.0.0] - 2026-08-01
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">First stable release.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Added
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> User accounts with login and password reset by e-mail.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Document upload and management.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">[Unreleased]: https://github.com/example/project/compare/v1.1.0...HEAD
</span></span><span class="line"><span class="cl">[1.1.0]: https://github.com/example/project/compare/v1.0.0...v1.1.0
</span></span><span class="line"><span class="cl">[1.0.0]: https://github.com/example/project/releases/tag/v1.0.0
</span></span></code></pre></div><p>Zwei Teile des Beispiels verdienen einen genaueren Blick.</p>
<h3 id="der-abschnitt-unreleased">Der Abschnitt <code>Unreleased</code></h3>
<p>Ganz oben steht immer ein Abschnitt für alles, was noch nicht veröffentlicht ist.
Entwickler tragen ihre Änderung ein, sobald sie diese machen, und zwar im selben Merge Request.
Das ist viel leichter als bei einem Release rückwirkend zu rekonstruieren, was in den letzten Wochen passiert ist und nichts wird vergessen.
Beim Release wird eine Überschrift mit der Versionsnummer inklusive Datum ergänzt und ein leerer <code>Unreleased</code>-Abschnitt verbleibt darüber.</p>
<h3 id="die-links-am-ende">Die Links am Ende</h3>
<p>Die Versionsnummern in den Überschriften sind Links auf den Vergleich zur vorherigen Version.
In Markdown erreichst du das mit Referenz-Links:
Die Überschrift <code>## [1.1.0] - 2026-09-15</code> verweist auf eine Definition, die du am Dateiende sammelst.</p>
<p>Die Links selbst baust du aus den Git-Tags der Releases.
Die Vergleichsansicht von GitHub und GitLab nimmt zwei Tags und zeigt alle Änderungen dazwischen:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">[1.1.0]: https://github.com/example/project/compare/v1.0.0...v1.1.0
</span></span></code></pre></div><p>Bei GitLab lautet das Muster <code>https://gitlab.com/example/project/-/compare/v1.0.0...v1.1.0</code>.
Die erste Version hat keinen Vorgänger.
Sie verweist deshalb direkt auf den Tag, bei GitHub mit <code>/releases/tag/v1.0.0</code>, bei GitLab mit <code>/-/tags/v1.0.0</code>.
Der Link für <code>[Unreleased]</code> vergleicht den letzten Tag mit <code>HEAD</code>, also mit dem aktuellen Stand des Hauptzweigs.</p>
<p>Beim Release gibt es eine Besonderheit, denn du schreibst den Changelog-Eintrag, bevor du den Tag setzt.
Der Tag soll ja schon das fertige Changelog enthalten.
Der Link auf <code>v1.2.0</code> zeigt in diesem Moment also ins Leere, du <em>rätst</em> ihn gewissermaßen.
Das ist in Ordnung: Sobald du den Tag gepusht hast, funktioniert der Link.
Vergiss dabei nicht, den <code>[Unreleased]</code>-Link auf <code>v1.2.0...HEAD</code> umzustellen.</p>
<p>Wer wissen will, was <em>genau</em> im Code passiert ist, klickt auf den Link und sieht den vollständigen Diff.
Das Changelog liefert die Zusammenfassung, der Vergleich die Details.</p>
<h2 id="zurückgezogene-versionen">Zurückgezogene Versionen</h2>
<p>Manchmal geht eine Veröffentlichung schief, etwa wegen eines schwerwiegenden Fehlers oder einer Sicherheitslücke.
Version 1.1.0 der Spezifikation hat dafür eine Kennzeichnung ergänzt:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl"><span class="gu">## [1.1.1] - 2026-09-20 [YANKED]
</span></span></span></code></pre></div><p>Die Version bleibt in der Historie stehen, ist aber als zurückgezogen markiert.
Löschen wäre die schlechtere Wahl, denn dann wüsste niemand, warum die Versionsnummern eine Lücke haben.</p>
<h2 id="der-partner-semantic-versioning">Der Partner: Semantic Versioning</h2>
<p>Keep a Changelog sagt dir nicht, welche Versionsnummer ein Release bekommt.
Das regelt <a href="https://semver.org/lang/de/">Semantic Versioning</a>, kurz SemVer, mit dem Schema <code>MAJOR.MINOR.PATCH</code>:</p>
<ul>
<li><em>MAJOR</em> erhöhst du bei inkompatiblen Änderungen.</li>
<li><em>MINOR</em> erhöhst du bei neuen Funktionen, die abwärtskompatibel sind.</li>
<li><em>PATCH</em> erhöhst du bei abwärtskompatiblen Fehlerkorrekturen.</li>
</ul>
<p>Beide Konventionen ergänzen sich gut, denn die Gruppen im Changelog verraten, welche Nummer fällig ist.
Steht unter <code>Removed</code> oder in einem <code>Changed</code> mit Breaking Change etwas, wird es eine neue Hauptversion.
Gibt es nur <code>Added</code>, reicht eine neue Nebenversion.
Gibt es nur <code>Fixed</code>, bleibt es bei einem Patch-Release.</p>
<h2 id="schreibtipps">Schreibtipps</h2>
<p>Überleg dir zuerst, <em>wer</em> dein Changelog liest.
Ein Endanwender will wissen, was sich in der Bedienung ändert.
Ein Ops-Admin interessiert sich für neue Konfigurationsoptionen, Migrationsschritte und geänderte Abhängigkeiten.
Ein Security-Spezialist sucht nach behobenen Schwachstellen und deren Schwere.
Jeder dieser Leser braucht andere Details, und davon hängt ab, wie ausführlich und wie technisch du schreibst.
Manchmal heißt das auch, dass mehrere Zielgruppen in einer Datei bedient werden müssen und dann helfen die Gruppen von oben beim Navigieren.</p>
<p>Ein paar weitere Erfahrungen aus der Praxis:</p>
<ul>
<li><em>Schreib aus Sicht des Lesers:</em> Statt <code>Refactor Parser</code> besser <code>Parser erkennt jetzt verschachtelte Listen</code>.</li>
<li><em>Beschreibe die Auswirkung statt der Umsetzung:</em> Ein bis zwei Sätze genügen, bei Bedarf mit Link auf das Issue oder den Merge Request.</li>
<li><em>Erwähne Breaking Changes deutlich</em>, denn genau danach suchen die Leser.</li>
<li><em>Pflege das Changelog beim Ändern</em>, nicht erst beim Release.</li>
</ul>
<h3 id="nicht-jede-änderung-braucht-einen-eintrag">Nicht jede Änderung braucht einen Eintrag</h3>
<p>Ein Changelog enthält nur nennenswerte Änderungen, sonst wird es zum <code>git log</code> mit anderem Namen.
Refactorings ohne Verhaltensänderung, Tests, CI-Konfiguration oder Tippfehler im Code kannst du getrost weglassen.
Frag dich dafür:
Würde ein Leser aus meiner Zielgruppe wegen dieser Änderung etwas anders machen oder anders erwarten?
Wenn nicht, bleibt der Eintrag weg.</p>
<p>Ein zweiter Blick lohnt sich bei Abhängigkeiten und scheinbar kleinen Korrekturen.
Ein Update, das eine Sicherheitslücke schließt, gehört unter <code>Security</code>.
Eine neue Mindestversion von Python oder der Datenbank betrifft Ops-Admins.
Und ein behobener Fehler bleibt einen Eintrag wert, auch wenn die Korrektur nur eine Zeile umfasst.</p>
<p>Im Team hilft es, das Weglassen bewusst zu machen, zum Beispiel mit einem Hinweis in der Vorlage für Merge Requests („Kein Changelog-Eintrag nötig, weil …“).
So sieht ein fehlender Eintrag nicht wie ein Versehen aus.</p>
<h3 id="gitmoji">Gitmoji</h3>
<p>Wer mag, ergänzt jede Zeile um ein <a href="https://identeco.de/gitmoji/">Gitmoji</a>, also ✨ für Neues und 🐛 für Fehlerkorrekturen.
Das ist keine Vorgabe von Keep a Changelog, sondern eine persönliche Vorliebe, die das Überfliegen erleichtert.</p>
<h2 id="fazit">Fazit</h2>
<p>Keep a Changelog ist keine Technik, sondern eine Vereinbarung.
Sie kostet fast nichts, macht Updates planbar und zeigt deinen Anwendern, dass du ihre Zeit respektierst.
Fang mit einer <code>CHANGELOG.md</code> und einem leeren <code>Unreleased</code>-Abschnitt an, der Rest ergibt sich mit der Zeit.</p>
]]></content:encoded></item><item><title>Lucide-Icons in Hugo: Inline-SVG statt Icon-Font</title><link>https://theus.name/post/lucide-icons-in-hugo/</link><pubDate>Tue, 01 Sep 2026 18:30:00 +0200</pubDate><guid>https://theus.name/post/lucide-icons-in-hugo/</guid><description>&lt;p&gt;Vor kurzem habe ich meinen Blog neu gebaut.&#10;Endlich weg von WordPress und hin zu &lt;a href="https://gohugo.io/"&gt;Hugo&lt;/a&gt;.&#10;Hugo nutze ich schon lange für Kundenprojekte und auch für Websites in meinem Hauptjob.&#10;Das passende Template habe ich auf Basis von &lt;a href="https://simplecss.org"&gt;Simple.css&lt;/a&gt; gebaut.&#10;Was noch fehlte: Icons.&lt;/p&gt;&#10;&lt;p&gt;Meine Wahl fiel auf &lt;a href="https://lucide.dev/"&gt;Lucide&lt;/a&gt;, das ich dir schon in meiner &lt;a href="https://theus.name/post/icons-icons-icons/"&gt;Übersicht freier Icon-Sets&lt;/a&gt; vorgestellt habe.&#10;Ich liebe dieses Set, weil es die schlichte Handschrift von Feather fortführt, dabei aber deutlich mehr Auswahl bietet.&#10;Wie es in die Seiten kommt?&#10;Über ein &lt;a href="https://gohugo.io/hugo-modules/use-modules/"&gt;Hugo-Modul&lt;/a&gt; und über zehn Zeilen Template-Code, die verhindern, dass Screenreader dekorative Symbole vorlesen.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Vor kurzem habe ich meinen Blog neu gebaut.
Endlich weg von WordPress und hin zu <a href="https://gohugo.io/">Hugo</a>.
Hugo nutze ich schon lange für Kundenprojekte und auch für Websites in meinem Hauptjob.
Das passende Template habe ich auf Basis von <a href="https://simplecss.org">Simple.css</a> gebaut.
Was noch fehlte: Icons.</p>
<p>Meine Wahl fiel auf <a href="https://lucide.dev/">Lucide</a>, das ich dir schon in meiner <a href="https://theus.name/post/icons-icons-icons/">Übersicht freier Icon-Sets</a> vorgestellt habe.
Ich liebe dieses Set, weil es die schlichte Handschrift von Feather fortführt, dabei aber deutlich mehr Auswahl bietet.
Wie es in die Seiten kommt?
Über ein <a href="https://gohugo.io/hugo-modules/use-modules/">Hugo-Modul</a> und über zehn Zeilen Template-Code, die verhindern, dass Screenreader dekorative Symbole vorlesen.</p>
<h2 id="was-mir-wichtig-war">Was mir wichtig war</h2>
<p>Icons lassen sich auf viele Arten einbinden: als Font, als Sprite oder per JavaScript-Bibliothek – jeweils von einem CDN oder aus dem eigenen Projekt.
Bevor ich mich entschieden habe, habe ich deshalb aufgeschrieben, was mir dabei überhaupt wichtig ist.</p>
<h3 id="keine-zusätzliche-build-chain">Keine zusätzliche Build-Chain</h3>
<p>Für ein paar Symbole ein npm-Projekt, einen Bundler und ein <code>node_modules/</code> ins Repo zu holen, steht in keinem Verhältnis.
Webentwicklung darf und soll unkompliziert bleiben.
Dieser (mein persönlicher) Blog kommt bis heute ohne <code>package.json</code> aus und das darf gern auch so bleiben.</p>
<h3 id="kein-javascript">Kein JavaScript</h3>
<p>Es gibt reichlich Bibliotheken, die ihre Symbole erst im Browser ins HTML setzen.
JavaScript als Zugabe ist eine feine Sache, aber um ein Icon neben einen Link zu stellen, brauche ich es nicht.
Eine statische Seite baut ihr HTML einmal fertig und liefert es dann aus.
Symbole, die erst im Browser in die Seite eingefügt werden, widersprechen genau diesem Ansatz.</p>
<h3 id="schnelle-ladezeiten">Schnelle Ladezeiten</h3>
<p>Jede Datei, die eine Seite zusätzlich anfordert, kostet Zeit.
Eine Icon-Font zum Beispiel ist ein weiterer Download, und bis er da ist, stehen an den Stellen der Symbole gerne mal leere Kästchen.
Kurze Ladezeiten belohnen die Besucher mit einer besseren Erfahrung und die Suchmaschinen mit einer besseren Bewertung.</p>
<h3 id="datenschutz">Datenschutz</h3>
<p>Diese Seite lädt keine Ressourcen von fremden Servern – auch die Schriftarten liegen deshalb selbst gehostet im Projekt.
Ein CDN würde die IP-Adresse jedes Besuchers an einen Dritten weitergeben, ganz gleich, ob es eine Font, ein Skript oder ein einzelnes SVG ausliefert.
Ein hoher Preis für ein paar Kilobyte SVG.</p>
<p>Alle vier Punkte führen zur selben Antwort: Das Icon steht als SVG direkt im HTML, hineingeschrieben von Hugo beim Bauen der Seite.
Kein zusätzlicher Request, kein Skript und nichts, was nachlädt.
Im ausgelieferten HTML landen wirklich nur die Symbole, die auf der Seite auch vorkommen.</p>
<h2 id="das-modul">Das Modul</h2>
<p>Lucide gibt es glücklicherweise als fertiges <a href="https://icons.hugomods.com/">Hugo-Modul</a>.
Eingebunden wird es in <code>config/_default/hugo.toml</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">module</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">[[</span><span class="nx">module</span><span class="p">.</span><span class="nx">imports</span><span class="p">]]</span>
</span></span><span class="line"><span class="cl">    <span class="nx">path</span> <span class="p">=</span> <span class="s1">&#39;github.com/hugomods/icons/vendors/lucide&#39;</span>
</span></span></code></pre></div><p>Danach muss man einmal das Modul laden:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">hugo mod get github.com/hugomods/icons/vendors/lucide
</span></span></code></pre></div><p>Das legt <code>go.mod</code> und <code>go.sum</code> an.
Dafür muss die Go-Toolchain installiert sein, Node braucht es aber nicht.
Das Modul bringt die einzelnen SVG-Dateien des Sets mit.
Diese werden gemountet nach <code>assets/icons/lucide/</code>.</p>
<p>Ab jetzt stehen dir zwei Wege offen, ein Icon zu setzen.
Im Text, also in einer Markdown-Datei, nimmst du den Shortcode <code>ico</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">Ein Stern {{&lt; ico lucide star &gt;}}, der deinen Namen trägt…
</span></span></code></pre></div><p>Der erste Parameter ist das Set, der zweite der Name des Icons, so wie er in der <a href="https://lucide.dev/icons/">Lucide-Suche</a> steht.
Optional kommt als dritter Parameter eine Größe dazu, etwa <code>2rem</code>.
Wer es genauer mag, nutzt die Variante mit benannten Parametern, die zusätzlich <code>color</code>, <code>width</code>, <code>height</code> und <code>className</code> kennt:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">{{&lt; icons/icon vendor=&#34;lucide&#34; name=&#34;star&#34; size=&#34;2rem&#34; &gt;}}
</span></span></code></pre></div><p>In den Templates rufst du stattdessen das Partial des Moduls auf:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go-html-template" data-lang="go-html-template"><span class="line"><span class="cl"><span class="cp">{{</span><span class="w"> </span><span class="nx">partial</span><span class="w"> </span><span class="s">&#34;icons/icon&#34;</span><span class="w"> </span><span class="o">(</span><span class="nx">dict</span><span class="w"> </span><span class="s">&#34;vendor&#34;</span><span class="w"> </span><span class="s">&#34;lucide&#34;</span><span class="w"> </span><span class="s">&#34;name&#34;</span><span class="w"> </span><span class="s">&#34;star&#34;</span><span class="o">)</span><span class="w"> </span><span class="cp">}}</span>
</span></span></code></pre></div><p>Egal welchen Weg du nimmst – im fertigen HTML landet dasselbe eingebettete SVG:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">svg</span>
</span></span><span class="line"><span class="cl">  <span class="na">aria-hidden</span><span class="o">=</span><span class="s">&#34;true&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="na">class</span><span class="o">=</span><span class="s">&#34;hi-svg-inline&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="na">fill</span><span class="o">=</span><span class="s">&#34;none&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="na">height</span><span class="o">=</span><span class="s">&#34;1em&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="na">width</span><span class="o">=</span><span class="s">&#34;1em&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="na">stroke</span><span class="o">=</span><span class="s">&#34;currentColor&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="na">stroke-width</span><span class="o">=</span><span class="s">&#34;2&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="na">stroke-linecap</span><span class="o">=</span><span class="s">&#34;round&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="na">stroke-linejoin</span><span class="o">=</span><span class="s">&#34;round&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="na">viewBox</span><span class="o">=</span><span class="s">&#34;0 0 24 24&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="na">xmlns</span><span class="o">=</span><span class="s">&#34;http://www.w3.org/2000/svg&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">path</span> <span class="na">d</span><span class="o">=</span><span class="s">&#34;M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 …&#34;</span> <span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">svg</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>Drei Details daran sind wichtig.
<code>currentColor</code> bedeutet, dass ein Icon automatisch die Farbe des umgebenden Textes annimmt.
Im Dark Mode dieser Seite muss dafür also nichts weiter angepasst werden.
<code>1em</code> bedeutet, dass es so hoch wie der Text ist und mit diesem mitwächst, wenn jemand die Schriftgröße hochdreht.
Und <code>aria-hidden=&quot;true&quot;</code> setzt das Modul immer, was für die meisten Icons genau richtig ist, aber eben nicht für alle.
Dazu gleich mehr.</p>
<p>Aktualisiert wird das Set wie jede andere Abhängigkeit (siehe <a href="https://gohugo.io/hugo-modules/use-modules/#update">Docs</a>):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">hugo mod get -u ./...
</span></span></code></pre></div><h2 id="meine-zwischenschicht">Meine Zwischenschicht</h2>
<p>Aufgerufen wird das Modul-Partial in meinem Template aber nie direkt.
Dazwischen liegt <code>layouts/_partials/icon.html</code>, und das ist der eigentlich interessante Teil:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go-html-template" data-lang="go-html-template"><span class="line"><span class="cl"><span class="cp">{{</span><span class="w"> </span><span class="k">with</span><span class="w"> </span><span class="na">.title</span><span class="w"> </span><span class="cp">}}</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">span</span> <span class="na">title</span><span class="o">=</span><span class="s">&#34;</span><span class="cp">{{</span><span class="w"> </span><span class="na">.</span><span class="w"> </span><span class="cp">}}</span><span class="s">&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="cp">{{</span><span class="w"> </span><span class="k">end</span><span class="w"> </span><span class="cp">}}</span>
</span></span><span class="line"><span class="cl"><span class="cp">{{-</span><span class="w"> </span><span class="nx">partial</span><span class="w"> </span><span class="s">&#34;icons/icon&#34;</span><span class="w"> </span><span class="o">(</span><span class="nx">dict</span><span class="w"> </span><span class="s">&#34;vendor&#34;</span><span class="w"> </span><span class="s">&#34;lucide&#34;</span><span class="w"> </span><span class="s">&#34;name&#34;</span><span class="w"> </span><span class="o">(</span><span class="na">.name</span><span class="o">))</span><span class="w"> </span><span class="cp">-}}</span>
</span></span><span class="line"><span class="cl"><span class="cp">{{</span><span class="w"> </span><span class="k">with</span><span class="w"> </span><span class="na">.title</span><span class="w"> </span><span class="cp">}}</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;sr-only&#34;</span><span class="p">&gt;</span><span class="cp">{{-</span><span class="w"> </span><span class="na">.</span><span class="w"> </span><span class="cp">-}}</span>:<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="cp">{{</span><span class="w"> </span><span class="k">end</span><span class="w"> </span><span class="cp">}}</span>
</span></span></code></pre></div><p>Das hat zwei Gründe.</p>
<p>Der erste ist Bequemlichkeit: Der Vendor-Name steht genau einmal im Projekt.
Denn ich will ja nicht verschiedene Icon-Sets in meinem Blog benutzen.
Ein Aufruf sieht damit so aus:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go-html-template" data-lang="go-html-template"><span class="line"><span class="cl"><span class="cp">{{</span><span class="w"> </span><span class="nx">partial</span><span class="w"> </span><span class="s">&#34;icon&#34;</span><span class="w"> </span><span class="o">(</span><span class="nx">dict</span><span class="w"> </span><span class="s">&#34;name&#34;</span><span class="w"> </span><span class="s">&#34;rss&#34;</span><span class="o">)</span><span class="w"> </span><span class="cp">}}</span>
</span></span></code></pre></div><p>Der zweite Grund ist der wichtigere: Barrierefreiheit, der ich bei meinem Blog besondere Aufmerksamkeit gewidmet habe.
Icons treten hier nämlich in zwei völlig verschiedenen Rollen auf.</p>
<p><em>Dekorativ</em>, wenn direkt daneben schon steht, was gemeint ist.
In der Fußzeile bekommt jede Kategorie ein Symbol: <code>code</code> für Softwareentwicklung, <code>shield</code> für IT-Sicherheit, <code>wrench</code> für Tooling.
Der Linktext nennt die Kategorie jedoch ohnehin beim Namen.
Ein Screenreader soll hier gar nichts vorlesen.
Genau das passiert, wenn <code>title</code> fehlt: Übrig bleibt ein nacktes SVG mit <code>aria-hidden=&quot;true&quot;</code>.</p>
<p><em>Semantisch</em>, wenn das Icon die Beschriftung <em>ersetzt</em>.
In der Fußzeile einer Beitragsvorschau steht ein aufgeschlagenes Buch und daneben „5 min“.
Wer das Symbol sieht, versteht sofort: Lesezeit.
Wer es nicht sieht, weil ein Screenreader vorliest, hört nur „5 min“ und weiß nicht, worum es geht.</p>
<p>Für diesen Fall nimmt das Partial ein <code>title</code> entgegen und macht daraus beides: ein <code>title</code>-Attribut für den Tooltip mit der Maus und einen visuell versteckten Text davor.
Die Karte ist eine Definitionsliste, und das Ergebnis liest sich vorgelesen als „Lesezeit: 5 min“, während auf dem Bildschirm nur das Icon steht.
Ein <code>alt</code>-Text am SVG würde das nicht leisten, denn das <code>aria-hidden</code> des Moduls bliebe ja bestehen.</p>
<h2 id="eine-stolperfalle-zum-schluss">Eine Stolperfalle zum Schluss</h2>
<p>Das Modul-Partial gibt seinen SVG-String mit einem abschließenden Zeilenumbruch zurück.
Steht das Icon direkt vor Text, rendert der Browser diesen Umbruch als Leerzeichen zusätzlich zu dem Abstand, den das Stylesheet ohnehin setzt.
Die Trim-Marker von Go-Templates (<code>{{-</code> und <code>-}}</code>) helfen hier nicht, denn sie greifen auf den Template-Text drumherum, jedoch nicht auf den Rückgabewert eines Partials.</p>
<p>Das habe ich neu gelernt:
Der Umweg dafür ist <code>chomp</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go-html-template" data-lang="go-html-template"><span class="line"><span class="cl"><span class="cp">{{</span><span class="w"> </span><span class="nx">partial</span><span class="w"> </span><span class="s">&#34;icon&#34;</span><span class="w"> </span><span class="o">(</span><span class="nx">dict</span><span class="w"> </span><span class="s">&#34;name&#34;</span><span class="w"> </span><span class="s">&#34;arrow-left&#34;</span><span class="o">)</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nx">chomp</span><span class="w"> </span><span class="cp">}}</span>
</span></span></code></pre></div><h2 id="fazit">Fazit</h2>
<p>Viel ist es am Ende nicht: ein Modul in der Konfiguration, ein Partial mit zehn Zeilen und ein <code>chomp</code> an der richtigen Stelle.
Dafür kommt der Blog ohne Node, ohne JavaScript und ohne Ressourcen von fremden Servern aus.
Und alle Icons sind da, sobald die Seite geladen hat.
Falls du auf den Geschmack gekommen bist: Das <a href="https://icons.hugomods.com/">Hugo Icons Module</a> bietet übrigens noch weitere Icon-Sets.</p>
]]></content:encoded></item><item><title>Fährräder zählen – seit 7 Jahren</title><link>https://theus.name/post/fahrraeder-zaehlen-seit-7-jahren/</link><pubDate>Wed, 08 Jul 2026 13:42:00 +0000</pubDate><guid>https://theus.name/post/fahrraeder-zaehlen-seit-7-jahren/</guid><description>&lt;p&gt;Seit sieben Jahren &lt;a href="https://theus.name/post/fahrraeder-zaehlen/"&gt;zähle ich Fahrräder&lt;/a&gt;.&#10;Das erledige ich natürlich nicht von Hand, sondern das machen 15 Zählstellen im Bonner Stadtgebiet verteilt.&#10;Und die Werte der Zählstellen ruft ein Python-Skript für mich ab.&#10;Bisher über eine REST-API, die ich reverse-engineered habe.&#10;Seit zwei Tagen kommen aber keine Antworten mehr, eine neue Lösung muss her.&lt;/p&gt;&#10;&lt;p&gt;Die Daten aller Fahrradzählstellen kann man über die &lt;a href="https://opendata.bonn.de/dataset/cfc2a3cf-e585-472a-b70d-679aa3070f41/resource/053f5572-7f88-4156-b456-21c573750d32"&gt;Open-Data-Plattform der Stadt Bonn&lt;/a&gt; als CSV abrufen.&#10;Allerdings sind das wirklich nur die Daten des Vortags und ich benötige auch die Daten der Vorwoche – und manchmal mache ich auch Analysen über größere Zeiträume hinweg.&#10;Daten doppelt zu halten (und das als CSV) ist mir zu umständlich.&#10;Daher gehe ich den direkten Weg über die API des Herstellers, doch die alte REST-API ist seit zwei Tagen offline und &lt;a href="https://bonn.social/@bonnbike/116876793108102564"&gt;mein Mastodon-Account liefert keine Daten&lt;/a&gt; mehr.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Seit sieben Jahren <a href="https://theus.name/post/fahrraeder-zaehlen/">zähle ich Fahrräder</a>.
Das erledige ich natürlich nicht von Hand, sondern das machen 15 Zählstellen im Bonner Stadtgebiet verteilt.
Und die Werte der Zählstellen ruft ein Python-Skript für mich ab.
Bisher über eine REST-API, die ich reverse-engineered habe.
Seit zwei Tagen kommen aber keine Antworten mehr, eine neue Lösung muss her.</p>
<p>Die Daten aller Fahrradzählstellen kann man über die <a href="https://opendata.bonn.de/dataset/cfc2a3cf-e585-472a-b70d-679aa3070f41/resource/053f5572-7f88-4156-b456-21c573750d32">Open-Data-Plattform der Stadt Bonn</a> als CSV abrufen.
Allerdings sind das wirklich nur die Daten des Vortags und ich benötige auch die Daten der Vorwoche – und manchmal mache ich auch Analysen über größere Zeiträume hinweg.
Daten doppelt zu halten (und das als CSV) ist mir zu umständlich.
Daher gehe ich den direkten Weg über die API des Herstellers, doch die alte REST-API ist seit zwei Tagen offline und <a href="https://bonn.social/@bonnbike/116876793108102564">mein Mastodon-Account liefert keine Daten</a> mehr.</p>
<p>Um herauszufinden, wie ich jetzt noch an die Daten herankomme, öffne ich die <a href="https://stadtbonn.eco-counter.com/">Zählstellen-Website des Herstellers für die Stadt Bonn</a> in meinem Browser.
Wenn man sich den Datenverkehr der Website in seinem Browser genauer anschaut, fällt einem schnell auf, dass es sich um eine <a href="https://nextjs.org/">Next.js</a>-Applikation handeln muss.
Man kann dies an <code>TURBOPACK</code> in den Daten der übertragenen JavaScript-Dateien erkennen oder aber auch an dem Präfix der CSS-Variablen <code>next-</code>.</p>
<p>Diese Website stellt jedoch keine JSON-API mehr bereit; stattdessen sind die Tageszahlen in den React Server Components (RSC) der Seite eingebettet.
Ich zeige euch hier ein Beispiel, wie man diesen Stream abfragt und daraus die aggregierten Tageswerte, die ich für meine Toots benötige, extrahiert.
Ich habe den Code extra etwas vereinfacht. Bitte achtet auf die Validierung der Input-Daten.</p>
<p>Die Abfrage der Daten geschieht in drei Schritten:</p>
<ol>
<li>den RSC-Stream abrufen</li>
<li>den gewünschten JSON-Wert aus dem Stream extrahieren</li>
<li>das extrahierte JSON in eine Liste der täglichen Zahlen umwandeln</li>
</ol>
<p>Das komplette Skript findet ihr ganz unten. Ich gehe hier nur die wichtigsten Teile durch.</p>
<h2 id="rsc-stream-abrufen">RSC-Stream abrufen</h2>
<p>Ich selbst habe noch nie eine Website mit Next.js gebaut. Das ist also auch neu für mich. Schreibt mir gern in die Kommentare, wenn etwas nicht ganz korrekt erklärt ist. Fakt ist, die Next.js-Anwendung verwendet RSCs. Eine URL wie <code>/site/100021143?…</code> kann je nach Art der Anfrage zwei völlig unterschiedliche Ergebnisse liefern:</p>
<ul>
<li>Initiale Browseranfrage: Der Server gibt eine vollständige HTML-Seite zurück. React läuft im Browser und die eigentlichen Zähldaten werden nach dem Laden via JavaScript abgerufen und im DOM gerendert.</li>
<li>Anfrage mit gesetztem HTTP-Header <code>RSC: 1</code>: Next.js gibt stattdessen den RSC-Payload zurück: einen Stream, der den DOM und die Daten beschreibt. Das ist, was der Browser selbst anfordert, wenn du innerhalb der App navigierst – React ruft die neue Seite als RSC-Daten ab, anstatt das vollständige HTML erneut zu laden.</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">fetch_rsc_payload</span><span class="p">(</span><span class="n">counter_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">year</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">month</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Return the raw RSC stream text for one site and month.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">url</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">SITE_BASE</span><span class="si">}</span><span class="s2">/site/</span><span class="si">{</span><span class="n">counter_id</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">params</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;year&#34;</span><span class="p">:</span> <span class="n">year</span><span class="p">,</span> <span class="s2">&#34;month&#34;</span><span class="p">:</span> <span class="n">month</span><span class="p">,</span> <span class="s2">&#34;granularity&#34;</span><span class="p">:</span> <span class="s2">&#34;P1D&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">headers</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;RSC&#34;</span><span class="p">:</span> <span class="s2">&#34;1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Accept&#34;</span><span class="p">:</span> <span class="s2">&#34;text/x-component&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;User-Agent&#34;</span><span class="p">:</span> <span class="s2">&#34;Mozilla/5.0&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Referer&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">SITE_BASE</span><span class="si">}</span><span class="s2">/site/</span><span class="si">{</span><span class="n">counter_id</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">resp</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">params</span><span class="o">=</span><span class="n">params</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="n">headers</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">30</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">resp</span><span class="o">.</span><span class="n">raise_for_status</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">resp</span><span class="o">.</span><span class="n">text</span>
</span></span></code></pre></div><p>Was wir von dieser Funktion erhalten, ist etwa folgender Payload (hier deutlich gekürzt):</p>
<pre tabindex="0"><code>1:&#34;$Sreact.fragment&#34;
3:I[97367,[&#34;/_next/static/chunks/0s2yzmf9j.2c6.js&#34;, ...]]
4:&#34;$Sreact.suspense&#34;
...
</code></pre><p>Wie du schnell erkennst, ist das nicht einfach JSON. Es ist Next.js&rsquo; zeilenbasiertes RSC Wire Format (hier ein <a href="https://sachinsharma.dev/blogs/rsc-wire-protocol-custom-parser-2026">Blogartikel von Sachin</a> zum Deep Dive). Jede Zeile ist durchnummeriert. Aber – und das ist das Wichtige – die eigentlichen Daten, die uns interessieren, sind als gewöhnliches JSON-Array gespeichert und befinden sich direkt in einer dieser Zeilen:</p>
<pre tabindex="0"><code>22:[&#34;$&#34;,&#34;$L25&#34;,null,{&#34;granularity&#34;:&#34;P1D&#34;,&#34;site&#34;:&#34;$2:2:props:...&#34;,&#34;chartData&#34;:[{&#34;travelMode&#34;:&#34;bike&#34;,&#34;data&#34;:[{&#34;timestamp&#34;:&#34;2026-07-01T00:00:00+02:00&#34;,&#34;traffic&#34;:{&#34;counts&#34;:19014}}, ... ]}], &#34;directionGraphData&#34;:[...], ...}]
</code></pre><h2 id="json-aus-dem-stream-extrahieren">JSON aus dem Stream extrahieren</h2>
<p>In den erhaltenen Daten suchen wir jetzt nach <code>chartData</code> oder <code>graphData</code> (ich hab je nach Monat der Daten diese in dem einen oder in dem anderen Key gefunden, eventuell hat der Betreiber einmal sein Schema geändert?). Wir suchen also nach diesen beiden Schlüsseln und merken uns die Zeile.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">SITE_BASE</span> <span class="o">=</span> <span class="s2">&#34;https://stadtbonn.eco-counter.com&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">_extract_keyed_value</span><span class="p">(</span><span class="n">body</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">key</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">object</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Find `&#34;key&#34;:` in the stream and json.loads the line.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">needle</span> <span class="o">=</span> <span class="sa">f</span><span class="s1">&#39;&#34;</span><span class="si">{</span><span class="n">key</span><span class="si">}</span><span class="s1">&#34;:&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="n">body</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">needle</span> <span class="ow">in</span> <span class="n">line</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">_extract_json</span><span class="p">(</span><span class="n">line</span><span class="p">))[</span><span class="mi">3</span><span class="p">][</span><span class="n">key</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">            <span class="k">except</span> <span class="p">(</span><span class="ne">IndexError</span><span class="p">,</span> <span class="ne">KeyError</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">                <span class="k">continue</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">raise</span> <span class="n">RSCParseError</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;Key &#34;</span><span class="si">{</span><span class="n">key</span><span class="si">}</span><span class="s1">&#34; not found&#39;</span><span class="p">)</span>
</span></span></code></pre></div><p>Mit dieser Zeile rufen wir dann eine weitere Funktion auf, welche das JSON aus der Zeile herauslöst (es ist ja durch die Zeilennummer gepräfixt, wir trennen daher am ersten Doppelpunkt).</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">_extract_json</span><span class="p">(</span><span class="n">line</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Return the json from a line of the RSCP.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">json_data</span> <span class="o">=</span> <span class="n">line</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;:&#34;</span><span class="p">,</span> <span class="mi">1</span><span class="p">)[</span><span class="mi">1</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">json_data</span>
</span></span><span class="line"><span class="cl">    <span class="k">except</span> <span class="ne">IndexError</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="n">RSCParseError</span><span class="p">(</span><span class="s2">&#34;Unexpected format of RSC payload.&#34;</span><span class="p">)</span>
</span></span></code></pre></div><p>Aus dem erhaltenen JSON lassen wir uns jetzt in <code>_extract_keyed_value</code> das vierte Element der Liste ausgeben und aus diesem Objekt dann den Value für unseren gesuchten Key. Warum? Naja, das habe ich halt einfach so aus den erhaltenen Daten abgelesen. Und ob es morgen noch funktioniert, sehen wir dann morgen.</p>
<p><strong>Achtung:</strong> Das ist ein sehr fragiler Teil des Codes und überhaupt ist das Parsen von Daten Dritter eine sehr gefährliche Angelegenheit. Dieser Beispielcode ist auch nicht mein Produktivcode! Mit Libraries wie <a href="https://github.com/pydantic/pydantic/">Pydantic</a> könnt ihr euren Code auf wesentlich sichere Beine stellen. Hier aber möchte ich euch den grundsätzlichen Ansatz zeigen.</p>
<h2 id="liste-der-täglichen-zahlen">Liste der täglichen Zahlen</h2>
<p>Jetzt bauen wir uns eine <a href="https://docs.python.org/3/library/dataclasses.html">Dataclass</a>, welche die täglichen Daten halten soll.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="nd">@dataclass</span><span class="p">(</span><span class="n">frozen</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">DayCount</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;One day&#39;s total count. `count` is None when no data exists for that day.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">date</span><span class="p">:</span> <span class="n">dt</span><span class="o">.</span><span class="n">date</span>
</span></span><span class="line"><span class="cl">    <span class="n">count</span><span class="p">:</span> <span class="nb">int</span> <span class="o">|</span> <span class="kc">None</span>
</span></span></code></pre></div><p>Jetzt bauen wir uns eine Funktion, um aus der Liste von Tageswerten eine Liste von <code>DayCount</code>-Objekten zu erhalten.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">DATA_KEYS</span> <span class="o">=</span> <span class="p">(</span><span class="s2">&#34;chartData&#34;</span><span class="p">,</span> <span class="s2">&#34;graphData&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">parse_daily_counts</span><span class="p">(</span><span class="n">body</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="n">DayCount</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Extract the aggregate daily counts from an RSC payload.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">series</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">key</span> <span class="ow">in</span> <span class="n">DATA_KEYS</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">series</span> <span class="o">=</span> <span class="n">_extract_keyed_value</span><span class="p">(</span><span class="n">body</span><span class="p">,</span> <span class="n">key</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">break</span>
</span></span><span class="line"><span class="cl">        <span class="k">except</span> <span class="n">RSCParseError</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">continue</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">series</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="n">RSCParseError</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="sa">f</span><span class="s2">&#34;none of </span><span class="si">{</span><span class="n">DATA_KEYS</span><span class="si">}</span><span class="s2"> found; the page structure may have changed &#34;</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;or the RSC header is no longer honored&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># series looks like: [{&#34;travelMode&#34;: &#34;bike&#34;, &#34;data&#34;: [ {timestamp, traffic}, ... ]}]</span>
</span></span><span class="line"><span class="cl">    <span class="n">rows</span> <span class="o">=</span> <span class="n">series</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;data&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">counts</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="n">DayCount</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">date</span><span class="o">=</span><span class="n">dt</span><span class="o">.</span><span class="n">date</span><span class="o">.</span><span class="n">fromisoformat</span><span class="p">(</span><span class="n">row</span><span class="p">[</span><span class="s2">&#34;timestamp&#34;</span><span class="p">][:</span><span class="mi">10</span><span class="p">]),</span>
</span></span><span class="line"><span class="cl">            <span class="n">count</span><span class="o">=</span><span class="n">row</span><span class="p">[</span><span class="s2">&#34;traffic&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;counts&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">rows</span>
</span></span><span class="line"><span class="cl">    <span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">counts</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">d</span><span class="p">:</span> <span class="n">d</span><span class="o">.</span><span class="n">date</span><span class="p">)</span>
</span></span></code></pre></div><p><strong>Achtung:</strong> Auch dieser Beispielcode ist nicht reif für Produktiv!</p>
<p>In dem kompletten Beispielcode, der jetzt folgt, könnt ihr dann auch sehen – und gerne auch ausprobieren – wie alles zusammengestöpselt wird. Dazu erstellt ihr euch am besten ein <a href="https://docs.python.org/3/library/venv.html">virtuelles Environment</a> und installiert euch requests mittels <code>pip install requests</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">datetime</span> <span class="k">as</span> <span class="nn">dt</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">json</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">dataclasses</span> <span class="kn">import</span> <span class="n">dataclass</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">requests</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">SITE_BASE</span> <span class="o">=</span> <span class="s2">&#34;https://stadtbonn.eco-counter.com&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">DATA_KEYS</span> <span class="o">=</span> <span class="p">(</span><span class="s2">&#34;chartData&#34;</span><span class="p">,</span> <span class="s2">&#34;graphData&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@dataclass</span><span class="p">(</span><span class="n">frozen</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">DayCount</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;One day&#39;s total count. `count` is None when no data exists for that day.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">date</span><span class="p">:</span> <span class="n">dt</span><span class="o">.</span><span class="n">date</span>
</span></span><span class="line"><span class="cl">    <span class="n">count</span><span class="p">:</span> <span class="nb">int</span> <span class="o">|</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">RSCParseError</span><span class="p">(</span><span class="ne">ValueError</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Raised when the expected count data can&#39;t be located or parsed.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># --------------------------------------------------------------------------- #</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Step 1: fetch the RSC stream</span>
</span></span><span class="line"><span class="cl"><span class="c1"># --------------------------------------------------------------------------- #</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">fetch_rsc_payload</span><span class="p">(</span><span class="n">counter_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">year</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">month</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Return the raw RSC stream text for one site and month.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">url</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">SITE_BASE</span><span class="si">}</span><span class="s2">/site/</span><span class="si">{</span><span class="n">counter_id</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">params</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;year&#34;</span><span class="p">:</span> <span class="n">year</span><span class="p">,</span> <span class="s2">&#34;month&#34;</span><span class="p">:</span> <span class="n">month</span><span class="p">,</span> <span class="s2">&#34;granularity&#34;</span><span class="p">:</span> <span class="s2">&#34;P1D&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">headers</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;RSC&#34;</span><span class="p">:</span> <span class="s2">&#34;1&#34;</span><span class="p">,</span>  <span class="c1"># &lt;- the key that makes Next.js return the data stream</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Accept&#34;</span><span class="p">:</span> <span class="s2">&#34;text/x-component&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;User-Agent&#34;</span><span class="p">:</span> <span class="s2">&#34;Mozilla/5.0&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Referer&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">SITE_BASE</span><span class="si">}</span><span class="s2">/site/</span><span class="si">{</span><span class="n">counter_id</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">resp</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">params</span><span class="o">=</span><span class="n">params</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="n">headers</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">30</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">resp</span><span class="o">.</span><span class="n">raise_for_status</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">resp</span><span class="o">.</span><span class="n">text</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># --------------------------------------------------------------------------- #</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Step 2: extract exactly the JSON value we want from the stream</span>
</span></span><span class="line"><span class="cl"><span class="c1"># --------------------------------------------------------------------------- #</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">_extract_json</span><span class="p">(</span><span class="n">line</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Return the json from a line of the RSCP.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">json_data</span> <span class="o">=</span> <span class="n">line</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;:&#34;</span><span class="p">,</span> <span class="mi">1</span><span class="p">)[</span><span class="mi">1</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">json_data</span>
</span></span><span class="line"><span class="cl">    <span class="k">except</span> <span class="ne">IndexError</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="n">RSCParseError</span><span class="p">(</span><span class="s2">&#34;Unexpected format of RSC payload.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">_extract_keyed_value</span><span class="p">(</span><span class="n">body</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">key</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">object</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Find `&#34;key&#34;:` in the stream and json.loads the line.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">needle</span> <span class="o">=</span> <span class="sa">f</span><span class="s1">&#39;&#34;</span><span class="si">{</span><span class="n">key</span><span class="si">}</span><span class="s1">&#34;:&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="n">body</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">needle</span> <span class="ow">in</span> <span class="n">line</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">_extract_json</span><span class="p">(</span><span class="n">line</span><span class="p">))[</span><span class="mi">3</span><span class="p">][</span><span class="n">key</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">            <span class="k">except</span> <span class="p">(</span><span class="ne">IndexError</span><span class="p">,</span> <span class="ne">KeyError</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">                <span class="k">continue</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">raise</span> <span class="n">RSCParseError</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;Key &#34;</span><span class="si">{</span><span class="n">key</span><span class="si">}</span><span class="s1">&#34; not found&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># --------------------------------------------------------------------------- #</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Step 3: turn the extracted JSON into a clean list of daily counts</span>
</span></span><span class="line"><span class="cl"><span class="c1"># --------------------------------------------------------------------------- #</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">parse_daily_counts</span><span class="p">(</span><span class="n">body</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="n">DayCount</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Extract the aggregate daily counts from an RSC payload.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">series</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">key</span> <span class="ow">in</span> <span class="n">DATA_KEYS</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">series</span> <span class="o">=</span> <span class="n">_extract_keyed_value</span><span class="p">(</span><span class="n">body</span><span class="p">,</span> <span class="n">key</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">break</span>
</span></span><span class="line"><span class="cl">        <span class="k">except</span> <span class="n">RSCParseError</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">continue</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">series</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="n">RSCParseError</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="sa">f</span><span class="s2">&#34;none of </span><span class="si">{</span><span class="n">DATA_KEYS</span><span class="si">}</span><span class="s2"> found; the page structure may have changed &#34;</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;or the RSC header is no longer honored&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># series looks like: [{&#34;travelMode&#34;: &#34;bike&#34;, &#34;data&#34;: [ {timestamp, traffic}, ... ]}]</span>
</span></span><span class="line"><span class="cl">    <span class="n">rows</span> <span class="o">=</span> <span class="n">series</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;data&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">counts</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="n">DayCount</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">date</span><span class="o">=</span><span class="n">dt</span><span class="o">.</span><span class="n">date</span><span class="o">.</span><span class="n">fromisoformat</span><span class="p">(</span><span class="n">row</span><span class="p">[</span><span class="s2">&#34;timestamp&#34;</span><span class="p">][:</span><span class="mi">10</span><span class="p">]),</span>
</span></span><span class="line"><span class="cl">            <span class="n">count</span><span class="o">=</span><span class="n">row</span><span class="p">[</span><span class="s2">&#34;traffic&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;counts&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">rows</span>
</span></span><span class="line"><span class="cl">    <span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">counts</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">d</span><span class="p">:</span> <span class="n">d</span><span class="o">.</span><span class="n">date</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># --------------------------------------------------------------------------- #</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Convenience: put the three steps together</span>
</span></span><span class="line"><span class="cl"><span class="c1"># --------------------------------------------------------------------------- #</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">get_daily_counts</span><span class="p">(</span><span class="n">counter_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">year</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">month</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="n">DayCount</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Fetch and parse the daily counts for one site and month.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">parse_daily_counts</span><span class="p">(</span><span class="n">fetch_rsc_payload</span><span class="p">(</span><span class="n">counter_id</span><span class="p">,</span> <span class="n">year</span><span class="p">,</span> <span class="n">month</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Example: the Kennedybrücke counter in Bonn</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># It&#39;s an honour to be a Bonner! :)</span>
</span></span><span class="line"><span class="cl">    <span class="n">KENNEDYBRUECKE</span> <span class="o">=</span> <span class="mi">100021143</span>
</span></span><span class="line"><span class="cl">    <span class="n">today</span> <span class="o">=</span> <span class="n">dt</span><span class="o">.</span><span class="n">date</span><span class="o">.</span><span class="n">today</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">counts</span> <span class="o">=</span> <span class="n">get_daily_counts</span><span class="p">(</span><span class="n">KENNEDYBRUECKE</span><span class="p">,</span> <span class="n">today</span><span class="o">.</span><span class="n">year</span><span class="p">,</span> <span class="n">today</span><span class="o">.</span><span class="n">month</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Daily counts for site </span><span class="si">{</span><span class="n">KENNEDYBRUECKE</span><span class="si">}</span><span class="s2">, </span><span class="si">{</span><span class="n">today</span><span class="si">:</span><span class="s2">%B %Y</span><span class="si">}</span><span class="s2">:&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">day</span> <span class="ow">in</span> <span class="n">counts</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">value</span> <span class="o">=</span> <span class="s2">&#34;—&#34;</span> <span class="k">if</span> <span class="n">day</span><span class="o">.</span><span class="n">count</span> <span class="ow">is</span> <span class="kc">None</span> <span class="k">else</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">day</span><span class="o">.</span><span class="n">count</span><span class="si">:</span><span class="s2">,</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;  </span><span class="si">{</span><span class="n">day</span><span class="o">.</span><span class="n">date</span><span class="si">:</span><span class="s2">%Y-%m-%d</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">value</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Pick out a single day (e.g. yesterday).</span>
</span></span><span class="line"><span class="cl">    <span class="n">yesterday</span> <span class="o">=</span> <span class="n">today</span> <span class="o">-</span> <span class="n">dt</span><span class="o">.</span><span class="n">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">match</span> <span class="o">=</span> <span class="nb">next</span><span class="p">((</span><span class="n">d</span> <span class="k">for</span> <span class="n">d</span> <span class="ow">in</span> <span class="n">counts</span> <span class="k">if</span> <span class="n">d</span><span class="o">.</span><span class="n">date</span> <span class="o">==</span> <span class="n">yesterday</span><span class="p">),</span> <span class="kc">None</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="k">match</span> <span class="ow">and</span> <span class="k">match</span><span class="o">.</span><span class="n">count</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">Yesterday (</span><span class="si">{</span><span class="n">yesterday</span><span class="si">}</span><span class="s2">): </span><span class="si">{</span><span class="n">match</span><span class="o">.</span><span class="n">count</span><span class="si">:</span><span class="s2">,</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">No data for yesterday (</span><span class="si">{</span><span class="n">yesterday</span><span class="si">}</span><span class="s2">) yet.&#34;</span><span class="p">)</span>
</span></span></code></pre></div>]]></content:encoded></item><item><title>Von Poetry zu uv: So migrierst du dein Python-Projekt</title><link>https://theus.name/post/poetry-projekt-nach-uv-umziehen/</link><pubDate>Fri, 30 May 2025 12:41:00 +0000</pubDate><guid>https://theus.name/post/poetry-projekt-nach-uv-umziehen/</guid><description>&lt;p&gt;Die meisten meiner privaten Projekte schreibe ich in Python.&#10;&lt;a href="https://github.com/python-poetry/poetry/releases/tag/1.0.0"&gt;Ende 2019&lt;/a&gt; erschien Poetry.&#10;Es war lange mein bevorzugtes Tool für das Dependency- und Paketmanagement in Python.&#10;Es hat das Hinzufügen, Aktualisieren und Entfernen von Abhängigkeiten enorm vereinfacht, automatisch virtuelle Umgebungen verwaltet und mit der &lt;code&gt;poetry.lock&lt;/code&gt;-Datei für reproduzierbare Setups gesorgt.&#10;Heute probiere ich ein neues Tool aus: &lt;a href="https://docs.astral.sh/uv/"&gt;uv&lt;/a&gt;.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Die meisten meiner privaten Projekte schreibe ich in Python.
<a href="https://github.com/python-poetry/poetry/releases/tag/1.0.0">Ende 2019</a> erschien Poetry.
Es war lange mein bevorzugtes Tool für das Dependency- und Paketmanagement in Python.
Es hat das Hinzufügen, Aktualisieren und Entfernen von Abhängigkeiten enorm vereinfacht, automatisch virtuelle Umgebungen verwaltet und mit der <code>poetry.lock</code>-Datei für reproduzierbare Setups gesorgt.
Heute probiere ich ein neues Tool aus: <a href="https://docs.astral.sh/uv/">uv</a>.</p>
<p>Mit uv gibt es einen neuen, in Rust geschriebenen Paketmanager, der manche Schwächen von Poetry adressiert und neue Vorteile bietet.
Ausschlaggebend für meinen Test war, dass uv von den gleichen Entwicklern wie <a href="https://docs.astral.sh/ruff/">Ruff</a> stammt – ein Tool, das ich bereits intensiv nutze und von dem ich absolut begeistert bin.
Da Ruff meine Arbeitsweise in Python bereits deutlich verbessert hat, war ich gespannt, ob uv ähnliche Vorteile bieten kann.</p>
<p>Was mir bei Poetry bislang wirklich gefehlt hat, war die einfache Möglichkeit, zwischen verschiedenen Python-Versionen zu wechseln.
uv ersetzt nicht nur pip und Poetry, sondern auch Tools wie pyenv, mit denen sich mehrere Python-Versionen parallel installieren, verwalten und projektbezogen auswählen lassen – ohne Versionskonflikte.
Die jeweils benötigte Python-Version wird von uv automatisch installiert und verwaltet, indem du eine <code>.python-version</code>-Datei im Projekt setzt.</p>
<p>Und auch wenn Poetry schon einen enormen Geschwindigkeitsvorteil gegenüber dem klassischen Ansatz mit pip bietet, ist uv noch einmal schneller.
10-100 Mal schneller als pip versprechen die Entwickler.
In den meisten Projekten, die ich privat oder auf der Arbeit betreue, hat mich die Geschwindigkeit von Poetry zwar nur selten gestört, wenn du uv aber einmal benutzt, ist die Schwuppdizität spürbar erhöht.
Auch für CI-Pipelines ein enormer Gewinn.</p>
<h2 id="installation">Installation</h2>
<p>Zur <a href="https://docs.astral.sh/uv/getting-started/installation/">Installation von uv</a> gibt es eine Vielzahl Möglichkeiten.
Ich habe uv aus den Paketquellen meiner Linux-Distribution installiert, wenn das für dich nicht möglich ist, empfehle ich die Installation mit dem <a href="https://docs.astral.sh/uv/getting-started/installation/#standalone-installer">Standalone installer</a>.</p>
<p>Anschließend trägst du dir die <a href="https://docs.astral.sh/uv/getting-started/installation/#shell-autocompletion">Shell autocompletion</a> in die Konfigurations-Datei deiner Shell ein.
Folge dazu einfach den Schritten in der Dokumenation.</p>
<h2 id="migration">Migration</h2>
<p>Die Migration deines bestehenden Projekts ist wirklich extrem einfach.
uv bringt uvx mit.
uvx ist ein Kommandozeilen-Tool und eine Kurzform für <code>uv tool run</code>, das zum schnellen Ausführen von Python-basierten Kommandozeilen-Tools dient.
Mit uvx kannst du beliebige Python-Tools ausführen, ohne sie vorher in deiner Projektumgebung installieren zu müssen.
Stattdessen wird das gewünschte Tool in einer temporären, von uv verwalteten virtuellen Umgebung ausgeführt, die im Cache liegt.
Das hält deine Projektabhängigkeiten sauber und ermöglicht eine sehr schnelle Ausführung.</p>
<p>uvx kannst du auch nutzen, um das Tool <a href="https://pypi.org/project/migrate-to-uv/">migrate-to-uv</a> zu nutzen und damit dein Projekt von Poetry (oder einem anderen Paketmanager) zu uv zu migrieren.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">uvx migrate-to-uv
</span></span></code></pre></div><p>Dieses Tool übernimmt automatisch:</p>
<ul>
<li>Die Konvertierung der Projektmetadaten von Poetry zu uv</li>
<li>Die Übernahme der Abhängigkeitsversionen</li>
<li>Die Erzeugung einer neuen <code>uv.lock</code>-Datei (und das Entfernen der <code>poetry.lock</code>-Datei)</li>
<li>Die Beibehaltung von Abhängigkeitsgruppen (Dependency Groups)</li>
</ul>
<p>Zum Schluss bietet sich noch das Setzen eines Alias zum Aktivieren des virtuellen Environment an.
Einen solchen Alias habe ich schon seit langem und erleichtert mir enorm die Aktivierung.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">avenv<span class="o">()</span> <span class="o">{</span> <span class="nb">source</span> venv/bin/activate 2<span class="p">&amp;</span>gt<span class="p">;</span>/dev/null <span class="o">||</span> <span class="nb">source</span> .venv/bin/activate 2<span class="p">&amp;</span>gt<span class="p">;</span>/dev/null <span class="o">||</span> <span class="nb">echo</span> <span class="s2">&#34;No venv found.&#34;</span><span class="p">;</span> <span class="o">}</span>
</span></span></code></pre></div><p>Diese Zeile trägst du in deine Shell-Konfiguration ein und kannst ab sofort mit <code>avenv</code> das Environment öffnen.</p>
<h2 id="fazit">Fazit</h2>
<p>Nach vielen Jahren mit Poetry als treuem Begleiter im Python-Ökosystem hat mich uv wirklich überrascht.
Die Kombination aus beeindruckender Geschwindigkeit, einfacher Verwaltung verschiedener Python-Versionen und der wirklich unkomplizierten Migration bestehender Projekte macht uv zu einer spannenden Alternative – gerade auch für Entwickler:innen, die Wert auf moderne und effiziente Workflows legen.</p>]]></content:encoded></item><item><title>Sicherheit oder Sackgasse? 2-Faktor-Authentifizierung der Bahn unter der Lupe</title><link>https://theus.name/post/sicherheit-oder-sackgasse-2-faktor-authentifizierung-der-bahn-unter-der-lupe/</link><pubDate>Mon, 05 Feb 2024 16:33:00 +0000</pubDate><guid>https://theus.name/post/sicherheit-oder-sackgasse-2-faktor-authentifizierung-der-bahn-unter-der-lupe/</guid><description>&lt;p&gt;Die Deutsche Bahn setzt auf die Sicherheit von 2-Faktor-Authentifizierung (2FA), doch es scheint, dass es dabei einige Unannehmlichkeiten für die Nutzer gibt.&#10;In diesem Artikel werfe ich einen kritischen Blick auf die Herausforderungen und schlage mögliche Verbesserungen vor.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Die Deutsche Bahn setzt auf die Sicherheit von 2-Faktor-Authentifizierung (2FA), doch es scheint, dass es dabei einige Unannehmlichkeiten für die Nutzer gibt.
In diesem Artikel werfe ich einen kritischen Blick auf die Herausforderungen und schlage mögliche Verbesserungen vor.</p>
<p>Die Deutsche Bahn ermöglicht die Nutzung von SMS-Codes oder Authentifikator-Apps für die 2FA.
Allerdings gibt es während und nach der Einrichtung einige Fallstricke.
Eine besonders kritische Situation entsteht, wenn der Zugang über die Authentifikator-App verloren geht.
Ich würde mir wünschen, dass die Bahn eine Möglichkeit zur Sicherung und Wiederherstellung anbietet für die man keine Hotline bemühen muss.</p>
<p><strong>Fehlende Backup-Codes</strong></p>
<p>Ein großes Manko scheint mir die fehlende Möglichkeit zu sein, Backup-Codes herunterzuladen.
Backup-Codes sind entscheidend, besonders wenn der Zugang über die Hauptmethode verloren geht.
Es sind vorab generierte Codes, die du sicher aufbewahren kannst.
Im Falle, dass du keinen Zugriff auf deine Haupt-2FA-Methode (z.B., wenn du dein Handy verlierst oder die Authentifikator-App nicht funktioniert) hast, kannst du diese Backup-Codes verwenden, um dich dennoch anzumelden.</p>
<figure>
    
    <img
      src="https://theus.name/post/sicherheit-oder-sackgasse-2-faktor-authentifizierung-der-bahn-unter-der-lupe/grafik.png"
      width="1180"
      height="424"
      alt="Ein Screenshot des „Account Manager“ der Deutschen Bahn. Der Tab „2-Faktor-Authentifizierung“ ist aktiviert. Dort steht der Text: „Kundenkonto geschützt. Zusätzlicher Schutz via Authentifizierungs-App“. Darunter nur ein Link zurück zur Seite der Bahn.">
  
  <figcaption>Der „Account Manager“ erlaubt dir keine weitere 2-Faktor-Methode</figcaption>
</figure>

<p><strong>Begrenzte Möglichkeiten nach Einrichtung</strong></p>
<p>Nach erfolgreicher Einrichtung einer der beiden Methoden ist es scheinbar nicht möglich, eine zweite Methode hinzuzufügen.
Auf Grund der fehlenden Backup-Codes wäre das eine gute Möglichkeit den Zugriff auf sein Konto sicherstellen zu können.
Hilft die Bahn dir im Worst-Case wenigstens mit guten Hilfetexten weiter?</p>
<p><strong>Fehlermeldungen und Support</strong></p>
<p>Wenn man dann doch keinen Zugriff auf seinen zweiten Faktor mehr hat, steht man erst einmal alleine da.
Die Login-Maske fordert stur die Eingabe des 2-Faktor-Codes.
Gibt man einen falschen ein, erscheint lediglich „Ungültiger Authentifizierungscode“.
Der Link „Login-Daten vergessen?“ ist nach Eingabe des Passworts bereits verschwunden – und würde auch nicht weiterhelfen.
Zum Glück gibt es ein FAQ zur 2-Faktor-Authentifizierung.
Die muss man aber erst einmal finden.
Zum Glück erscheinen die relevanten Fragen aber auch dann, wenn man „2-Faktor“ in das Suchfeld der allgemeinen „Hilfe &amp; Kontakt“-Seite für Privatkunden eintippt.</p>
<p><strong>FAQ</strong></p>
<p>Die FAQ spricht in der <a href="https://www.bahn.de/faq/keinen-zugriff-mehr-auf-handy-und-2-fa-aktiviert--neues-passwort-anfordern">relevanten Hilfe-Seite</a> zwar nur davon, was man bei verlorenem Zugriff auf SMS-Codes tun kann.
Ich gehe aber davon aus, dass die Antwort bei Nutzung einer Authentifikator-App die gleiche wäre: „Bitte kontaktieren Sie unser Service-Team unter 030 2970.“ Dauer der Bearbeitungszeit: Bis zu einer Woche!</p>
<figure>
    
    <img
      src="https://theus.name/post/sicherheit-oder-sackgasse-2-faktor-authentifizierung-der-bahn-unter-der-lupe/grafik-1.png"
      width="600"
      height="262"
      alt="Ein Auszug aus der FAQ der Deutschen Bahn. Frage: Ich kann mich nicht mehr in mein Kundenkonto einloggen, da der SMS-Code zur 2-Faktor-Authentifizierung an eine alte Handynummer gesendet wird. Was kann ich tun? Antwort: Bitte kontaktieren Sie unser Service-Team unter 030 2970. Die Kolleg:innen helfen Ihnen rund um die Uhr nach einem Datenabgleich gerne weiter. Bitte beachten Sie, dass eine Rücksetzung der 2-Faktor-Authentifizierung bis zu einer Woche dauern kann. Bei dringendem Buchungswunsch empfehlen wir Ihnen eine Buchung ohne Anmeldung.">
  
  <figcaption>Letzter Ausweg: Die Support-Hotline. Bearbeitungszeit: Bis zu einer Woche</figcaption>
</figure>

<p>Insbesondere die Dauer für die Rücksetzung der 2FA könnte für viele Kunden problematisch sein.
Hier muss die Deutsche Bahn die Prozesse optimieren, um eine schnellere Unterstützung zu gewährleisten.
Ist man auf Reisen unterwegs und hat sein Ticket nur in der App dabei, verliert aber – aus welchen Gründen auch immer – den Zugriff auf sein Bahn-Konto, wird man im Zweifelsfall ein zweites Ticket kaufen müssen.</p>
<p><strong>Mein Fazit</strong></p>
<p>Insgesamt gibt es großen Raum für Verbesserungen bei der 2FA der Deutschen Bahn.
Eine erhöhte Flexibilität bei der Einrichtung durch die Möglichkeit der Einrichtung mehrer 2Fa-Methoden, klare Anweisungen bei Problemen und eine schnellere Unterstützung durch den Kundenservice könnten die Nutzererfahrung erheblich verbessern.
Und die Deutsche Bahn würde davon auch profitieren: Durch eine Entlastung ihrer Support-Hotline.
Aktuell kann ich die Einrichtung der 2-Faktor-Authentifizierung bei der Deutschen Bahn nur bedingt empfehlen.</p>]]></content:encoded></item><item><title>Hugo: Seite verbergen</title><link>https://theus.name/post/hugo-seite-verbergen/</link><pubDate>Sat, 16 Dec 2023 10:18:32 +0000</pubDate><guid>https://theus.name/post/hugo-seite-verbergen/</guid><description>&lt;p&gt;Während meiner langjährigen Arbeit mit Hugo, einem Static-Site-Generator zur Erstellung von Webseiten, bin ich auf ein spannendes Problem gestoßen.&#10;Bei der Erstellung einer Logowand für die Startseite einer Website kam es zu einem unerwarteten Ergebnis.&#10;Obwohl die Logos auf der Startseite angezeigt wurden, wurden für jedes Partnerunternehmen separate Seiten generiert.&#10;Diese unerwünschten Seiten tauchten auch in den Ergebnisseiten der Suchmaschinen auf und stellten somit ein Problem dar.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Während meiner langjährigen Arbeit mit Hugo, einem Static-Site-Generator zur Erstellung von Webseiten, bin ich auf ein spannendes Problem gestoßen.
Bei der Erstellung einer Logowand für die Startseite einer Website kam es zu einem unerwarteten Ergebnis.
Obwohl die Logos auf der Startseite angezeigt wurden, wurden für jedes Partnerunternehmen separate Seiten generiert.
Diese unerwünschten Seiten tauchten auch in den Ergebnisseiten der Suchmaschinen auf und stellten somit ein Problem dar.</p>
<p>Ich arbeite seit geraumer Zeit mit Hugo.
Für die Erzeugung einer Logowand von Partnern eines Unternehmens habe ich unter <code>content/partners/</code> für jeden Partner, der angezeigt werden soll, eine Markdown-Datei angelegt.
Dort konnte ich dann z.B. das Logo definieren und eine URL als Link hinterlegen.
Das hat den großen Vorteil, dass man alle Inhalte sehr strukturiert abgelegt hat.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="nx">---</span>
</span></span><span class="line"><span class="cl"><span class="nx">title</span><span class="err">:</span> <span class="s2">&#34;Beispielunternehmen goes brrr&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">image</span><span class="err">:</span> <span class="s2">&#34;images/partners/partner-logo.svg&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">link</span><span class="err">:</span> <span class="s2">&#34;https://example.com/&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">---</span>
</span></span></code></pre></div><p>Die Logos habe ich dann in das Layout der Startseite integriert:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl">{{ with .Site.GetPage &#34;/partners&#34; }}
</span></span><span class="line"><span class="cl">  {{ range .Pages }}
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;{{ .Params.link }}&#34;</span> <span class="na">target</span><span class="o">=</span><span class="s">&#34;_blank&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">img</span>
</span></span><span class="line"><span class="cl">        <span class="na">src</span><span class="o">=</span><span class="s">&#34;{{ (resources.GetMatch .Params.image).RelPermalink }}&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="na">alt</span><span class="o">=</span><span class="s">&#34;{{ .Params.title }}&#34;</span> <span class="na">loading</span><span class="o">=</span><span class="s">&#34;lazy&#34;</span> <span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl">     <span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  {{ end }}
</span></span><span class="line"><span class="cl">{{ end }}
</span></span></code></pre></div><p>Was mir erst ein paar Tage nach der Veröffentlichung der Website aufgefallen ist, ist, dass ich jetzt zwar alle Logos auf der Startseite angezeigt bekomme, aber jeder Partner jetzt auch eine eigene Seite generiert bekommen hat.
So gibt es jetzt für jeden Partner unter <code>https://example.com/partners/beispielunternehmen/</code> eine Seite, auf der nur das Logo und der Titel angezeigt werden.</p>
<p>Das hätte ich natürlich wissen müssen, aber irgendwie ist es mir entgangen.
Die Seiten sind zwar alle nirgendwo auf der Seite verlinkt, können aber trotzdem irgendwann in der Suchmaschine deiner Wahl auftauchen, weil sie in der von Hugo erzeugten <code>sitemap.xml</code> gelistet sind.
Und das ist hier nicht erwünscht.</p>
<h2 id="mein-erster-versuch">Mein erster Versuch</h2>
<p>Ich erstelle zuerst eine <code>index.md</code>. Dadurch werden die Partnerseiten nicht mehr angezeigt.
Allerdings habe ich dann immer noch eine Partnerseite, also <code>https://example.com/partners/</code>.
Nach einigem Suchen in Hugos Dokumentation habe ich die <a href="https://gohugo.io/content-management/build-options/">Build Options</a> gefunden.
Das schien genau das zu sein, was ich gesucht hatte.
Jetzt musste ich nur noch den folgenden <a href="https://gohugo.io/content-management/front-matter/%22">Front Matter</a> in die <code>index.md</code> einfügen:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="nx">---</span>
</span></span><span class="line"><span class="cl"><span class="nx">_build</span><span class="err">:</span>
</span></span><span class="line"><span class="cl">    <span class="nx">list</span><span class="err">:</span> <span class="nx">never</span>
</span></span><span class="line"><span class="cl">    <span class="nx">publishResources</span><span class="err">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">    <span class="nx">render</span><span class="err">:</span> <span class="nx">never</span>
</span></span><span class="line"><span class="cl"><span class="nx">---</span>
</span></span></code></pre></div><p>Das funktioniert auch wunderbar.
Allerdings habe ich nicht bedacht, dass durch die <code>index.md</code> und die Art und Weise, wie ich derzeit die Logowand im Layout programmiert habe, kein einziges Partnerlogo mehr angezeigt wird.
Denn im Layout iteriere ich über alle Unterseiten der <code>/partners</code>-Seite.
Und die gibt es nicht mehr.</p>
<h2 id="mein-zweiter-versuch">Mein zweiter Versuch</h2>
<p>Aber auch das lässt sich mit wenig Aufwand ändern.
Denn die Markdown-Dateien mit den Partnerlogos sind jetzt Ressourcen der Partnerseite.
Statt über die Unterseiten iteriere ich jetzt einfach über die <a href="https://gohugo.io/content-management/page-resources/">Ressourcen der Seite</a>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl">{{ with .Site.GetPage &#34;/partners&#34; }}
</span></span><span class="line"><span class="cl">  {{ range .Pages }}
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;{{ .Params.link }}&#34;</span> <span class="na">target</span><span class="o">=</span><span class="s">&#34;_blank&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">img</span>
</span></span><span class="line"><span class="cl">        <span class="na">src</span><span class="o">=</span><span class="s">&#34;{{ (resources.GetMatch .Params.image).RelPermalink }}&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="na">alt</span><span class="o">=</span><span class="s">&#34;{{ .Params.title }}&#34;</span> <span class="na">loading</span><span class="o">=</span><span class="s">&#34;lazy&#34;</span> <span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl">     <span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  {{ end }}
</span></span><span class="line"><span class="cl">{{ end }}
</span></span></code></pre></div><h2 id="fazit">Fazit</h2>
<p>Bei der Suche nach einer Lösung habe ich zunächst versucht, die Partnerseiten durch Erstellung einer <code>index.md</code> zu verbergen.
Allerdings führte dies dazu, dass die Partnerlogos nicht mehr angezeigt wurden, da das Layout über die nicht mehr vorhandenen Unterseiten iterierte.
Nach etwas Recherche in der Hugo-Dokumentation stieß ich auf die <em>Build Options</em>, die eine Lösung zu bieten schienen.
Durch die Nutzung dieser Funktion konnte ich die Datei <code>index.md</code> so konfigurieren, dass sie keine Partnerseiten anzeigt, und die Partnerlogos dennoch auf der Startseite darstellt.</p>
<p>Die Anpassung des Layout-Codes, um über die Ressourcen der Seite zu iterieren, anstatt über die nicht mehr vorhandenen Unterseiten, war der Schlüssel, um die Partnerlogos zurück auf die Startseite zu bringen.
Dieser Prozess hat nicht nur das unerwünschte Indexierungsproblem in den Suchmaschinen gelöst, sondern erneut meine Kenntnisse über die Verwendung von Hugo und dessen vielfältigen Funktionen erweitert.</p>]]></content:encoded></item><item><title>Transparenz entfernen mit Nautilus-Skript</title><link>https://theus.name/post/transparenz-entfernen-mit-nautilus-skript/</link><pubDate>Mon, 10 Jul 2023 15:21:00 +0000</pubDate><guid>https://theus.name/post/transparenz-entfernen-mit-nautilus-skript/</guid><description>&lt;p&gt;In meiner täglichen Arbeit muss ich oft die Transparenz einer PNG-Datei entfernen und stattdessen einen weißen Hintergrund einfügen.&#10;Zum Beispiel, wenn ich ein Bild in einer Anwendung oder einem Webdesign verwenden möchte.&#10;In diesem Artikel zeige ich dir, wie du ein einfaches Bash-Skript in dein Gnome-Dateimanager integrieren kannst, um diesen Vorgang schnell und einfach durchzuführen.&lt;/p&gt;</description><content:encoded><![CDATA[<p>In meiner täglichen Arbeit muss ich oft die Transparenz einer PNG-Datei entfernen und stattdessen einen weißen Hintergrund einfügen.
Zum Beispiel, wenn ich ein Bild in einer Anwendung oder einem Webdesign verwenden möchte.
In diesem Artikel zeige ich dir, wie du ein einfaches Bash-Skript in dein Gnome-Dateimanager integrieren kannst, um diesen Vorgang schnell und einfach durchzuführen.</p>
<p><strong>Schritt 1: Lade ImageMagick herunter und installiere es</strong></p>
<p>Zunächst musst du ImageMagick herunterladen und installieren.
ImageMagick ist ein Open-Source-Tool, das zum Bearbeiten und Konvertieren von Bildern verwendet wird.
Am einfachsten und sichersten installierst du ImageMagick über den Paketmanager deiner Linux-Distribution.
Alternativ kannst du es auch von der <a href="https://imagemagick.org">offiziellen Website</a> herunterladen.</p>
<p><strong>Schritt 2: Öffne einen Texteditor und erstelle das Bash-Skript</strong></p>
<p>Als nächstes öffnest du deinen <a href="https://www.vim.org">bevorzugten Texteditor</a> und erstellst ein neues Bash-Skript.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Überprüfe ob ein Dateiname als Argument übergeben wurde</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> <span class="nv">$#</span> -eq <span class="m">0</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Usage: </span><span class="nv">$0</span><span class="s2"> [input_file]&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Überprüfe ob die übergebene Datei eine PNG-Datei ist</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> ! file <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="p">|</span> grep -q <span class="s2">&#34;PNG image data&#34;</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Error: </span><span class="nv">$1</span><span class="s2"> is not a PNG file&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Erstellen einer temporären Datei für das Ergebnis</span>
</span></span><span class="line"><span class="cl"><span class="nv">temp_file</span><span class="o">=</span><span class="k">$(</span>mktemp<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Alpha-Kanal entfernen und weißen Hintergrund hinzufügen</span>
</span></span><span class="line"><span class="cl">convert <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> -background white -alpha remove <span class="s2">&#34;</span><span class="nv">$temp_file</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Überprüfen, ob die Konvertierung erfolgreich war</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> <span class="nv">$?</span> -eq <span class="m">0</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Umbenennen der ursprünglichen Datei als Backup und Verschieben der konvertierten Datei an ihre Stelle</span>
</span></span><span class="line"><span class="cl">  mv <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">.bak&#34;</span>
</span></span><span class="line"><span class="cl">  mv <span class="s2">&#34;</span><span class="nv">$temp_file</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Success: </span><span class="nv">$1</span><span class="s2"> converted&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Error: failed to convert </span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span></code></pre></div><p>Dieses Skript überprüft zunächst, ob eine Datei als Argument übergeben wurde und ob es sich um eine PNG-Datei handelt.
Dann wird eine temporäre Datei erstellt, der Alpha-Kanal wird entfernt und ein weißer Hintergrund wird hinzugefügt.
Schließlich wird die ursprüngliche Datei umbenannt und die konvertierte Datei wird an ihre Stelle verschoben.
Wenn du keine Sicherungsdatei des Originals behalten möchtest, kannst du <code>mv &quot;$1&quot; &quot;$1.bak&quot;</code> auch durch <code>rm &quot;$1&quot;</code> ersetzen.</p>
<p><strong>Schritt 3: Speichere das Bash-Skript</strong></p>
<p>Speichere das Bash-Skript unter einem aussagekräftigen Namen, z.B.
„remove-alpha“ unter <code>~/.local/share/nautilus/scripts</code> ab.</p>
<p><strong>Schritt 4: Setze die Ausführungsberechtigung für das Bash-Skript</strong></p>
<p>Um das Bash-Skript ausführen zu können, musst du die Ausführungsberechtigung für das Skript setzen.
Öffne dazu ein Terminal-Fenster im Verzeichnis, in dem sich das Bash-Skript befindet, und gebe den folgenden Befehl ein:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">chmod +x remove-alpha
</span></span></code></pre></div><p><strong>Schritt 5: Nautilus öffnen und Bilder konvertieren</strong></p>
<p>Jetzt musst du nur noch deinen Dateibrowser Nautilus öffnen und navigierst zu der Datei, dessen Transparenz du entfernen möchtest.
Mittels Rechtsklick → Skripte → remove-alpha kannst du jetzt schnell und einfach die Transparenz aus dem Bild entfernen.</p>
<p><strong>Fazit</strong></p>
<p>Insgesamt ist das Einrichten des Bash-Skripts ein einfacher Prozess, der dir Zeit und Aufwand bei der Konvertierung von PNG-Dateien sparen kann.
Mit diesem Skript kannst du schnell und einfach den Alpha-Kanal entfernen und einen weißen Hintergrund hinzufügen.
Durch die Integration in Nautilus kannst du super leicht Bilder direkt aus deiner Dateibrowser bearbeiten.</p>]]></content:encoded></item><item><title>Icons, Icons, Icons!</title><link>https://theus.name/post/icons-icons-icons/</link><pubDate>Mon, 10 Jan 2022 13:08:28 +0000</pubDate><guid>https://theus.name/post/icons-icons-icons/</guid><description>&lt;p&gt;Wer hin und wieder Software oder Websites entwickelt, wer Plakate und Flyer gestaltet – all diese Menschen brauchen sie: Icons. Ich stelle dir freie und kostenlose Icon-Sets vor, die aufgrund ihrer Lizenz und Qualität einen Blick wert sind. Alle hier vorgestellten Sets habe ich selbst einmal verwendet und für gut befunden. Von Zeit zu Zeit werde ich diese Liste erweitern.&lt;/p&gt;&#10;&lt;p&gt;Hast du weitere Tipps für gute Icon-Sets? Schreib’s unten in die Kommentare!&lt;/p&gt;</description><content:encoded><![CDATA[<p>Wer hin und wieder Software oder Websites entwickelt, wer Plakate und Flyer gestaltet – all diese Menschen brauchen sie: Icons. Ich stelle dir freie und kostenlose Icon-Sets vor, die aufgrund ihrer Lizenz und Qualität einen Blick wert sind. Alle hier vorgestellten Sets habe ich selbst einmal verwendet und für gut befunden. Von Zeit zu Zeit werde ich diese Liste erweitern.</p>
<p>Hast du weitere Tipps für gute Icon-Sets? Schreib’s unten in die Kommentare!</p>
<h2 id="font-awesome">Font Awesome</h2>
<figure>
    
    <img
      src="https://theus.name/post/icons-icons-icons/fontawesome-300x46.png"
      width="300"
      height="46"
      alt="Vier Beispiel-Icons von Font Awesome: Stern, Pfeil nach rechts, gefülltes Warndreieck und Kamera">
  
</figure>

<p>Den Klassiker vorweg: <a href="https://fontawesome.com/">Font Awesome</a> ist eine der größten Icon-Sammlungen, die es im Netz zu finden gibt.
Wahrscheinlich kennt sie fast jeder, der mit Design irgendwie zu tun hat.
Kostenlos bekommst du rund 2.100 Icons in den Stilen <em>Solid</em> und <em>Regular</em> – die über 600 Marken-Logos sind darin schon enthalten.
Wichtig: Der große Rest kostet extra.
Die Pro-Version bringt deutlich mehr Icons mit und schaltet die Familien <em>Duotone</em> und <em>Sharp</em> sowie die Stile <em>Light</em> und <em>Thin</em> frei.</p>
<p>Lizenz: <a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0 License</a> / <a href="https://scripts.sil.org/OFL">SIL OFL 1.1 License</a></p>
<h2 id="feather">Feather</h2>
<figure>
    
    <img
      src="https://theus.name/post/icons-icons-icons/feather-300x46.png"
      width="300"
      height="46"
      alt="Vier Beispiel-Icons von Feather: Stern, Pfeil nach rechts, Warndreieck und Kamera">
  
</figure>

<p>Ein deutlich kleineres Set an knapp 300 Icons bringt <a href="https://feathericons.com/">Feather</a> mit. Schau also dringend einmal durch, ob Feather alles mitbringt, was du so brauchst. Wenn du Font Awesome inzwischen einmal leid geworden bist, dann lohnt es sich aber dieses Icon-Set einmal anzuschauen.</p>
<p>Lizenz: <a href="https://github.com/feathericons/feather/blob/master/LICENSE">MIT License</a></p>
<h2 id="openmoji">OpenMoji</h2>
<figure>
    
    <img
      src="https://theus.name/post/icons-icons-icons/openmoji-300x50.png"
      width="300"
      height="50"
      alt="Vier Beispiel-Icons von OpenMoji: Smiley mit Sternenaugen, Pfeil nach rechts, Warndreieck und Kamera">
  
</figure>

<p>Ich bin im letzten Jahr ein echter Fan von einem Icon-Set geworden, das eigentlich ein Set von Emoticons ist: <a href="https://openmoji.org">OpenMoji</a>. Rund 4500 Emoticons stellt das Projekt der <a href="https://www.hfg-gmuend.de/">HfG Schwäbisch Gmünd</a> vollständig als Open Source zur Verfügung.</p>
<p>Lizenz: <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a></p>
<h2 id="remix-icon">Remix Icon</h2>
<figure>
    
    <img
      src="https://theus.name/post/icons-icons-icons/remix-300x46.png"
      width="300"
      height="46"
      alt="Vier Beispiel-Icons von Remix Icon: Stern, Pfeil nach rechts, Ausrufezeichen im Kreis und Kamera">
  
</figure>

<p><a href="https://remixicon.com/">Remix Icon</a> ist ein sehr neutrales und schlichtes Icon Set mit Symbolen, die es jeweils ausgefüllt und als Kontur gibt. Über 3200 Symbole sind inzwischen verfügbar.</p>
<p>Lizenz: <a href="https://github.com/Remix-Design/remixicon/blob/master/License">Apache License 2.0</a></p>
<h2 id="material-icons--material-symbols">Material Icons / Material Symbols</h2>
<figure>
    
    <img
      src="https://theus.name/post/icons-icons-icons/material-icons-1-300x44.png"
      width="300"
      height="44"
      alt="Vier Beispiel-Icons von Material Symbols: Stern, Pfeil nach rechts, Ausrufezeichen im Kreis und Fotokamera">
  
</figure>

<p>Die <a href="https://fonts.google.com/icons">Material Symbols</a> sind zwei verschiedene offizielle Icon-Sets von Google, die auf dem gleichen Design basieren. Material Icons ist das klassische Set, während Material Symbols im April 2022 eingeführt wurde und auf einer neuen Technologie namens „Variable Font“ basiert. Dadurch hast du noch mehr Möglichkeiten die Symbole an deine Bedürfnisse anzupassen. Die Schriftart als Dateien kannst du im <a href="https://github.com/google/material-design-icons">Github-Repository</a> herunterladen.</p>
<p>Lizenz: <a href="https://github.com/google/material-design-icons/blob/master/LICENSE">Apache License 2.0</a></p>
<h2 id="lucide">Lucide</h2>
<figure>
    
    <img
      src="https://theus.name/post/icons-icons-icons/lucide-300x46.png"
      width="300"
      height="46"
      alt="Vier Beispiel-Icons von Lucide: Stern, Pfeil nach rechts, Warndreieck mit Ausrufezeichen und Kamera">
  
</figure>

<p><a href="https://lucide.dev/">Lucide</a> ist als Community-Fork von Feather entstanden, nachdem dessen Entwicklung ins Stocken geraten war.
Die schlichte Handschrift ist geblieben, der Umfang aber deutlich gewachsen: Inzwischen sind es über 1700 Icons.
Wer Feather kennt und einfach mehr Auswahl braucht, ist hier genau richtig: der Umstieg fällt kaum auf.
Fertige Pakete gibt es für so ziemlich jedes Framework.</p>
<p>Lizenz: <a href="https://github.com/lucide-icons/lucide/blob/main/LICENSE">ISC License</a></p>
<h2 id="heroicons">Heroicons</h2>
<figure>
    
    <img
      src="https://theus.name/post/icons-icons-icons/heroicons-300x46.png"
      width="300"
      height="46"
      alt="Vier Beispiel-Icons von Heroicons: Stern, Pfeil nach rechts, Warndreieck mit Ausrufezeichen und Kamera">
  
</figure>

<p><a href="https://heroicons.com/">Heroicons</a> kommt von den Machern von Tailwind CSS.
Mit 316 Symbolen ist das Set sehr klein.
Jedes Icon gibt es in vier optimierten Varianten: als Kontur und gefüllt in 24 Pixel, dazu <em>Mini</em> mit 20 und <em>Micro</em> mit 16 Pixel.</p>
<p>Lizenz: <a href="https://github.com/tailwindlabs/heroicons/blob/master/LICENSE">MIT License</a></p>
<h2 id="update">Update</h2>
<ul>
<li><em>1. September 2026: Lucide und Heroicons hinzugefügt, Zahlen aktualisiert</em></li>
<li><em>10. März 2023: Material Icons / Material Symbols hinzugefügt</em></li>
<li><em>16. November 2022: Aktualisierte Zahlen</em></li>
</ul>
]]></content:encoded></item><item><title>Absatz oder Zeilenumbruch</title><link>https://theus.name/post/absatz-oder-zeilenumbruch/</link><pubDate>Wed, 07 Oct 2020 07:29:08 +0000</pubDate><guid>https://theus.name/post/absatz-oder-zeilenumbruch/</guid><description>&lt;p&gt;Immer wieder sehe ich in Dokumenten, dass vielen Leuten der Unterschied zwischen einem Zeilenumbruch und einem Absatz nicht ganz klar ist.&#10;In diesem kleinen Artikel erkläre ich den Unterschied und zeige wie man in Word/LibreOffice und Latex das eine oder das andere erstellt.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Immer wieder sehe ich in Dokumenten, dass vielen Leuten der Unterschied zwischen einem Zeilenumbruch und einem Absatz nicht ganz klar ist.
In diesem kleinen Artikel erkläre ich den Unterschied und zeige wie man in Word/LibreOffice und Latex das eine oder das andere erstellt.</p>
<p>Am einfachsten lässt sich der Unterschied an einem Gedicht erkennen.</p>
<blockquote >
  <p>Ach, wer bringt die schönen Tage, <br>
Jene Tage der ersten Liebe, <br>
Ach, wer bringt nur eine Stunde <br>
Jener holden Zeit zurück!</p>
<p>Einsam nähr ich meine Wunde, <br>
Und mit stets erneuter Klage <br>
Traur ich ums verlorne Glück.</p>
<p>Ach, wer bringt die schönen Tage, <br>
Jene holde Zeit zurück!</p>

  
    <cite>Erster Verlust, Johann Wolfgang von Goethe</cite>
  
</blockquote>
<p>Wie du sicher weißt, besteht ein Gedicht aus <em>Versen</em> und <em>Strophen</em>.
Einen (harten) Zeilenumbruch nutzt man in einem Gedicht zwischen einzelnen Versen, einen Absatz zwischen einzelnen Strophen.
Ein Zeilenumbruch trennt also Verse, lässt aber trotzdem ihre Zugehörigkeit zueinander erkennen.
Bei einem Absatz ist die Trennung größer und daher werden Absätze in der Regel auch durch einen deutlichen Abstand zueinander dargestellt.
Dieser Abstand erleichtert uns ganz erheblich das Lesen von langen Texten. Manchmal wird zusätzlich auch die erste Zeile des Absatzes etwas eingerückt.</p>
<p>Zur besseren Lesbarkeit und für ein einfacheres Verständnis sollte ein Artikel oder ein sonstiger langer Text in Absätze gegliedert sein.
Harte Zeilenumbrüche sollten dagegen die große Ausnahme sein.
Doch wie erzeugt man einen Absatz und wie einen harten Zeilenumbruch?</p>
<h2 id="word--libreoffice--">Word / LibreOffice / …</h2>
<p>In Word und LibreOffice und vielen weiteren Applikationen erzeugt man in einem Text einen Absatz durch ein einfaches <code>Enter</code>.
Einen Zeilenumbruch erzeugt man durch <code>Shift+Enter</code>.</p>
<h2 id="latex">Latex</h2>
<p>In Latex erzeugt man einen Absatz, indem man eine Leerzeile anlegt.
Einen Zeilenumbruch erzeugt man mit zwei Backslashes <code>\\</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-latex" data-lang="latex"><span class="line"><span class="cl">Ach, wer bringt die schönen Tage,<span class="k">\\</span>
</span></span><span class="line"><span class="cl">Jene Tage der ersten Liebe,<span class="k">\\</span>
</span></span><span class="line"><span class="cl">Ach, wer bringt nur eine Stunde<span class="k">\\</span>
</span></span><span class="line"><span class="cl">Jener holden Zeit zurück!
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Einsam nähr ich meine Wunde,<span class="k">\\</span>
</span></span><span class="line"><span class="cl">Und mit stets erneuter Klage<span class="k">\\</span>
</span></span><span class="line"><span class="cl">Traur ich ums verlorne Glück.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Ach, wer bringt die schönen Tage,<span class="k">\\</span>
</span></span><span class="line"><span class="cl">Jene holde Zeit zurück!
</span></span></code></pre></div><h2 id="tldr">TL;DR</h2>
<p>Benutze harte Zeilenumbrüche nur wenn du dir ganz sicher bist, dass sie da hingehören.
Ansonsten benutze Absätze.</p>]]></content:encoded></item><item><title>SSH-Config nutzen</title><link>https://theus.name/post/ssh-config-nutzen/</link><pubDate>Wed, 18 Dec 2019 15:58:55 +0000</pubDate><guid>https://theus.name/post/ssh-config-nutzen/</guid><description>&lt;p&gt;Ich hab viel zu lange nichts mehr geschrieben und daher gibt es heute ein kurzes, wenn auch sehr wichtiges Thema: Die SSH-Config!&#10;Viel zu wenige nutzen sie und machen sich dadurch viel Arbeit, die man sich doch so gut sparen kann.&lt;/p&gt;&#10;&lt;p&gt;Es gibt bestimmt viel zu erzählen über die SSH-Config.&#10;Ich möchte heute erklären, wofür sie die meisten Leute nutzen: Um sich Server, die sie regelmäßig besuchen, abzuspeichern und nur noch einen Hostnamen hinter den SSH-Befehl tippen zu müssen.&#10;Kein User-Name, kein alternativer Port und auch kein abweichendes Zertifikat.&#10;Nur ein:&lt;/p&gt;</description><content:encoded><![CDATA[<p>Ich hab viel zu lange nichts mehr geschrieben und daher gibt es heute ein kurzes, wenn auch sehr wichtiges Thema: Die SSH-Config!
Viel zu wenige nutzen sie und machen sich dadurch viel Arbeit, die man sich doch so gut sparen kann.</p>
<p>Es gibt bestimmt viel zu erzählen über die SSH-Config.
Ich möchte heute erklären, wofür sie die meisten Leute nutzen: Um sich Server, die sie regelmäßig besuchen, abzuspeichern und nur noch einen Hostnamen hinter den SSH-Befehl tippen zu müssen.
Kein User-Name, kein alternativer Port und auch kein abweichendes Zertifikat.
Nur ein:</p>
<pre tabindex="0"><code>$ ssh servername
</code></pre><p>Erreichen kann man dieses Verhalten durch eine Bearbeitung der Datei <code>~/.ssh/config</code> (eventuell muss diese erst angelegt werden).
Dort kann man dann zum Beispiel Folgendes definieren:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">Host servername</span>
</span></span><span class="line"><span class="cl">    <span class="na">HostName servername.uberspace.de</span>
</span></span><span class="line"><span class="cl">    <span class="na">Port 65022</span>
</span></span><span class="line"><span class="cl">    <span class="na">User theus</span>
</span></span><span class="line"><span class="cl">    <span class="na">IdentityFile ~/.ssh/id_work</span>
</span></span></code></pre></div><p>Die erste Zeile definiert einen Host und gibt direkt dessen Alias <em>servername</em> mit an.
Die zweite Zeile gibt nun den Hostnamen an, also die Domain deines Servers.
Falls SSH abweichend zum Standard mal nicht auf Port 22 läuft, so kannst du das wie in Zeile 3 definieren.
Außerdem kannst du dir die Eingabe eines Benutzernamens durch eine entsprechende Konfiguration sparen und schließlich auch ein abweichendes Zertifikat angeben, wenn du zum Beispiel dienstliche und private Server mit verschiedenen Identities ansprechen möchtest.</p>
<p>Für jeden Server, den du definieren möchtest, erstellst du einen solchen Block in der <code>~/.ssh/config</code> (oder alternativ systemweit in der <code>/etc/ssh/ssh_config</code>).
Wenn du einen Server abweichend von der Config zum Beispiel mit einem anderen Benutzer ansprechen möchtest, kannst du diesen Parameter wie gewohnt in der Kommandozeile überschreiben.
So würde</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">$ ssh root@servername
</span></span></code></pre></div><p>die gleichen Parameter nutzen, wie oben in der Konfiguration angegeben wurde, aber abweichend davon den Nutzer <em>root</em> statt <em>theus</em> nehmen.</p>
<p>Weitere Parameter findet ihr auf unter anderem auf <a href="https://www.ssh.com/ssh/config">ssh.com</a>.
Und nun viel Spaß beim Ausprobieren.</p>
]]></content:encoded></item><item><title>Fahrräder zählen</title><link>https://theus.name/post/fahrraeder-zaehlen/</link><pubDate>Sat, 01 Jun 2019 11:38:22 +0000</pubDate><guid>https://theus.name/post/fahrraeder-zaehlen/</guid><description>&lt;p&gt;In Bonn gibt es 15 Fahrrad-Zählstationen.&#10;Immer wenn ein:e Radfahrer:in diese passiert, wird dies erfasst und bei dem Anbieter dieser Stationen in die Datenbank geschaufelt.&#10;Angeschafft hat diese Zähltationen die Stadt Bonn.&#10;Die Sichtbarkeit dieser Daten ist aber eher gering.&#10;An der Kennedybrücke steht zwar z.B. eine Säule, welche die Tageswerte dieser Station anzeigt, die meisten Werte sind aber nur umständlich über die &lt;a href="https://www.bonn.de/themen-entdecken/verkehr-mobilitaet/staerkung-des-radverkehrs-das-tut-sich-in-bonn.php"&gt;Webseite der Stadt&lt;/a&gt; und des &lt;a href="http://www.eco-public.com/public2/?&amp;amp;id=100021143"&gt;Herstellers&lt;/a&gt; ersichtlich.&#10;Um dies zu ändern poste ich seit einer kurzen Weile auf &lt;del&gt;&lt;a href="https://twitter.com/bonnbike"&gt;Twitter&lt;/a&gt; und&lt;/del&gt; &lt;a href="https://bonn.social/@bonnbike"&gt;Mastodon&lt;/a&gt; tägliche Statistiken aller Fahrradzählstationen.&lt;/p&gt;</description><content:encoded><![CDATA[<p>In Bonn gibt es 15 Fahrrad-Zählstationen.
Immer wenn ein:e Radfahrer:in diese passiert, wird dies erfasst und bei dem Anbieter dieser Stationen in die Datenbank geschaufelt.
Angeschafft hat diese Zähltationen die Stadt Bonn.
Die Sichtbarkeit dieser Daten ist aber eher gering.
An der Kennedybrücke steht zwar z.B. eine Säule, welche die Tageswerte dieser Station anzeigt, die meisten Werte sind aber nur umständlich über die <a href="https://www.bonn.de/themen-entdecken/verkehr-mobilitaet/staerkung-des-radverkehrs-das-tut-sich-in-bonn.php">Webseite der Stadt</a> und des <a href="http://www.eco-public.com/public2/?&amp;id=100021143">Herstellers</a> ersichtlich.
Um dies zu ändern poste ich seit einer kurzen Weile auf <del><a href="https://twitter.com/bonnbike">Twitter</a> und</del> <a href="https://bonn.social/@bonnbike">Mastodon</a> tägliche Statistiken aller Fahrradzählstationen.</p>
<p><strong>Wichtig:</strong> Die folgende Erklärung über die API funktioniert so nicht mehr.</p>
<p>Allgemein dokumentiert ist aktuell nicht, wie man die Daten der Zählstationen erreichen kann.
Ich habe mir dieses Wissen also etwas umständlich aus den Herstellerseiten zusammengefummelt und möchte ich es hier heute für euch dokumentieren.
Es sei bedacht, dass die Stadt zwar regelmäßig (aber nur alle paar Jubelmonate) die Daten der Stationen auf <a href="https://opendata.bonn.de">opendata.bonn.de</a> veröffentlicht, der Zugriff auf die API des Herstellers selbst bisher jedoch nicht explizit gestattet ist.</p>
<p>Der Zugriff auf die Daten einer Zählstation ist recht einfach.
Sie werden im JSON-Format bereitgestellt.
Jede Zählstation hat eine ID, die man einfach an die RESTful API übermittelt.
So sieht eine Anfrage dann ungefähr aus:</p>
<pre tabindex="0"><code>https://www.eco-public.com/api/cw6Xk4jW4X4R/data/periode/{counter_id}?begin={begin_date}&amp;end={end_date}&amp;step={step}
</code></pre><p>Die Variablen in den geschweiften Klammern müsst ihr nun nur noch wie gewünscht ersetzen und erhaltet dann die Daten:</p>
<ul>
<li><code>{counter_id}</code> mit der ID eines Zählers (s. unten)</li>
<li><code>{begin}</code> mit dem Anfangsdatum der gewünschten Daten in der Form <code>YYYYMMDD</code></li>
<li><code>{end}</code> mit dem Enddatum der gewünschten Daten in der Form <code>YYYYMMDD</code></li>
<li><code>{step}</code> mit <code>2</code> für ein Interval von 15 Minuten oder <code>3</code> für ein Intervall von einer Stunde oder <code>4</code> für ein Intervall von einem Tag</li>
</ul>
<p>Das sieht dann zum Beispiel für die Tagesdaten des Monats Mai 2019 für die Kennedeybrücke so aus:</p>
<pre tabindex="0"><code>https://www.eco-public.com/api/cw6Xk4jW4X4R/data/periode/100021143?begin=20190501&amp;end=20190531&amp;step=4
</code></pre><p>Die IDs der einzelnen Zähler habe ich euch hier zusammengetragen:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="s1">&#39;id&#39;</span><span class="p">:</span> <span class="mi">100021143</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">:</span> <span class="s1">&#39;Kennedybrücke (gesamt)&#39;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="s1">&#39;id&#39;</span><span class="p">:</span> <span class="mi">100019810</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">:</span> <span class="s1">&#39;Kennedybrücke (Südseite)&#39;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="s1">&#39;id&#39;</span><span class="p">:</span> <span class="mi">100019809</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">:</span> <span class="s1">&#39;Kennedybrücke (Nordseite)&#39;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="s1">&#39;id&#39;</span><span class="p">:</span> <span class="mi">100032772</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">:</span> <span class="s1">&#39;Südbrücke (gesamt)&#39;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="s1">&#39;id&#39;</span><span class="p">:</span> <span class="mi">100019722</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">:</span> <span class="s1">&#39;Südbrücke (Südseite)&#39;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="s1">&#39;id&#39;</span><span class="p">:</span> <span class="mi">100019723</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">:</span> <span class="s1">&#39;Südbrücke (Nordseite)&#39;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="s1">&#39;id&#39;</span><span class="p">:</span> <span class="mi">100019720</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">:</span> <span class="s1">&#39;Nordbrücke (Südseite)&#39;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="s1">&#39;id&#39;</span><span class="p">:</span> <span class="mi">100019721</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">:</span> <span class="s1">&#39;Nordbrücke (Nordseite)&#39;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="s1">&#39;id&#39;</span><span class="p">:</span> <span class="mi">100019724</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">:</span> <span class="s1">&#39;Estermannufer&#39;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="s1">&#39;id&#39;</span><span class="p">:</span> <span class="mi">100019726</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">:</span> <span class="s1">&#39;Brühler Straße&#39;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="s1">&#39;id&#39;</span><span class="p">:</span> <span class="mi">100027077</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">:</span> <span class="s1">&#39;Wilhelm-Spiritus-Ufer&#39;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="s1">&#39;id&#39;</span><span class="p">:</span> <span class="mi">100019727</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">:</span> <span class="s1">&#39;Straßenburger Weg&#39;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="s1">&#39;id&#39;</span><span class="p">:</span> <span class="mi">100019728</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">:</span> <span class="s1">&#39;Von-Sandt-Ufer&#39;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="s1">&#39;id&#39;</span><span class="p">:</span> <span class="mi">100027075</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">:</span> <span class="s1">&#39;John-J.-McCloy-Ufer&#39;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="s1">&#39;id&#39;</span><span class="p">:</span> <span class="mi">100019729</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">:</span> <span class="s1">&#39;Rhenusallee&#39;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="s1">&#39;id&#39;</span><span class="p">:</span> <span class="mi">100027076</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">:</span> <span class="s1">&#39;Hochwasserdamm Beuel&#39;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="s1">&#39;id&#39;</span><span class="p">:</span> <span class="mi">100019725</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">:</span> <span class="s1">&#39;Bröhltalbahnweg&#39;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span></code></pre></div><p>Die Daten der Kenndeybrücke und der Südbrücke gibt es jeweils getrennt (Südseite / Nordseite) und zusammengerechnet (gesamt = Südseite + Nordseite).
Somit hat die Liste insgesamt 17 Einträge für 15 verschiedene Zähler.</p>
<p>Darüber hinaus werden für alle Stationen die Vorbeifahrten auch getrennt der gefahrene Richtung bereitgehalten.
Dafür müsste ihr die dritte Ziffer einer ID durch eine <code>1</code> bzw. eine <code>2</code> ersetzen (fragt bloß nicht warum!?).
Für welche Richtung das dann jeweils genau steht, müsst ihr über die offiziellen Websites selbst herausfinden.
Da ich das aktuell nicht nutze, habe ich das bisher nicht zusammengetragen.</p>
<p>Und nun: Viel Spaß damit und gebt Bescheid, wenn ihr aus den Daten etwas bastelt.</p>
<p><em><strong>Update (29. März 2021):</strong> Die hier vorgestellte Methode funktioniert nicht mehr bei allen Zählstellen. Zumindest die Zählstelle am John-J.-McCloy-Ufer lässt sich nach einer Reparatur so nicht mehr abfragen. Ich werde beizeiten erklären, welche alternativen Möglichkeiten sich bieten.</em></p>
<p><em><strong>Update (26. Juli 2021):</strong> Seit einiger Zeit funktioniert dies so auch nicht mehr bei den anderen Zählstellen. Ich werde in Kürze ein Update verfügbar machen.</em></p>]]></content:encoded></item><item><title>Tastenmagie!</title><link>https://theus.name/post/tastenmagie/</link><pubDate>Wed, 29 May 2019 11:39:15 +0000</pubDate><guid>https://theus.name/post/tastenmagie/</guid><description>&lt;p&gt;Wer hin und wieder noch einen klassischen PC nutzt und an diesem eine klassische Tastatur angeschlossen hat, der kennt sie vielleicht schon: die &lt;code&gt;[S-Abf]&lt;/code&gt;-Taste oder im US-Tastatorlayout die &lt;code&gt;[SysReq]&lt;/code&gt;-Taste.&#10;Sie teilt sich den Platz gemeinsam mit der &lt;code&gt;[Druck]&lt;/code&gt;- oder &lt;code&gt;[Print]&lt;/code&gt;-Taste.&#10;Aber warum gibt es sie eigentlich? Zur Systemabfrage!&lt;/p&gt;</description><content:encoded><![CDATA[<p>Wer hin und wieder noch einen klassischen PC nutzt und an diesem eine klassische Tastatur angeschlossen hat, der kennt sie vielleicht schon: die <code>[S-Abf]</code>-Taste oder im US-Tastatorlayout die <code>[SysReq]</code>-Taste.
Sie teilt sich den Platz gemeinsam mit der <code>[Druck]</code>- oder <code>[Print]</code>-Taste.
Aber warum gibt es sie eigentlich? Zur Systemabfrage!</p>
<figure>
    
    <img
      src="https://theus.name/post/tastenmagie/Tastatur-S-Abf.png"
      width="779"
      height="438"
      alt="Ein Ausschnitt einer Tastatur: Die Druck/S-Abf-Taste ist rot markiert">
  
</figure>

<p>Interessant wird es jetzt vor allem für Linux-Nutzer.
Windows bietet entsprechende Funktionalität leider nicht.
Nützlich wird unsere Taste nämlich immer genau dann, wenn deine grafische Oberfläche einfriert und nicht einmal mehr ein Wechsel in die Konsole möglich ist.
Vielen Dank an Jonas, der mir eine kluge Tastenfolge gezeigt hat, mir der man seinen PC noch recht geregelt neu starten kann.</p>
<p>Dafür hältst du die ganze Zeit die <code>[Alt]</code>-Taste gedrückt.
Um die Magie zu beginnen, drückst du dann die <code>[S-Abf]</code>- bzw.
<code>[SysReq]</code>-Taste.
Jetzt werden die folgenden Tastatureingaben direkt an den Kernel weitergeleitet (schön weiter die <code>[Alt]</code>-Taste gedrückt halten).
Um deinen Rechner nun neu zu starten drückst du nun hintereinander <code>[R]</code>+<code>[E]</code>+<code>[I]</code>+<code>[S]</code>+<code>[U]</code>+<code>[B]</code>.</p>
<p>| R | unraw  | Nimmt der grafischen Oberfläche den Zugriff auf die Tastatur |
| E | term   | Sendet ein SIGTERM (abschließen &amp; beenden) an alle Prozesse außer Init |
| I | kill   | Sendet ein SIGKILL (abbrechen &amp; debuggen) an alle Prozesse außer Init |
| S | sync   | Schreibt alle noch nicht auf die Festplatte geschriebenen Daten aus dem Kernel-Festplatten-Cache |
| U | umount | Alle eingehängten Partitionen werden erst ausgehängt und dann nur-lesbar eingehängt |
| B | reboot | Fährt den Rechner sofort herunter und startet den Rechner neu.
|</p>
<p>Und wer zum Teufel soll sich das merken? Naja, dafür gibts eine ganze handvoll Merksprüche.
Ich merke mir das mit „busier“ rückwärts (der Rechner ist schließlich in solchen Momenten beschäftigter als normal).</p>]]></content:encoded></item><item><title>Podcasts</title><link>https://theus.name/post/podcasts/</link><pubDate>Sun, 28 Apr 2019 12:37:00 +0000</pubDate><guid>https://theus.name/post/podcasts/</guid><description>&lt;p&gt;Der Podcast-Hype ist im vollen Gange.&#10;Wer heute noch keine Podcasts hört, der hat (vielleicht) etwas verpasst.&#10;An dieser Stelle möchte ich euch meine Lieblingspodcasts vorstellen.&#10;Nicht alle sind streng auf Technik fokussiert, aber alle haben mich begeistert.&#10;Der Deal läuft übrigens so: Du liest dir durch, was &lt;em&gt;ich&lt;/em&gt; so toll finde und danach schreibst du in die Kommentare, was &lt;em&gt;du&lt;/em&gt; so toll findest.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Der Podcast-Hype ist im vollen Gange.
Wer heute noch keine Podcasts hört, der hat (vielleicht) etwas verpasst.
An dieser Stelle möchte ich euch meine Lieblingspodcasts vorstellen.
Nicht alle sind streng auf Technik fokussiert, aber alle haben mich begeistert.
Der Deal läuft übrigens so: Du liest dir durch, was <em>ich</em> so toll finde und danach schreibst du in die Kommentare, was <em>du</em> so toll findest.</p>
<h2 id="radfunk">Radfunk</h2>
<p>Der erste Podcast hat eher weniger mit Computern und Technik zu tun: <a href="https://www.deutschlandfunk.de/radfunk.3543.de.html">Radfunk</a> ist ein Podcast von Deutschlandfunk in welchem recht einfach und unkompliziert alle Themen rund um den Drahtesel angesprochen werden.
Dabei wird vor nichts halt gemacht: Es geht um fahrradgerechtere Städte, die richtige Kleidung im Winter oder Sicherheit beim Radfahren.
Als Gäste laden sich die beiden Moderatoren des Podcasts Paulus Müller und Klaas Reese, Unfallforscher, Stadtplaner, Polizisten, Verkehrswissenschaftler – und natürlich Radfahrer ein.</p>
<p>Die einzelnen Folgen des Podcasts sind ca. 1 Stunde lang.
Dabei braucht es für den Zuhörer kein Spezialwissen und man muss auch kein eingefleischter Radfahrer sein.
Müller und Reese holen auch in Sachen Radfahren unbedarfte Hörer ab.
In den Themen gehen sie trotzdem in die Tiefe, sodass auch ausgefuchste Radaktivisten ihren Spaß darin finden.</p>
<p>Aktuell hat Radfunk acht Folgen.
Die erste Folge kam im Mai 2018 heraus.
Es folgten bis Mitte Juni 2018 noch weitere fünf Folgen.
Im Dezember 2018 und Januar 2019 erschien noch jeweils eine weitere Folge.
Anfang April 2019 <a href="https://twitter.com/kornybrot/status/1111562185271136256">versprach mir Reese auf Twitter</a>, dass neue Folgen in Vorbereitung seien.</p>
<h2 id="rechtsbelehrung">Rechtsbelehrung</h2>
<p>Auch wenn sich der Titel dieses Podcasts recht trocken anhört, so sind die Themen immer interessant und, zu meiner Freude, recht häufig sehr internet- und techniklastig.
Kein Wunder, denn produziert wird der Podcast <a href="https://rechtsbelehrung.com/">Rechtsbelehrung</a> vom Radiojournalisten und Informatiker Marcus Richter und Rechtsanwalt Dr. Thomas Schwenke.</p>
<p>Während Schwenke die juristischen Hintergründe zu den Themen erklärt, holt Richter ihn immer wieder da ob, wo wir juristisch unbedarften Laien stehen: Ganz vorne.
In den Folgen thematisieren die zwei zähe Dinge wie die Datenschutzgrundverordnung (in drei langen Folgen) und das Kartellrecht, aber auch viele lockere Themen wie Freifunk oder den verschwundenen Glühweinbecher.
Langweilig wird es dabei nie und wenn Schwenke einmal nicht weiter weiß, dann lädt er sich Gäste ein, die weiterhelfen können.</p>
<p>Der Podcast erscheint erfreulich zuverlässig einmal im Monat.
Das klappt dank Internet sogar dann, wenn zwischen den beiden Podcastern mal die halbe Weltkugel liegt.
Meist nehmen die beiden jedoch gemeinsam auf.
Schwenke hegt den Wunsch die Podcasts auf eine Länge von einer Stunde zu begrenzen.
Gott sei dank klappt das aber nur selten.</p>
<h2 id="netzbasteln">Netzbasteln</h2>
<p>Noch ein Podcast von Deutschlandfunk bzw. dem Digitalsender Deutschlandfunk Nova.
<a href="https://netzbasteln.de/">Netzbasteln</a> ist allerdings kein echter Podcast, sondern <em>bloß</em> ein Zusammenschnitt der <a href="https://www.deutschlandfunknova.de/serien/netzbasteln">gleichnamigen Radioserie</a>.
Entsprechend professionell sind die Beiträge gestaltet, in welchen Netzbastler Moritz Metz dem Moderator Sebastian Sonntag seine neusten Life-Hacks und DIY-Basteleien vorstellt.
Und das ist immer wieder ziemlich kreativ: eine selbst gebaute Wärmebildkamera, eine Einführung ins Lockpicking oder der Hipsterfahrradwandhalter.
Immer gerne mit dabei: WLAN – in Form eines <a href="https://de.wikipedia.org/wiki/ESP8266">ESP8266</a>.</p>
<p>Netzbasteln erscheint alle zwei Wochen in der Radiosendung <a href="https://www.deutschlandfunknova.de/dein-sonntag">Dein Sonntag</a> und danach auch online.
Die Beiträge dauern gute 20 Minuten.</p>
<h2 id="logbuchnetzpolitik">Logbuch:Netzpolitik</h2>
<p>Im <a href="https://logbuch-netzpolitik.de">Logbuch:Netzpolitik</a> geht es ganz genau um das: Netzpolitik.
Linus Neumann versucht im Team mit <a href="https://metaebene.me/">Tim Pritlove</a>, Andre Meister und Thomas Lohninger das politische Geschehen rund um das Digitale und Internet einzuordnen.</p>
<p>Das Team selbst <a href="https://logbuch-netzpolitik.de/about">erhebt für sich den Anspruch</a> „weitgehend neutral und unaufgeregt“ einen Einblick in Themen wir Vorratsdatenspeicherung, Upload-Filter oder Wahlcomputer zu geben.
Jedoch bleiben die Podcasts nicht nur berichtend.
Gerade Linus kommentiert das aktuelle Geschehen häufig mit einer guten Portion Zynismus – vielleicht ist das auch die einzige Möglichkeit mit den Wirrungen der Politik umzugehen.</p>
<p>Der Podcast erscheint ungefähr einmal die Woche.
Manchmal aber auch nicht und dann aus tagesaktuellen Anlässen widerum gleich mehrmals.
Die Länge eines einzelnen Podcast liegt in der Regel bei circa 2 Stunden.</p>
<h2 id="freak-show">Freak Show</h2>
<p>Die <a href="https://freakshow.fm">Freak Show</a> ist ein Plauderpodcast über „Leben mit Technik im 21. Jahrhundert“.
Das <a href="https://freakshow.fm/team">sechsköpfige Team</a> um Tim Pritlove, der auch das Logbuch:Netzpolitik herausbringt, spricht im lockeren Ton über eben all das, was es in letzter Zeit beschäftigt hat.
Dabei geht es um wirklich alles, was technikbegeisterte Menschen interessieren könnte.
Egal ob Streetscooter, DNS, Küchenmaschinen, Elektroautos oder IoT.
Außerdem immer wieder Thema: die Produkte von Apple.
Das hat auch seinen Grund, denn die Freak Show hieß früher noch <em>mobileMac</em>.</p>
<p>Aufgenommen wird die Show meistens jeden zweiten Mittwoch um 20:00 Uhr (auch wenn die Website von 19:30 Uhr spricht) und kann auch <a href="https://freakshow.fm/live">live im Stream</a> verfolgt werden.
Wer lange Podcasts genießt, der ist bei der Freak Show genau richtig.
Eine einzelne Folge dauert meist circa 4 Stunden.</p>
<h2 id="methodisch-inkorrekt">Methodisch inkorrekt!</h2>
<p><a href="https://minkorrekt.de/">Methodisch inkorrekt!</a> ist ein Wissenschaftspodcast von Nicolas Wöhrl und Reinhard Remfort.
Die beiden Physiker erklären in lockerer Weise physikalische Effekte und andere vorwiegend, aber nicht ausschließlich naturwissenschaftliche Themen.
Daneben küren sie regelmäßig den <em>Schwurbel der Woche</em> aus dem Bereich der <a href="https://de.wikipedia.org/wiki/Pseudowissenschaft">Pseudowissenschaften</a> und stellen ein sogenanntes <em>China-Gadget</em> vor.
Außerdem ist immer ein <em>Experiment der Woche</em> dabei.</p>
<p>Die einzelnen Folgen erscheinen jeden zweiten Dienstag und sind mit drei bis vier Stunden eher lang.
Wer die beiden einmal live erleben möchte, hat dieses Jahr außerdem in einer der <a href="http://minkorrekt.de/minkorrekt-live/">Live-Shows</a> der aktuellen Deutschland-Tournee die Möglichkeit dazu.</p>]]></content:encoded></item><item><title>Vergiss es!</title><link>https://theus.name/post/vergiss-es/</link><pubDate>Thu, 28 Mar 2019 18:25:13 +0000</pubDate><guid>https://theus.name/post/vergiss-es/</guid><description>&lt;p&gt;In Zeiten fast unbegrenzten Speicherplatzes für E-Mails, Fotos, Messenger-Nachrichten und Co tendieren wir immer mehr dazu, alles zu speichern, was irgendwie geht.&#10;Experten fordern von Unternehmen das Prinzip der &lt;em&gt;Datensparsamkeit&lt;/em&gt;.&#10;Fangen wir doch einmal bei uns selbst damit an!&lt;/p&gt;</description><content:encoded><![CDATA[<p>In Zeiten fast unbegrenzten Speicherplatzes für E-Mails, Fotos, Messenger-Nachrichten und Co tendieren wir immer mehr dazu, alles zu speichern, was irgendwie geht.
Experten fordern von Unternehmen das Prinzip der <em>Datensparsamkeit</em>.
Fangen wir doch einmal bei uns selbst damit an!</p>
<h2 id="e-mail">E-Mail</h2>
<p>Mein erstes E-Mail-Postfach habe ich irgendwann als Jugendlicher auf den Tipp meiner Schwestern bei web.de angelegt.
Wie viel Speicherplatz es damals gab, kann ich gar nicht mehr sagen.
Alte web.de-Nutzer mussten aber lange Zeit mit 12 MB klarkommen und vor über 15 Jahren war es sicherlich noch weniger.</p>
<p>Man kam nicht darumherum alte E-Mails zu löschen.
Ansonsten kam es (und dazu kam es öfter) dazu, dass E-Mails an den Absender mit der Meldung „Postfach voll“ zurückgingen.
Heute passiert das so schnell nicht mehr.
Google bietet bei seinem Postfach bis zu 15 GB Speicherplatz, Posteo hat 2 GB im Angebote und auch web.de hat auf 2 GB aufgestockt.</p>
<p>Vielen Leuten (und so auch mir) ist das Löschen von E-Mails lästig und lassen es dann einfach sein, denn es ist ja Platz genug.
Das bleibt aber nicht ohne negative Folgen: Bei einem Einbruch in das Mailpostfach hat der Angreifer Zugriff auch auf private Nachrichten, die schon uralt sind, deren Inhalt aber noch unangenehme Folgen haben kann.</p>
<p>Wenn du alte Nachrichten nicht einfach löschen möchtest und du einen lokalen Mail-Client wie Mozilla Thunderbird nutzt, kannst du mit einem kleinen AddOn automatisiert alte E-Mails aus der Cloud holen.
<a href="https://addons.thunderbird.net/de/thunderbird/addon/awesome-auto-archive/">Awesome Auto Archive</a> bietet einem die Möglichkeit E-Mails nach selbst definierbaren Regeln zu archivieren.
Gibt man hier einen lokalen Ordner an, dann verschwinden alte Nachrichten automatisch vom Server des Mail-Anbieters, aber bleiben lokal auf dem Rechner vorhanden (Backups nicht vergessen, wenn euch der Inhalt dieser Mails wirklich wichtig ist!).</p>
<h2 id="messenger-dienste">Messenger-Dienste</h2>
<p>Auch Messenger-Dienste häufen über die Zeit viele Daten an.
Darüberhinaus werden immer noch viele Android-Smartphones nur über kurze Zeit mit Updates versorgt.
Das Risiko sich Schadsoftware einzufangen, ist also durchaus gegeben.</p>
<h3 id="whatsapp">WhatsApp</h3>
<p>Leute, ich hab keine Ahnung von WhatsApp.
Ich habe die App vor vielen Jahren schon von meinem Smartphone geworfen und auch nicht wieder installiert, als die durchaus gute Ende-zu-Ende-Verschlüsselung eingeführt wurde.
Ich habe es nicht bereut, als Facebook schließlich nach dem Messenger griff.</p>
<p>Erstaunlicherweise habe ich bei ein wenig Recherche herausgefunden, dass es bei WhatsApp keine Möglichkeit gibt, alte Nachrichten automatisch löschen zu lassen.
Was du tun kannst, ist einzelne Chats oder in den Einstellungen auch alle Chats zu löschen.
Wenn du einen Weg weist, wie man auf einfachen Weg nur alte Nachrichten löschen kann, dann schreib mir in die Kommentare.</p>
<h3 id="signal">Signal</h3>
<figure class="push-right w-50">
    
    <img
      src="https://theus.name/post/vergiss-es/Screenshot_Signal_Kuerzen_von_Nachrichten.png"
      width="1080"
      height="1215"
      alt="Kürzen von Nachrichten in Signal">
  
</figure>

<p>Wenn du den freien und datenschutzfreundlichen Messenger <em>Signal</em> nutzt, kannst du mit einer einfachen Angabe in den Einstellungen alte Nachrichten automatisch löschen lassen.
Dabei löscht Signal nicht anhand des Alters der Nachrichten, sondern anhand der Menge.
Überschreitet eine einzelne Unterhaltung die Höchstgrenze, fliegt die älteste Nachricht raus.
Die Funktion scheint damit eher mit dem Blick auf begrenzten Speicherplatz erschaffen worden zu sein.</p>
<p>Dafür gehst du in den <em>Einstellungen</em> auf <em>Unterhaltungen und Medieninhalte → Kürzen von Unterhaltungen</em> und aktivierst die Option <em>Alte Nachrichten löschen</em>.
Direkt darunter kannst du die Höchstzahl an Nachrichten je Unterhaltungen frei einstellen.</p>
<h3 id="threema">Threema</h3>
<figure class="push-right w-50">
    
    <img
      src="https://theus.name/post/vergiss-es/Screenshot_Threema_Speicherplatzmanagement.png"
      width="1080"
      height="1744"
      alt="Speichermanagement in Threema">
  
</figure>

<p>Der schweizer Messenger <em>Threema</em> verfolgt ein anderes Konzept als Signal.
Aber auch hier scheint der Fokus auf begrenztem Speicher zu liegen.
So heißt denn auch die Option, die man in den <em>Einstellungen</em> unter <em>Medien &amp; Speicher</em> aufrufen muss, <em>Speichermanagement</em>.
Dort kann man entweder nur Bilder und Dateien, aber auch Nachrichten, die älter als eine gewünschte Zeitspanne sind, löschen lassen.
Im Gegensatz zu Signal geht das hier nicht automatisch, sondern immer nur manuell.</p>
<h2 id="cloud">Cloud</h2>
<p>Was liegt bei dir eigentlich so in der – Achtung: Buzzword! – Cloud herum? Also ganz konkret bei Diensten wie <em>Dropbox</em>, <em>Google Drive</em> oder <em>iCloud</em>.
Schau dich doch mal um, was da an Daten so herumliegt und räume auf.
Alles, was nicht unbedingt in die Cloud muss, kannst du entweder direkt löschen (auch den Papierkorb der Cloud leeren) oder auf deinem lokalen Gerät speichern.
Und in Zukunft willst du deine Daten vielleicht <a href="https://cryptomator.org/">verschlüsselt</a> in die Cloud legen?</p>
<h2 id="ist-da-noch-mehr">Ist da noch mehr?</h2>
<p>Bestimmt hast du auch anderswo noch Daten liegen.
Es schadet sicher auch nicht, deine lokalen Daten auf PC, Notebook und Smartphone hin und wieder aufzuräumen.
Alles, was du nicht mehr brauchst, ab in den Papierkorb! Hast du noch Ideen, wo man hin und wieder sonst so aufräumen sollte, dann schreib mir in die Kommentare.</p>]]></content:encoded></item><item><title>Untar that Shit!</title><link>https://theus.name/post/untar-that-shit/</link><pubDate>Sat, 02 Mar 2019 11:32:39 +0000</pubDate><guid>https://theus.name/post/untar-that-shit/</guid><description>&lt;p&gt;Archive – am besten komprimiert – sind eine feine Sache um große Mengen an Dateien auszutauschen.&#10;Normale Menschen nutzen für so etwas zip-Dateien.&#10;Unter Linux aber hat sich ein anderes Format durchgesetzt: tar.&#10;Doch wie packt man auf der Konsole so ein Ding aus?&lt;/p&gt;</description><content:encoded><![CDATA[<p>Archive – am besten komprimiert – sind eine feine Sache um große Mengen an Dateien auszutauschen.
Normale Menschen nutzen für so etwas zip-Dateien.
Unter Linux aber hat sich ein anderes Format durchgesetzt: tar.
Doch wie packt man auf der Konsole so ein Ding aus?</p>
<p>tar-Archive sind nicht zwingend komprimiert.
Die Komprimierung ist optional.
Deswegen findet man Dateien mit der Endung <em>.tar.gz</em> genauso wie <em>.tar.bz2</em> oder <em>.tar.xz</em>.
Und so entpackt man die Dinger in der Konsole:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">$ tar xvzf archive.tar.gz
</span></span><span class="line"><span class="cl">$ tar xvjf archive.tar.bz2
</span></span><span class="line"><span class="cl">$ tar xvJf archive.tar.xz
</span></span></code></pre></div><p>Ganz ehrlich? Das kann sich wirklich keiner merken.
Abhilfe schafft hier das kleine Programm <a href="https://brettcsmith.org/2007/dtrx/">dtrx</a> (<em>Do The Right eXtraction</em>) von Brett Smith.
Also schnell installieren:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="c1"># Ubuntu</span>
</span></span><span class="line"><span class="cl">$ sudo apt install dtrx
</span></span><span class="line"><span class="cl"><span class="c1"># Arch Linux</span>
</span></span><span class="line"><span class="cl">$ yay -S dtrx
</span></span></code></pre></div><p>Für Arch Linux liegen die Pakete im <a href="https://wiki.archlinux.org/index.php/Arch_User_Repository">AUR</a>.
Je nach verwendetem Packman Wrapper für das AUR musst du also noch <code>yay</code> durch den von dir installierten Wrapper ersetzen (bspw.
<a href="https://aur.archlinux.org/packages/pakku/">pakku</a>, <a href="https://aur.archlinux.org/packages/pacaur/">pacaur</a> oder <a href="https://aur.archlinux.org/packages/trizen/">trizen</a>).</p>
<p>dtrx kann übrigens auch <em>zip</em>, <em>7z</em>, <em>rar</em>, <em>rpm</em>, <em>deb</em> und vieles mehr.
Von nun an braucht es also nur noch:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">$ dtrx archive.tar.gz
</span></span></code></pre></div>]]></content:encoded></item><item><title>Awesome-Selfhosted</title><link>https://theus.name/post/awesome-selfhosted/</link><pubDate>Sat, 16 Feb 2019 15:58:06 +0000</pubDate><guid>https://theus.name/post/awesome-selfhosted/</guid><description>&lt;p&gt;Bei meiner Suche nach Software, die ich auf Grund meines guten Vorsatzes im Laufe des Jahres ersetzen will, bin ich auf eine grandiose Liste von Edward Dickson auf GitHub gestoßen, die ich euch natürlich nicht vorenthalten will.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Bei meiner Suche nach Software, die ich auf Grund meines guten Vorsatzes im Laufe des Jahres ersetzen will, bin ich auf eine grandiose Liste von Edward Dickson auf GitHub gestoßen, die ich euch natürlich nicht vorenthalten will.</p>
<p>In dem GitHub-Projekt <a href="https://github.com/Kickball/awesome-selfhosted">awesome-selfhosted</a> findet sich zu wirklich fast allen Kategorien Software für Netzwerkdienste und Webanwendungen.
Bei allen aufgeführten Projekten wird zudem die Lizenz (z. B. GPL, MIT…) und die zugrunde liegende Programmiersprache angegeben.</p>
<p>Durch die Liste der <a href="https://github.com/Kickball/awesome-selfhosted#note-taking-and-editors">Note-taking and Editors</a> und der <a href="https://github.com/Kickball/awesome-selfhosted#user-content-task-managementto-do-lists">Task management/To-do lists</a> muss ich mich auf jeden Fall erst einmal durcharbeiten, denn besonders erwähnenswerte Programme werden in der Liste leider nicht extra hervorgehoben. ¯_(ツ)_/¯</p>]]></content:encoded></item><item><title>MHPG mit Auto-Copy</title><link>https://theus.name/post/mhpg-mit-auto-copy/</link><pubDate>Thu, 10 Jan 2019 10:07:04 +0000</pubDate><guid>https://theus.name/post/mhpg-mit-auto-copy/</guid><description>&lt;p&gt;Mein &lt;a href="https://theus.name/pwd/"&gt;Mobiler Hash-Passwort-Generator&lt;/a&gt; (MHPG) hat schon vor zwei Wochen ein Update bekommen.&#10;Die neue Auto-Copy-Funktion bietet zusätzlichen Komfort und auch mehr Sicherheit, denn die erzeugten Passwörter müssen so nicht mehr im Klartext angezeigt werden.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Mein <a href="https://theus.name/pwd/">Mobiler Hash-Passwort-Generator</a> (MHPG) hat schon vor zwei Wochen ein Update bekommen.
Die neue Auto-Copy-Funktion bietet zusätzlichen Komfort und auch mehr Sicherheit, denn die erzeugten Passwörter müssen so nicht mehr im Klartext angezeigt werden.</p>
<p>Den MHPG habe ich im <a href="https://theus.name/post/noch-bessere-passwoerter/">Februar 2017 vorgestellt</a>.
Er basiert auf einer Idee von Tim Anderson und generiert dir mithilfe eines Master-Passworts dienstespezifische Passwörter, sodass mehrfach genutzte, gleiche Passwörter der Vergangenheit angehören.
Das Tool ist komplett in HTML und JavaScript geschrieben, es werden keine Daten an meinen Server gesendet.</p>
<p>Mithilfe der neuen Auto-Copy-Funktion werden erzeugte Passwörter sofort in deine Zwischenablage kopiert.
So entfällt lästiges manuelles Kopieren der Passwörter.
Zusätzlich ermöglicht dies, dass das Passwort nicht mehr im Klartext ausgegeben werden muss.
Stattdessen wird das Ergebnis nur noch mit den klassischen Punkten angezeigt.
So kann niemand das Passwort mitlesen, wenn er dir über die Schultern schaut.</p>
<p>Möchtest du das Passwort doch einmal im Klartext sehen, so kannst du es dir mit einem Klick auf das Augen-Symbol ganz einfach anzeigen lassen (und ggf. auch manuell kopieren, falls es mit der Automatik mal nicht klappen sollte).</p>]]></content:encoded></item><item><title>Gute Vorsätze: Weg mit dem NAS!</title><link>https://theus.name/post/gute-vorsaetze-weg-mit-dem-nas/</link><pubDate>Wed, 02 Jan 2019 11:17:19 +0000</pubDate><guid>https://theus.name/post/gute-vorsaetze-weg-mit-dem-nas/</guid><description>&lt;p&gt;Hast du dir auch gute Vorsätze für das neue Jahr gemacht?&#10;Ich tue das eigentlich nie, aus gutem Grund:&#10;Denn Vorsätze müssen gut formuliert und geplant sein, sonst hält man sie ja doch nicht ein.&#10;Mein guter Vorsatz für dieses Jahr:&#10;Weg mit meinem NAS!&lt;/p&gt;</description><content:encoded><![CDATA[<p>Hast du dir auch gute Vorsätze für das neue Jahr gemacht?
Ich tue das eigentlich nie, aus gutem Grund:
Denn Vorsätze müssen gut formuliert und geplant sein, sonst hält man sie ja doch nicht ein.
Mein guter Vorsatz für dieses Jahr:
Weg mit meinem NAS!</p>
<p>Ich besitze seit vielen Jahren (es dürften jetzt schon circa sieben sein) ein <em>Network Attached Storage</em> (NAS).
Der <a href="https://global.download.synology.com/download/Document/DataSheet/DiskStation/11-year/DS211j/Synology_DS211j_Data_Sheet_deu.pdf">DS211j von Synology</a> ist inzwischen aber recht betagt und war schon bei der Anschaffung ein eher kleines Modell.
Aber es beinhaltet immer noch alle Vorteile, die man von einem NAS mit Einschüben für zwei Festplatten von jeweils maximal 3 TB so erwarten kann.
Ansonsten fällt es vor allem durch schwache Leistung auf: 1,2 GHz Single-Core und vor allem 128 MB RAM machen den alten Hasen langsam.</p>
<h2 id="weg-mit-dem-strommonster">Weg mit dem Strommonster</h2>
<p>Was läge also näher als den kleinen weißen Kasten, der so munter in meinem Wohnzimmer flackert endlich auszumisten?
Immerhin verbraucht das Ding bis zu 25 W im Betrieb und 10 W im Ruhezustand.
Mein (Öko-)Stromanbieter nimmt ca. 24 Cent pro kWh.
Das kostet, wenn mich meine Rechenkünste nicht verlassen haben, also irgendwas zwischen 21,02 € (Dauer-Ruhezustand) und 52,56 € (Dauer-Betrieb) im Jahr.
Das belastet nicht nur die Umwelt, sondern auch meinen Geldbeutel mehr als ich gedacht habe.</p>
<p>Die Dienste, die auf dem NAS laufen können eigentlich alle ausgelagert werden:
Auf meinen Raspberry Pi, der eh Tag und Nacht läuft, um mir <a href="https://pi-hole.net/">Werbetracker vom Hals zu halten</a> oder auf meinem vServer, der dann auch ordentliche Rechenleistung und vor allem Netzanbindung hat.</p>
<h2 id="welche-dienste-laufen-auf-dem-nas">Welche Dienste laufen auf dem NAS?</h2>
<p>Auf dem NAS gibt es eine Vielzahl von möglichen Diensten, die einfach (und per Klick) installiert und administriert werden können.
Alles per Weboberfläche, die allerdings sehr langsam und daher mühsam zu bedienen ist.
Um herauszufinden, welche Dienste bei mir aktuell laufen, die dann in Zukunft entweder vom Pi oder vom vServer übernommen werden müssen, öffne ich das sogenannte „Paket-Zentrum“.
Dort finde ich folgende installierte Pakete/Anwendungen:</p>
<ul>
<li><strong>Audio-Station</strong></li>
<li><strong>File-Station</strong></li>
<li><strong>WebDAV Server</strong></li>
<li><strong>CardDAV Server</strong></li>
<li><strong>Text-Editor</strong></li>
<li><strong>Note Station</strong></li>
<li><em>Python Module</em></li>
<li><em>PHP 7.0</em></li>
<li><em>Universal Search</em></li>
<li><em>USB Copy</em></li>
<li><em>Speicher-Analysator</em></li>
</ul>
<p>Dienste, die in der Liste fett markiert sind, muss ich in den nächsten Wochen durch Alternativen ersetzen. Alle anderen sind eher NAS-spezifisch.</p>
<h3 id="audio-station">Audio-Station</h3>
<p>In der Audio-Station liegt meine ganze Musiksammlung.
Da ich mich bisher noch erfolgreich von Streaming-Diensten wie Spotify oder Deezer verwehrt habe, ermöglicht mir dieses Stück Software auch von unterwegs mit meinem Smartphone auf all die Musik zuzugreifen, die ich gern höre.</p>
<h3 id="file-station">File-Station</h3>
<p>Nix Cloud oder so.
File-Station ermöglicht den Zugriff auf Dateien über das Netzwerk.
Per Samba, NFS oder auch dem Browser.
Den ganzen Wust an Dateien, die man eben nicht auf der Platte von seinem Compute haben möchte.
Ewig alte Fotos, irgendwelche Backups und Hörbücher.</p>
<h3 id="webdav-server">WebDAV Server</h3>
<p>Den WebDAV Server nutze ich, um meine Kalender online zu verwalten.</p>
<h3 id="carddav-server">CardDAV Server</h3>
<p>Mit dem CardDAV Server halte ich meine Kontakte über verschiedene Geräte wie Smartphone, Notebook und Co synchronisiert.</p>
<h3 id="text-editor--note-station">Text-Editor &amp; Note Station</h3>
<p>Den Text-Editor habe ich wahrscheinlich nur ein handvoll Male benutzt.
Die Note Station dagegen nutze ich auf meinem Smartphone dafür wofür man eine Notiz-Anwendung eben nutzt.
Schon einmal Weihnachtsgeschenkideen für Eltern, Geschwister und Freundin notieren.</p>
<h2 id="und-nun">Und nun?</h2>
<p>Einige von den Diensten lassen sich wirklich ganz einfach ersetzen.
Für Tipps über empfehlenswerte Software freue ich mich trotzdem.
Vollständig in Rente wird das NAS übrigens wohl eher nicht gehen.
Ich werde es wohl jede Nacht einmal aufwecken, um meine Backups draufzuspielen.</p>]]></content:encoded></item><item><title>Wettervorhersage mit cURL</title><link>https://theus.name/post/wettervorhersage-mit-curl/</link><pubDate>Thu, 20 Dec 2018 13:20:48 +0000</pubDate><guid>https://theus.name/post/wettervorhersage-mit-curl/</guid><description>&lt;p&gt;In meinem letzten Artikel habe ich ja schon gezeigt, wie man mit dem Kommandozeilen-Tool &lt;em&gt;cURL&lt;/em&gt; recht einfach seine öffentliche IP-Adresse bestimmen kann.&#10;Mit cURL geht aber noch viel mehr: Wettervorhersagen zum Beispiel!&lt;/p&gt;&#10;&lt;p&gt;Und ich muss ehrlich sagen, dass ich davon ziemlich begeistert bin.&#10;Ich nutze das schon seit ein paar Jahren und es ist an der Zeit das hier einmal festzuhalten.&#10;Alles, was man dafür tun muss, ist per cURL die Seite &lt;em&gt;wttr.in&lt;/em&gt; aufzurufen.&#10;Also schnell ins Terminal getippt:&lt;/p&gt;</description><content:encoded><![CDATA[<p>In meinem letzten Artikel habe ich ja schon gezeigt, wie man mit dem Kommandozeilen-Tool <em>cURL</em> recht einfach seine öffentliche IP-Adresse bestimmen kann.
Mit cURL geht aber noch viel mehr: Wettervorhersagen zum Beispiel!</p>
<p>Und ich muss ehrlich sagen, dass ich davon ziemlich begeistert bin.
Ich nutze das schon seit ein paar Jahren und es ist an der Zeit das hier einmal festzuhalten.
Alles, was man dafür tun muss, ist per cURL die Seite <em>wttr.in</em> aufzurufen.
Also schnell ins Terminal getippt:</p>
<pre tabindex="0"><code>$ curl wttr.in
</code></pre><p>Und so sieht das dann aus:</p>
<figure>
    
    <img
      src="https://theus.name/post/wettervorhersage-mit-curl/wttr.png"
      width="1252"
      height="768"
      alt="curl wttr.in – Eine zeichenbasierte Wettervorhersage für heute, morgen und übermorgen">
  
  <figcaption>Eine wunderhübsche Wettervorhersage in ANSI</figcaption>
</figure>

<p>Das Tool versucht den Standort des Nutzers über die IP zu bestimmen.
Das funktioniert aus dem Uni-Netz zum Beispiel immer super.
Zu Hause hat mich das Tool aber von Bonn circa 20 km weiter nördlich nach Köln verschoben.
Das kann man mit der Angabe des Ortes in der URL aber einfach korrigieren:</p>
<pre tabindex="0"><code>$ curl wttr.in/Bonn
</code></pre><figure>
    
    <img
      src="https://theus.name/post/wettervorhersage-mit-curl/wttr_bonn.png"
      width="1255"
      height="768"
      alt="curl wttr.in/Bonn">
  
  <figcaption>Schon zeigt sich mir die Vorhersage für das wunderhübsche Bonn</figcaption>
</figure>

<p>Statt Städtenamen kann man auch die dreistelligen internationalen Kürzel für Flughäfen angeben.
Da komme ich natürlich nicht drumherum, mir einmal das Wetter für den BER anzeigen zu lassen.</p>
<figure>
    
    <img
      src="https://theus.name/post/wettervorhersage-mit-curl/wttr_ber.png"
      width="1255"
      height="769"
      alt="curl wttr.in/ber">
  
  <figcaption>Das Wetter für den BER – das bisschen Regen kann schon mal nicht der Grund für die massiven Verzögerungen beim Bau sein</figcaption>
</figure>

<p><em>wttr.in</em> kannst du natürlich neben cURL auch mit <em>httpie</em> oder ähnlichen Tools nutzen.
Außerdem unterstützt <em>wttr.in</em> auch die Ausgabe im Browser oder sogar als Bild:</p>
<pre tabindex="0"><code>$ wget wttr.in/Bonn.png
</code></pre><p>Das heruntergeladene Bild könnt ihr dann mit einem Bildbetrachter eurer Wahl anschauen, auf eure Webseite einbinden oder sogar ausdrucken (<a href="https://twitter.com/search?q=%23internetausdrucker">#internetausdrucker</a>).</p>
]]></content:encoded></item></channel></rss>