From ab6b2bb8fe7710ec353cb925f705b23da6161d66 Mon Sep 17 00:00:00 2001 From: Kroonk Date: Sun, 12 Apr 2026 14:03:43 +0200 Subject: [PATCH] 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 --- Brain.md | 27 +++-- Dockerfile | 12 ++- assets/js/main.js | 247 +++++++++++++++++++++++---------------------- docker-compose.yml | 8 ++ entrypoint.sh | 39 +++++++ index.html | 12 +-- nginx.conf | 13 ++- 7 files changed, 217 insertions(+), 141 deletions(-) create mode 100644 entrypoint.sh diff --git a/Brain.md b/Brain.md index 5bd6e23..db06b2e 100644 --- a/Brain.md +++ b/Brain.md @@ -59,13 +59,21 @@ Photography/ └── CNAME # Custom Domain Config ``` -## Wie die Galerie funktioniert +## Wie die Galerie funktioniert (Dynamisch) -1. Jekyll scannt `images/fulls/` nach statischen Dateien -2. `index.html` iteriert mit `{% for image in site.static_files %}` ueber alle Bilder -3. Fuer jedes Bild wird ein `
` erstellt -4. Klick oeffnet Poptrox-Lightbox mit Vollbild + EXIF-Daten -5. EXIF-Daten werden clientseitig via `exif.js` aus dem Bild gelesen +1. **Bilder hochladen**: Fotos in Nextcloud-Ordner `Websitebilder` ablegen +2. **Volume-Mount**: Docker mountet den Nextcloud-Ordner als `/images/fulls/` (read-only) +3. **Thumbnails**: `entrypoint.sh` generiert automatisch 512px Thumbnails via ImageMagick +4. **Thumbnail-Check**: Alle 60 Sekunden prueft der Container auf neue Bilder +5. **nginx autoindex**: Liefert JSON-Liste aller Dateien in `/images/fulls/` +6. **JavaScript (main.js)**: Fetcht die JSON-Liste und baut dynamisch `
` 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) @@ -97,7 +105,8 @@ Photography/ ### Docker-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 - `.dockerignore` - Haelt Build-Context klein (kein .git, node_modules etc.) @@ -123,7 +132,9 @@ Photography/ - [x] Kontaktformular auf tom@mischlabs.de konfiguriert - [x] Google Analytics vom Originalautor entfernt - [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) - [ ] Farbschema/Design anpassen - [ ] Eigene Kategorien/Alben erstellen (Feature-Erweiterung) diff --git a/Dockerfile b/Dockerfile index 997cd5c..f51fe51 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,10 +12,20 @@ COPY . . RUN bundle exec jekyll build -# === Serve-Phase: nginx liefert nur die fertigen Dateien aus === +# === Serve-Phase: nginx + ImageMagick fuer Thumbnails === FROM nginx:alpine +RUN apk add --no-cache imagemagick + COPY nginx.conf /etc/nginx/conf.d/default.conf 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 + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/assets/js/main.js b/assets/js/main.js index 99be8a7..b6c0109 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -3,14 +3,14 @@ html5up.net | @ajlkn 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 ($) { var $window = $(window), $body = $("body"), $wrapper = $("#wrapper"); - + // Breakpoints. breakpoints({ xlarge: ["1281px", "1680px"], @@ -19,13 +19,13 @@ small: ["481px", "736px"], xsmall: [null, "480px"], }); - + // Hack: Enable IE workarounds. if (browser.name == "ie") $body.addClass("ie"); - + // Touch? if (browser.mobile) $body.addClass("touch"); - + // Transitions supported? if (browser.canUse("transition")) { // Play initial animations on page load. @@ -34,37 +34,37 @@ $body.removeClass("is-preload"); }, 100); }); - + // Prevent transitions/animations on resize. var resizeTimeout; - + $window.on("resize", function () { window.clearTimeout(resizeTimeout); - + $body.addClass("is-resizing"); - + resizeTimeout = window.setTimeout(function () { $body.removeClass("is-resizing"); }, 100); }); } - + // Scroll back to top. $window.scrollTop(0); - + // Panels. var $panels = $(".panel"); - + $panels.each(function () { var $this = $(this), $toggles = $('[href="#' + $this.attr("id") + '"]'), $closer = $('
').appendTo($this); - + // Closer. $closer.on("click", function (event) { $this.trigger("---hide"); }); - + // Events. $this .on("click", function (event) { @@ -75,25 +75,17 @@ else $this.triggerHandler("---show"); }) .on("---show", function () { - // Hide other content. if ($body.hasClass("content-active")) $panels.trigger("---hide"); - - // Activate content, toggles. $this.addClass("active"); $toggles.addClass("active"); - - // Activate body. $body.addClass("content-active"); }) .on("---hide", function () { - // Deactivate content, toggles. $this.removeClass("active"); $toggles.removeClass("active"); - - // Deactivate body. $body.removeClass("content-active"); }); - + // Toggles. $toggles .removeAttr("href") @@ -101,154 +93,166 @@ .on("click", function (event) { event.preventDefault(); event.stopPropagation(); - $this.trigger("---toggle"); }); }); - + // Global events. $body.on("click", function (event) { if ($body.hasClass("content-active")) { event.preventDefault(); event.stopPropagation(); - $panels.trigger("---hide"); } }); - + $window.on("keyup", function (event) { if (event.keyCode == 27 && $body.hasClass("content-active")) { event.preventDefault(); event.stopPropagation(); - $panels.trigger("---hide"); } }); - + // Header. var $header = $("#header"); - - // Links. + $header.find("a").each(function () { var $this = $(this), href = $this.attr("href"); - - // Internal link? Skip. if (!href || href.charAt(0) == "#") return; - - // Redirect on click. $this .removeAttr("href") .css("cursor", "pointer") .on("click", function (event) { event.preventDefault(); event.stopPropagation(); - window.location.href = href; }); }); - + // 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 () { var $this = $(this), $parent = $this.parent(), $lastParent = $parent.parent().children().last(); - + breakpoints.on("<=medium", function () { $this.appendTo($lastParent); }); - + breakpoints.on(">medium", function () { $this.appendTo($parent); }); }); - - // Main. + + // Main - Dynamische Galerie var $main = $("#main"), exifDatas = {}; - - // Thumbs. - $main.children(".thumb").each(function () { - var $this = $(this), - $image = $this.find(".image"), - $image_img = $image.children("img"), - x; - - // No image? Bail. - if ($image.length == 0) return; - - // Image. - // This sets the background of the "image" to the image pointed to by its child - // (which is then hidden). Gives us way more flexibility. - - // Set background. - $image.css("background-image", "url(" + $image_img.attr("src") + ")"); - - // Set background position. - if ((x = $image_img.data("position"))) $image.css("background-position", x); - - // Hide original img. - $image_img.hide(); - // EXIF data - $image_img[0].addEventListener("load", function() { - EXIF.getData($image_img[0], function () { - exifDatas[$image_img.data('name')] = getExifDataMarkup(this); + var imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp']; + + function loadGallery() { + $.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; + }); + + images.sort(function(a, b) { + return a.name.localeCompare(b.name); + }); + + images.forEach(function(file) { + var article = $('
'); + var link = $(''); + var img = $(''); + link.append(img); + article.append(link); + $main.append(article); + }); + + initThumbs(); + initPoptrox(); + }, + error: function() { + $main.html('

Keine Bilder gefunden. Lade Bilder in deinen Nextcloud-Ordner hoch.

'); + } }); - }); - }); - - // Poptrox. - $main.poptrox({ - baseZIndex: 20000, - caption: function ($a) { - var $image_img = $a.children('img'); - var data = exifDatas[$image_img.data('name')]; - if (data === undefined) { - // EXIF data - EXIF.getData($image_img[0], function () { - data = exifDatas[$image_img.data('name')] = getExifDataMarkup(this); + } + + function initThumbs() { + $main.children(".thumb").each(function () { + var $this = $(this), + $image = $this.find(".image"), + $image_img = $image.children("img"), + x; + + if ($image.length == 0) return; + + $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 ? '

' + data + '

' : ' '; - }, - 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. - breakpoints.on("<=xsmall", function () { - $main[0]._poptrox.windowMargin = 0; - }); - - breakpoints.on(">xsmall", function () { - $main[0]._poptrox.windowMargin = 50; - }); - + }); + } + + function initPoptrox() { + $main.poptrox({ + 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 ? '

' + data + '

' : ' '; + }, + 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 () { + $main[0]._poptrox.windowMargin = 0; + }); + + breakpoints.on(">xsmall", function () { + $main[0]._poptrox.windowMargin = 50; + }); + } + function getExifDataMarkup(img) { var exif = $('#main').data('exif'); var template = ''; @@ -263,4 +267,7 @@ return template; } + // Galerie laden + loadGallery(); + })(jQuery); diff --git a/docker-compose.yml b/docker-compose.yml index 8b99ed3..c0e2701 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,4 +4,12 @@ services: container_name: photography-website ports: - "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 + +volumes: + thumbs: diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..0904b32 --- /dev/null +++ b/entrypoint.sh @@ -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;' diff --git a/index.html b/index.html index a71adb7..5fb4a88 100644 --- a/index.html +++ b/index.html @@ -17,17 +17,7 @@ layout: default
- {% for image in site.static_files %} - {% if image.path contains 'fulls' %} - - {% endif %} - {% endfor %} +
diff --git a/nginx.conf b/nginx.conf index bebba29..1b44054 100644 --- a/nginx.conf +++ b/nginx.conf @@ -4,8 +4,19 @@ server { root /usr/share/nginx/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 - 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; add_header Cache-Control "public, immutable"; }