Bludit Blowfish Theme: Eigene Optimierungen
Fünf Problemchen, die mir bei der Nutzung von Blowfish aufgefallen sind — von der Seite, die beim Wechsel zu Catppuccin unter einem wegrutscht, bis zum Fortschrittsbalken, der sich im Light-Schema unsichtbar macht. Und wie sich das reparieren lässt, ohne eine einzige Theme-Datei anzufassen.
Ausgangslage
Blowfish ist mein Lieblingstheme für Bludit. Was es mitbringt und wie es eingerichtet wird, steht im Beitrag Vorstellung und Einrichtung. Im Zuge der Einrichtung sind mir allerdings fünf Problemchen aufgefallen, dazu zwei kleinere Beobachtungen. Alle sind reproduzierbar, für alle zeige ich, wie ich sie von außen gelöst habe und wie ein sauberer Fix im Theme aussehen würde. Die Zahlen stammen aus einem Testbeitrag mit acht Codeblöcken (Bash und YAML) und rund fünfzig Überschriften.
Die fünf Problemchen im Detail
Der Wechsel zu Catppuccin verschiebt die Seite unter einem weg
Artikel öffnen, reinscrollen, auf Catppuccin umschalten — der Text springt. Zurück zu einem anderen Schema, und er springt wieder zurück. Bei keinem der anderen vier Wechsel passiert das.
Der Grund sitzt nicht im Theme-Code, sondern in den Paletten-Dateien: Vier der fünf highlight.js-Paletten bringen den Standardblock pre code.hljs { display: block; overflow-x: auto; padding: 1em } mit, catppuccin-mocha.min.css enthält dagegen nur Token-Farben. style.css neutralisiert Padding und Hintergrund zwar mit !important, aber nichts setzt overflow-x zurück — unter Catppuccin ist der Codeblock also kein Scroll-Container mehr und verliert etwas Höhe. In meinem Testbeitrag ging der erste Block dadurch von 1003 auf 998 Pixel zurück, über acht Blöcke kamen so rund 40 Pixel mitten im Lesefluss zusammen. Zur Kontrolle habe ich beide Eigenschaften einzeln getestet: display: block allein änderte nichts, overflow-x: auto allein stellte die ursprüngliche Höhe exakt wieder her — das war also tatsächlich die entscheidende Zeile.
In css/style.css lässt sich das robuster lösen, unabhängig davon, was eine einzelne Palette mitbringt:
pre code.hljs {
padding: 0 !important;
background: transparent !important;
+ overflow-x: auto;
}
Damit kann auch eine künftige Palette diesen Sprung nicht wieder einschleppen.
Das mobile Inhaltsverzeichnis markiert die aktuelle Überschrift nicht
Auf dem Desktop wandert die Markierung in der Seitenleiste beim Scrollen sauber mit, im Drawer bleibt jede Überschrift unmarkiert. In js/blowfish.js entsteht der Drawer per list.cloneNode(true) aus der Seitenleiste, doch updateActive() sucht seine Links bisher nur innerhalb von #toc-nav — die Kopie bekommt von der Änderung also nie etwas mit:
- var links = tocNav.querySelectorAll('.toc-link');
- Array.prototype.forEach.call(links, function (a) { a.classList.remove('active'); });
- if (active) {
- var link = tocNav.querySelector('a[href="#' + active.id + '"]');
- if (link) link.classList.add('active');
- }
+ var links = document.querySelectorAll('.toc-link');
+ var href = active ? '#' + active.id : null;
+ Array.prototype.forEach.call(links, function (a) {
+ a.classList.toggle('active', a.getAttribute('href') === href);
+ });
Die Einschränkung auf .toc-link statt auf a[href="#id"] ist dabei wichtig: Das Theme hängt an jede Überschrift einen #-Anker mit demselben href, der sonst mitmarkiert würde. Der Vergleich per getAttribute statt eines zusammengebauten Selektors hat noch einen zweiten Grund — eine selbst vergebene Überschriften-ID mit einem Anführungszeichen darin würde querySelector sonst zum Absturz bringen. Für sich genommen macht dieser Patch die Markierung aber nur fett; die Farbe bleibt die alte.
Sidebar und Drawer folgen unterschiedlichen Farblogiken
Selbst wenn die Markierung sitzt, stimmt ihre Farbe noch nicht: .toc-drawer .toc-link steht in style.css hinter .toc-link.active und ist genauso spezifisch, gewinnt also — ein aktiver Eintrag im Drawer behält seine Ruhefarbe. Die beiden Listen meinen mit ihren Farben zudem nicht dasselbe:
| Seitenleiste | Drawer | |
|---|---|---|
| Ruhezustand | --text-muted |
--text-secondary |
| Hover | --text-primary |
--accent |
| Aktiv | --accent |
— |
Im Light-Schema fällt das kaum auf, weil --accent und --text-primary beide ein nahes Schwarz sind. In Nord, Dracula und Catppuccin ist es deutlicher: Der Drawer hovert auf die Akzentfarbe, die Seitenleiste auf nahes Weiß. Am saubersten lässt man die Drawer-Liste einfach die vorhandenen .toc-link-Regeln erben, statt eigene zu definieren:
.toc-drawer .toc-list { list-style: none; padding: 0; margin: 0; }
-.toc-drawer .toc-link { display: block; padding: 0.5rem 0; color: var(--text-secondary); }
-.toc-drawer .toc-link:hover { color: var(--accent); text-decoration: none; }
+.toc-drawer .toc-link { display: block; padding: 0.5rem 0; }
Sollte die dunklere Ruhefarbe im Drawer Absicht gewesen sein — auf dem Telefon liest sie sich durchaus angenehmer —, reicht es genauso, nur Hover und Aktiv anzugleichen und --text-secondary zu behalten:
.toc-drawer .toc-link:hover { color: var(--text-primary); text-decoration: none; }
.toc-drawer .toc-link.active { color: var(--accent); font-weight: 600; }
Der Fortschrittsbalken ist im Light-Schema unsichtbar
.reading-progress-fill wird mit var(--accent) gezeichnet und liegt über der Navigationsleiste. Im Light-Schema ist --accent aber #1f1f1f — exakt die Farbe der Leiste darunter. Schwarz auf Schwarz. In den vier dunklen Schemata ist der Akzent hell oder bunt, dort fällt es nicht auf.
Der Balken liegt auf der Chrome-Leiste, nicht auf dem Seitenhintergrund, darum bekommt er am saubersten einen eigenen Token — im Light-Block --progress-accent: #ffffff, in jedem dunklen Block --progress-accent: var(--accent):
/* :root — Light */
--chrome-text-hover: #ffffff;
+ --progress-accent: #ffffff;
/* und in jedem dunklen Farbschema-Block */
+ --progress-accent: var(--accent);
.reading-progress-fill {
- background: var(--accent);
+ background: var(--progress-accent, var(--accent));
}
Das mobile Menü lässt sich nur über den Hamburger schließen
Das Inhaltsverzeichnis am Telefon schließt sich, wenn man daneben tippt. Das Navigationsmenü nicht: kein Klick außerhalb, kein Escape, man muss den Button noch einmal exakt treffen. setOpen() existiert in js/blowfish.js bereits, und der Theme-Picker in derselben Datei löst ein ähnliches Problem schon nach demselben Muster — die Ergänzung fügt sich in dieselbe IIFE ein:
document.addEventListener('click', function (e) {
if (!target.classList.contains('show')) return;
if (toggler.contains(e.target)) return;
if (target.contains(e.target)) return;
setOpen(false);
});
document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape' && e.key !== 'Esc') return;
if (!target.classList.contains('show')) return;
setOpen(false);
toggler.focus();
});
Zwei kleinere Sachen
Codeblöcke und Inline-Code laufen farblich auseinander. Das Theme definiert pro Schema ein --code-text, ein hervorgehobener Codeblock nimmt seine Grundfarbe aber aus der highlight.js-Palette. In vier von fünf Schemata weichen die beiden Werte leicht voneinander ab: Light #24292e gegen #1f1f1f, Dark #c9d1d9 gegen #ededed, Nord #d8dee9 gegen #eceff4, Dracula #e9e9f4 gegen #f8f8f2. Nur bei Catppuccin fallen sie zufällig zusammen. Ein color: var(--code-text) in der pre code.hljs-Regel räumt das auf — wobei sich genauso vertreten ließe, die Paletten in der Führung zu lassen und --code-text nur für Inline-Code zu verwenden.
Die Paletten sind unterschiedlich vollständig. Zusammengenommen verwenden die fünf Dateien 44 verschiedene Token-Klassen, jede einzelne deckt davon nur 34 bis 40 ab. hljs-function, hljs-class, hljs-tag, hljs-property und hljs-punctuation fehlen in Light und Dark, hljs-operator und hljs-punctuation in Nord. Was eine Palette nicht kennt, bleibt in der Grundfarbe — derselbe Codeschnipsel wird in Dracula also bunter dargestellt als in Dark. Für diesen Blog ist das folgenlos: Bash und YAML erzeugen zusammen nur sieben Token-Klassen, und die sind in allen fünf Paletten definiert. Wer regelmäßig JavaScript, PHP oder CSS postet, sieht den Unterschied dagegen sofort beim Umschalten. Das ließe sich vereinheitlichen — allerdings gegen fünf gewachsene Upstream-Paletten, die jeweils bewusst so gestaltet sind. Ich habe es deshalb bei der Beobachtung belassen.
Was ich in der Zwischenzeit einsetze
Da ich die Theme-Dateien nicht anfassen wollte und die Änderungen beim nächsten Update sonst weg wären, laufen alle sieben Punkte bei mir vorerst von außen — über das mitgelieferte Plugin HTML-Code (Admin → Plugins), im Feld für den <head>-Bereich. Seit der Einrichtung ohne Probleme im Einsatz:
<style>
[data-theme="light"] .reading-progress-fill{background:#fff}
.toc-drawer a.toc-link{color:var(--text-muted)}
.toc-drawer a.toc-link:hover{color:var(--text-primary)}
.toc-drawer a.toc-link.active{color:var(--accent)}
pre code.hljs{overflow-x:auto;color:var(--code-text)}
</style>
<script>
addEventListener('click', function (e) {
var b = document.querySelector('.navbar-toggler');
var m = document.getElementById('navbarResponsive');
if (m && b && m.classList.contains('show') && !m.contains(e.target) && !b.contains(e.target)) b.click();
});
addEventListener('click', function (e) {
if (!e.target.closest('.toc-fab')) return;
var a = document.querySelector('#toc-nav .toc-link.active');
var href = a && a.getAttribute('href');
document.querySelectorAll('.toc-drawer .toc-link').forEach(function (l) {
l.classList.toggle('active', l.getAttribute('href') === href);
});
});
</script>
Die erste CSS-Zeile gibt dem Balken im Light-Schema Weiß statt Schwarz, die drei .toc-drawer-Zeilen ziehen die mobile Liste auf dieselbe Farblogik wie die Seitenleiste, und die letzte stellt overflow-x wieder her und zieht nebenbei die Grundfarbe von Code auf --code-text. Der erste Listener schließt das Navigationsmenü bei Klick daneben, indem er einfach noch einmal den Hamburger-Button selbst auslöst statt dessen Logik nachzubauen — so bleiben aria-expanded und die Klassen korrekt. Der zweite kopiert die Markierung aus der Seitenleiste in die mobile Liste, sobald man das Inhaltsverzeichnis öffnet. Das reicht, denn das Theme setzt bei geöffnetem Verzeichnis body { overflow: hidden } — die Leseposition kann sich in der Zeit gar nicht ändern, ein Listener auf jedes Scroll-Ereignis wäre also nur Arbeit ohne Wirkung.
[!NOTE] Der Plugin-Weg hat einen Vorteil und einen Preis. Der Vorteil: Ein Theme-Update überschreibt nichts davon. Der Preis: Die Anpassungen hängen an Interna wie
#navbarResponsive,.toc-faboder.toc-drawer. Benennt das Theme davon etwas um, hört der Fix ohne Fehlermeldung auf zu wirken. Nach einem Update lohnt also ein kurzer Test.
Autoscroll für das Inhaltsverzeichnis?
Ein Punkt, den ich bewusst offen lasse: Das Inhaltsverzeichnis scrollt den aktiven Eintrag nicht automatisch in den sichtbaren Bereich. Bei langen Artikeln steht die Markierung deshalb irgendwann weit unterhalb dessen, was man sieht — in meinem Testbeitrag mit fünfzig Überschriften bis zu 946 Pixel. Auf dem Desktop hat die Seitenleiste eine Maximalhöhe mit eigenem Scrollbereich, am Telefon gilt dasselbe für die Liste im Drawer.
Man könnte das automatisieren. Nur gibt es dafür mehr als eine vertretbare Umsetzung: Springt die Liste bei jedem Scroll-Ereignis nach, oder erst wenn die Markierung den Rand erreicht? Weich animiert oder hart? Und was passiert, wenn jemand gerade selbst in der Liste blättert, während der Text weiterläuft — dann zieht ihm die Automatik den Inhalt unter dem Finger weg. Ein schlecht abgestimmtes Autoscroll ist unangenehmer als gar keins.
Dazu kommt ein Detail, das für die manuelle Variante spricht: Der Drawer merkt sich seine Scrollposition. Öffnet man ihn, blättert in der Liste und schließt ihn wieder, steht sie beim nächsten Öffnen genau dort — auch dann, wenn die Markierung inzwischen weitergewandert ist. Wer sich in einem langen Verzeichnis eine Stelle gesucht hat, findet sie wieder. Automatik würde diese Position jedes Mal überschreiben.
Spürbar wird das ohnehin erst bei Artikeln mit sehr vielen Überschriften. Darunter reicht selbst scrollen.
Beim Entwickler gemeldet
Alle fünf Punkte samt Patches habe ich als Issue eingereicht: Spleenftw/bludit-blowfish#1. Landen sie in einer der nächsten Versionen, kann der Plugin-Block hier wieder verschwinden.