Astro Justified Gallery Layout — moderner Ersatz für Flickrs Klassiker
Mein neues npm-Paket @altner/astro-justified-gallery-layout: eine schlanke Justified-Gallery für Astro mit EXIF/IPTC/GPS, LQIP-Previews, virtualisierter Variante für riesige Sammlungen und einer eingebauten Lightbox.
Die Foto-Sektion dieser Seite läuft seit jeher mit Flickrs justified-layout — ein bewährtes Stück Code, technisch aber inzwischen aus der Zeit gefallen: UMD-Modul, ein paar Dependencies, und seit v4.1.0 (Januar 2021) keine neuen Releases mehr. Für die nächste Iteration der Foto-Seite und ein paar andere Astro-Projekte wollte ich einen Layer, der sich von vornherein wie eine Astro-Komponente anfühlt — also habe ich ihn neu geschrieben. Heute ist die erste öffentliche Version draußen: @altner/astro-justified-gallery-layout.
Das Paket ist ein von Grund auf neu gebauter Layer in der Tradition von Flickrs Original — deutlich kleiner, ESM-only, TypeScript-Typen gebündelt, im Kern null Runtime-Dependencies — und ergänzt um genau die Bausteine, die ich für eine echte Foto-Seite gebraucht habe.
Was drin ist
<JustifiedGallery />— die Standard-Komponente. Reflowt bei Container-Resize viaResizeObserver, Astros<Image />für automatische Optimierung, optionales chunked Rendering viaIntersectionObserverfür lange Listen.<JustifiedGalleryVirtual />— DOM-Windowing für riesige Sammlungen. Der Server liefert einen leeren Container, der Client zieht Metadaten in Häppchen aus einem JSON-Endpoint und hält nur die sichtbaren Reihen im DOM.<Lightbox />— natives<dialog>-Modal mit Pfeiltasten, Swipe, ESC und Backdrop-Close. Einmal pro Seite einbinden, opt-in pro Galerie über daslightbox-Prop.galleryLoader()— Astro-Content-Loader, der ein Verzeichnis scannt und optional EXIF/IPTC/XMP/GPS viaexifrausliest plus LQIP-Previews viasharperzeugt (~1 KB Base64-Platzhalter für instantanen First Paint).computeLayout()— die pure Funktion dahinter, ~120 Zeilen, framework-agnostisch und damit auch außerhalb von Astro nutzbar.readPhotoMeta()/formatCameraLine()— kleine Helfer, die aus EXIF eine humanisierte Zeile machen wieSony ILCE-7M3 · 35mm · f/2.8 · 1/250s · ISO 400.
Minimal-Beispiel
---
import JustifiedGallery from '@altner/astro-justified-gallery-layout/JustifiedGallery.astro';
import Lightbox from '@altner/astro-justified-gallery-layout/Lightbox.astro';
import type { ImageMetadata } from 'astro';
const modules = import.meta.glob<{ default: ImageMetadata }>(
'../assets/photos/*.{jpg,jpeg,png,webp,avif}',
{ eager: true },
);
const images = Object.values(modules).map((m) => ({ src: m.default }));
---
<JustifiedGallery images={images} targetRowHeight={240} gap={6} chunkSize={30} lightbox />
<Lightbox />
Das ist alles. Bilder ins Verzeichnis legen, astro dev, fertig — inklusive lazy-load, LQIP-Platzhaltern und Lightbox.
Der Trick beim Row-Break
Der spannendste Teil steckt in computeLayout(). Während Bilder einer Reihe hinzugefügt werden, wird die Höhe berechnet, die nötig wäre, damit die Reihe genau in die Containerbreite passt. Sinkt die Höhe auf oder unter die Ziel-Höhe, vergleicht der Algorithmus zwei Optionen:
- Reihe mit dem aktuellen Bild abschließen.
- Reihe ohne das aktuelle Bild abschließen, das Bild rutscht in die nächste Reihe.
Gewählt wird, was näher an der Ziel-Höhe liegt. Das Ergebnis: weder überhohe Einzelbilder, noch gequetschte Mini-Reihen. Die letzte (unvollständige) Reihe bleibt linksbündig auf Ziel-Höhe — drei Querformate auf 1500 px gestreckt sieht einfach lächerlich aus.
computeLayout() ist deterministisch, ohne DOM-Zugriff, ohne Side-Effects. Triviales Memoizen, läuft auch in Web Workers.
Was bewusst klein gehalten ist
| dieses Paket (Kern) | flickr/justified-layout | |
|---|---|---|
| Größe | ~120 LOC, ~1 kB min+gz | ~30 kB minified |
| Modul-Format | nur ESM | UMD (Node-Style) |
| Dependencies | 0 | mehrere |
| TypeScript-Typen | mitgeliefert | DefinitelyTyped, oft veraltet |
| Astro-Komponente | mitgeliefert | — |
| Letztes Release | v0.6.0 (April 2026) | v4.1.0 (Januar 2021) |
exifr und sharp sind optionale Peers — wer keine Metadaten und keine LQIPs braucht, installiert sie nicht. Fehlende Peers degradieren still: kein Crash, einfach kein Feature.
Und auf dieser Seite?
Die ehrliche Antwort: irgendwann ja, aktuell nein. Die Foto-Sektion hier hat über die Zeit eine eigene Logik bekommen — hierarchischer Foto-Baum mit index.md pro Set, vorab generierte JSON-Sidecars mit EXIF/IPTC/GPS, mehrsprachige Titel pro Foto, locale-spezifische URL-Segmente, Statistik- und Weltkarten-Ansichten, die alle auf der eigenen Datenform sitzen. Der galleryLoader() aus dem Paket erwartet eine flache Foto-Sammlung mit anderem EXIF-Schema und kennt keine Lokalisierung — würde ich das hier eins zu eins drüberkippen, bricht die Hälfte der Seite.
Der größere Bruch ist aber der Workflow: das Paket liest Tags, Titel, Beschreibung und GPS direkt aus der Bilddatei (EXIF/IPTC/XMP) — nicht aus separaten JSON-Sidecars wie aktuell. Das heißt, die Fotos brauchen vor dem Export eine Sonderbehandlung in Lightroom (oder einem anderen DAM): IPTC-Felder für Title, Description und Keywords müssen dort gepflegt sein, der Export-Preset muss IPTC und GPS in der JPG belassen — sonst stehen die Felder beim Auslesen leer. Vorteil: keine zweite Datenquelle, jedes Foto trägt seine Metadaten selbst. Nachteil: bestehende JSONs müssten erst zurück ins Bild geschrieben werden, oder der Loader brauchte einen Hybrid-Modus, der beides liest.
Realistisch werde ich das Paket erst beim nächsten größeren Umbau des Foto-Stacks als Grundlage nehmen — mit Lightroom-seitig sauber gepflegten IPTC-Feldern, gemappten Datenstrukturen, einem Layer für die Set-Hierarchie und einer Übersetzungsschicht für GPS und Camera-Metadaten. Bis dahin koexistiert das alte Setup hier und das neue Paket draußen auf npm.
Installation & Status
Version 0.6.0, MIT-Lizenz, auf npm:
npm install @altner/astro-justified-gallery-layout
Quellcode im Monorepo auf GitHub. Issues und Feature-Wünsche gehen in den Tracker.