Nextcloud-Integration: Dynamische Galerie + Auto-Thumbnails

- Galerie laedt Bilder dynamisch per JavaScript statt Jekyll-Loop
- nginx autoindex liefert JSON-Bilderliste
- Nextcloud-Ordner (Websitebilder) wird als Volume gemountet
- ImageMagick generiert automatisch 512px Thumbnails
- Hintergrund-Check alle 60s fuer neue Bilder
- Neuer entrypoint.sh fuer Thumbnail-Generierung + nginx Start

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kroonk
2026-04-12 14:03:43 +02:00
parent c885d22346
commit ab6b2bb8fe
7 changed files with 217 additions and 141 deletions

View File

@@ -59,13 +59,21 @@ Photography/
└── CNAME # Custom Domain Config └── CNAME # Custom Domain Config
``` ```
## Wie die Galerie funktioniert ## Wie die Galerie funktioniert (Dynamisch)
1. Jekyll scannt `images/fulls/` nach statischen Dateien 1. **Bilder hochladen**: Fotos in Nextcloud-Ordner `Websitebilder` ablegen
2. `index.html` iteriert mit `{% for image in site.static_files %}` ueber alle Bilder 2. **Volume-Mount**: Docker mountet den Nextcloud-Ordner als `/images/fulls/` (read-only)
3. Fuer jedes Bild wird ein `<article class="thumb">` erstellt 3. **Thumbnails**: `entrypoint.sh` generiert automatisch 512px Thumbnails via ImageMagick
4. Klick oeffnet Poptrox-Lightbox mit Vollbild + EXIF-Daten 4. **Thumbnail-Check**: Alle 60 Sekunden prueft der Container auf neue Bilder
5. EXIF-Daten werden clientseitig via `exif.js` aus dem Bild gelesen 5. **nginx autoindex**: Liefert JSON-Liste aller Dateien in `/images/fulls/`
6. **JavaScript (main.js)**: Fetcht die JSON-Liste und baut dynamisch `<article class="thumb">` Elemente
7. **Poptrox-Lightbox**: Klick oeffnet Vollbild + EXIF-Daten (clientseitig via exif.js)
### Nextcloud-Integration
- **Nextcloud-Pfad**: `/volume1/nextcloud/data/Tom/files/Websitebilder`
- **Mount im Container**: `/usr/share/nginx/html/images/fulls:ro`
- **Thumbnails-Volume**: Docker-Volume `thumbs` (persistent)
- **Workflow**: Bild in Nextcloud hochladen -> max. 60s warten -> Seite neu laden -> fertig
## Wichtige Konfiguration (_config.yml) ## Wichtige Konfiguration (_config.yml)
@@ -97,7 +105,8 @@ Photography/
### Docker-Dateien ### Docker-Dateien
- `Dockerfile` - Multi-Stage: Ruby/Jekyll baut Site -> nginx serviert statische Dateien - `Dockerfile` - Multi-Stage: Ruby/Jekyll baut Site -> nginx serviert statische Dateien
- `docker-compose.yml` - Container-Definition, Port 8080:80, auto-restart - `docker-compose.yml` - Container-Definition, Port 8080:80, Nextcloud-Volume, auto-restart
- `entrypoint.sh` - Thumbnail-Generierung + periodischer Check (alle 60s)
- `nginx.conf` - Webserver-Config mit Caching und Sicherheit - `nginx.conf` - Webserver-Config mit Caching und Sicherheit
- `.dockerignore` - Haelt Build-Context klein (kein .git, node_modules etc.) - `.dockerignore` - Haelt Build-Context klein (kein .git, node_modules etc.)
@@ -123,7 +132,9 @@ Photography/
- [x] Kontaktformular auf tom@mischlabs.de konfiguriert - [x] Kontaktformular auf tom@mischlabs.de konfiguriert
- [x] Google Analytics vom Originalautor entfernt - [x] Google Analytics vom Originalautor entfernt
- [x] Custom Domain: tom.mischlabs.de eingetragen - [x] Custom Domain: tom.mischlabs.de eingetragen
- [ ] Eigene Bilder einpflegen - [x] Nextcloud-Integration (Bilder aus Websitebilder-Ordner)
- [x] Dynamische Galerie (JS statt Jekyll-Loop)
- [x] Automatische Thumbnail-Generierung
- [ ] Footer Bio-Text schreiben (optional) - [ ] Footer Bio-Text schreiben (optional)
- [ ] Farbschema/Design anpassen - [ ] Farbschema/Design anpassen
- [ ] Eigene Kategorien/Alben erstellen (Feature-Erweiterung) - [ ] Eigene Kategorien/Alben erstellen (Feature-Erweiterung)

View File

@@ -12,10 +12,20 @@ COPY . .
RUN bundle exec jekyll build RUN bundle exec jekyll build
# === Serve-Phase: nginx liefert nur die fertigen Dateien aus === # === Serve-Phase: nginx + ImageMagick fuer Thumbnails ===
FROM nginx:alpine FROM nginx:alpine
RUN apk add --no-cache imagemagick
COPY nginx.conf /etc/nginx/conf.d/default.conf COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /site/_site /usr/share/nginx/html COPY --from=builder /site/_site /usr/share/nginx/html
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Leere Bildverzeichnisse anlegen (werden durch Volumes ueberschrieben)
RUN mkdir -p /usr/share/nginx/html/images/fulls \
/usr/share/nginx/html/images/thumbs
EXPOSE 80 EXPOSE 80
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -3,7 +3,7 @@
html5up.net | @ajlkn html5up.net | @ajlkn
Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
Added EXIF data and enhanced for Jekyll by Ram Patra Enhanced for dynamic gallery loading - Mischkomposition by Tom Misch
*/ */
(function ($) { (function ($) {
@@ -75,22 +75,14 @@
else $this.triggerHandler("---show"); else $this.triggerHandler("---show");
}) })
.on("---show", function () { .on("---show", function () {
// Hide other content.
if ($body.hasClass("content-active")) $panels.trigger("---hide"); if ($body.hasClass("content-active")) $panels.trigger("---hide");
// Activate content, toggles.
$this.addClass("active"); $this.addClass("active");
$toggles.addClass("active"); $toggles.addClass("active");
// Activate body.
$body.addClass("content-active"); $body.addClass("content-active");
}) })
.on("---hide", function () { .on("---hide", function () {
// Deactivate content, toggles.
$this.removeClass("active"); $this.removeClass("active");
$toggles.removeClass("active"); $toggles.removeClass("active");
// Deactivate body.
$body.removeClass("content-active"); $body.removeClass("content-active");
}); });
@@ -101,7 +93,6 @@
.on("click", function (event) { .on("click", function (event) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
$this.trigger("---toggle"); $this.trigger("---toggle");
}); });
}); });
@@ -111,7 +102,6 @@
if ($body.hasClass("content-active")) { if ($body.hasClass("content-active")) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
$panels.trigger("---hide"); $panels.trigger("---hide");
} }
}); });
@@ -120,7 +110,6 @@
if (event.keyCode == 27 && $body.hasClass("content-active")) { if (event.keyCode == 27 && $body.hasClass("content-active")) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
$panels.trigger("---hide"); $panels.trigger("---hide");
} }
}); });
@@ -128,22 +117,16 @@
// Header. // Header.
var $header = $("#header"); var $header = $("#header");
// Links.
$header.find("a").each(function () { $header.find("a").each(function () {
var $this = $(this), var $this = $(this),
href = $this.attr("href"); href = $this.attr("href");
// Internal link? Skip.
if (!href || href.charAt(0) == "#") return; if (!href || href.charAt(0) == "#") return;
// Redirect on click.
$this $this
.removeAttr("href") .removeAttr("href")
.css("cursor", "pointer") .css("cursor", "pointer")
.on("click", function (event) { .on("click", function (event) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
window.location.href = href; window.location.href = href;
}); });
}); });
@@ -151,9 +134,6 @@
// Footer. // Footer.
var $footer = $("#footer"); var $footer = $("#footer");
// Copyright.
// This basically just moves the copyright line to the end of the *last* sibling of its current parent
// when the "medium" breakpoint activates, and moves it back when it deactivates.
$footer.find(".copyright").each(function () { $footer.find(".copyright").each(function () {
var $this = $(this), var $this = $(this),
$parent = $this.parent(), $parent = $this.parent(),
@@ -168,86 +148,110 @@
}); });
}); });
// Main. // Main - Dynamische Galerie
var $main = $("#main"), var $main = $("#main"),
exifDatas = {}; exifDatas = {};
// Thumbs. var imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
$main.children(".thumb").each(function () {
var $this = $(this),
$image = $this.find(".image"),
$image_img = $image.children("img"),
x;
// No image? Bail. function loadGallery() {
if ($image.length == 0) return; $.ajax({
url: '/images/fulls/',
dataType: 'json',
success: function(files) {
var images = files.filter(function(f) {
if (f.type !== 'file') return false;
var ext = f.name.split('.').pop().toLowerCase();
return imageExtensions.indexOf(ext) !== -1;
});
// Image. images.sort(function(a, b) {
// This sets the background of the "image" <span> to the image pointed to by its child return a.name.localeCompare(b.name);
// <img> (which is then hidden). Gives us way more flexibility. });
// Set background. images.forEach(function(file) {
$image.css("background-image", "url(" + $image_img.attr("src") + ")"); var article = $('<article class="thumb"></article>');
var link = $('<a class="image" href="/images/fulls/' + encodeURIComponent(file.name) + '"></a>');
var img = $('<img src="/images/thumbs/' + encodeURIComponent(file.name) + '" alt="" data-name="' + file.name.replace(/"/g, '&quot;') + '" />');
link.append(img);
article.append(link);
$main.append(article);
});
// Set background position. initThumbs();
if ((x = $image_img.data("position"))) $image.css("background-position", x); initPoptrox();
},
// Hide original img. error: function() {
$image_img.hide(); $main.html('<p style="text-align:center;color:#fff;padding:2em;">Keine Bilder gefunden. Lade Bilder in deinen Nextcloud-Ordner hoch.</p>');
}
// EXIF data
$image_img[0].addEventListener("load", function() {
EXIF.getData($image_img[0], function () {
exifDatas[$image_img.data('name')] = getExifDataMarkup(this);
}); });
}); }
});
// Poptrox. function initThumbs() {
$main.poptrox({ $main.children(".thumb").each(function () {
baseZIndex: 20000, var $this = $(this),
caption: function ($a) { $image = $this.find(".image"),
var $image_img = $a.children('img'); $image_img = $image.children("img"),
var data = exifDatas[$image_img.data('name')]; x;
if (data === undefined) {
// EXIF data if ($image.length == 0) return;
EXIF.getData($image_img[0], function () {
data = exifDatas[$image_img.data('name')] = getExifDataMarkup(this); $image.css("background-image", "url(" + $image_img.attr("src") + ")");
if ((x = $image_img.data("position"))) $image.css("background-position", x);
$image_img.hide();
$image_img[0].addEventListener("load", function() {
EXIF.getData($image_img[0], function () {
exifDatas[$image_img.data('name')] = getExifDataMarkup(this);
});
}); });
} });
return data !== undefined ? '<p>' + data + '</p>' : ' '; }
},
fadeSpeed: 300,
onPopupClose: function () {
$body.removeClass("modal-active");
},
onPopupOpen: function () {
$body.addClass("modal-active");
},
overlayOpacity: 0,
popupCloserText: "",
popupHeight: 150,
popupLoaderText: "",
popupSpeed: 300,
popupWidth: 150,
selector: ".thumb > a.image",
usePopupCaption: true,
usePopupCloser: true,
usePopupDefaultStyling: false,
usePopupForceClose: true,
usePopupLoader: true,
usePopupNav: true,
windowMargin: 50,
});
// Hack: Set margins to 0 when 'xsmall' activates. function initPoptrox() {
breakpoints.on("<=xsmall", function () { $main.poptrox({
$main[0]._poptrox.windowMargin = 0; baseZIndex: 20000,
}); caption: function ($a) {
var $image_img = $a.children('img');
var data = exifDatas[$image_img.data('name')];
if (data === undefined) {
EXIF.getData($image_img[0], function () {
data = exifDatas[$image_img.data('name')] = getExifDataMarkup(this);
});
}
return data !== undefined ? '<p>' + data + '</p>' : ' ';
},
fadeSpeed: 300,
onPopupClose: function () {
$body.removeClass("modal-active");
},
onPopupOpen: function () {
$body.addClass("modal-active");
},
overlayOpacity: 0,
popupCloserText: "",
popupHeight: 150,
popupLoaderText: "",
popupSpeed: 300,
popupWidth: 150,
selector: ".thumb > a.image",
usePopupCaption: true,
usePopupCloser: true,
usePopupDefaultStyling: false,
usePopupForceClose: true,
usePopupLoader: true,
usePopupNav: true,
windowMargin: 50,
});
breakpoints.on(">xsmall", function () { breakpoints.on("<=xsmall", function () {
$main[0]._poptrox.windowMargin = 50; $main[0]._poptrox.windowMargin = 0;
}); });
breakpoints.on(">xsmall", function () {
$main[0]._poptrox.windowMargin = 50;
});
}
function getExifDataMarkup(img) { function getExifDataMarkup(img) {
var exif = $('#main').data('exif'); var exif = $('#main').data('exif');
@@ -263,4 +267,7 @@
return template; return template;
} }
// Galerie laden
loadGallery();
})(jQuery); })(jQuery);

View File

@@ -4,4 +4,12 @@ services:
container_name: photography-website container_name: photography-website
ports: ports:
- "8080:80" - "8080:80"
volumes:
# Nextcloud-Ordner mit Originalbildern (read-only)
- /volume1/nextcloud/data/Tom/files/Websitebilder:/usr/share/nginx/html/images/fulls:ro
# Thumbnails werden automatisch generiert und hier gespeichert
- thumbs:/usr/share/nginx/html/images/thumbs
restart: unless-stopped restart: unless-stopped
volumes:
thumbs:

39
entrypoint.sh Normal file
View File

@@ -0,0 +1,39 @@
#!/bin/sh
FULLS_DIR="/usr/share/nginx/html/images/fulls"
THUMBS_DIR="/usr/share/nginx/html/images/thumbs"
generate_thumbs() {
mkdir -p "$THUMBS_DIR"
for img in "$FULLS_DIR"/*; do
[ -f "$img" ] || continue
filename=$(basename "$img")
ext=$(echo "${filename##*.}" | tr '[:upper:]' '[:lower:]')
# Nur Bilddateien verarbeiten
case "$ext" in
jpg|jpeg|png|gif|webp) ;;
*) continue ;;
esac
# Thumbnail nur erstellen wenn noch nicht vorhanden oder aelter als Original
if [ ! -f "$THUMBS_DIR/$filename" ] || [ "$img" -nt "$THUMBS_DIR/$filename" ]; then
echo "Thumbnail erstellen: $filename"
convert "$img" -resize 512x512 -quality 80 "$THUMBS_DIR/$filename" 2>/dev/null
fi
done
}
echo "=== Mischkomposition - Starte Thumbnail-Generierung ==="
generate_thumbs
echo "=== Thumbnail-Generierung abgeschlossen ==="
# Hintergrund-Prozess: Alle 60 Sekunden nach neuen Bildern suchen
(while true; do
sleep 60
generate_thumbs
done) &
# nginx starten
nginx -g 'daemon off;'

View File

@@ -17,17 +17,7 @@ layout: default
<!-- Main --> <!-- Main -->
<div id="main" data-exif='{{ site.exif }}'> <div id="main" data-exif='{{ site.exif }}'>
{% for image in site.static_files %} <!-- Galerie wird dynamisch per JavaScript geladen -->
{% if image.path contains 'fulls' %}
<article class="thumb">
<a href="{{ site.baseurl }}{{ site.image_fulls_loc }}/{{ image.name }}" class="image">
<img src="{{ site.baseurl }}{{ site.image_thumbs_loc }}/{{ image.name }}" alt=""
data-name="{{ image.name }}"/></a>
<!--<h2></h2>
<p></p>-->
</article>
{% endif %}
{% endfor %}
</div> </div>
<!-- Footer --> <!-- Footer -->

View File

@@ -4,8 +4,19 @@ server {
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;
# Bilder-Verzeichnis: JSON-Listing fuer dynamische Galerie
location /images/fulls/ {
autoindex on;
autoindex_format json;
}
location /images/thumbs/ {
autoindex on;
autoindex_format json;
}
# Caching fuer Bilder und Assets # Caching fuer Bilder und Assets
location ~* \.(jpg|jpeg|png|gif|svg|ico|css|js|woff|woff2|ttf|eot|otf)$ { location ~* \.(jpg|jpeg|png|gif|svg|ico|css|js|woff|woff2|ttf|eot|otf|webp)$ {
expires 30d; expires 30d;
add_header Cache-Control "public, immutable"; add_header Cache-Control "public, immutable";
} }