From e2c40468b6b94982e28444dd5ef8be085e65ff83 Mon Sep 17 00:00:00 2001 From: Ricardo Date: Fri, 27 Feb 2026 10:14:35 +0100 Subject: [PATCH] feat: add fullscreen lightbox for article images Alpine.js component that lets visitors click any image inside article content to view it fullscreen with keyboard navigation (arrow keys, Escape to close) and prev/next buttons. --- _includes/layouts/base.njk | 1 + _includes/layouts/post.njk | 19 ++++++++- js/lightbox.js | 80 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 js/lightbox.js diff --git a/_includes/layouts/base.njk b/_includes/layouts/base.njk index f1fd24a..5d47014 100644 --- a/_includes/layouts/base.njk +++ b/_includes/layouts/base.njk @@ -72,6 +72,7 @@ {# Alpine.js components — MUST load before Alpine core (Alpine.data() registration via alpine:init) #} + diff --git a/_includes/layouts/post.njk b/_includes/layouts/post.njk index 76ee848..6f8e38c 100644 --- a/_includes/layouts/post.njk +++ b/_includes/layouts/post.njk @@ -2,7 +2,7 @@ layout: layouts/base.njk withBlogSidebar: true --- -
+
{% if title %}

{{ title }}

{% endif %} @@ -245,6 +245,23 @@ withBlogSidebar: true "image": ["{% if postImage.startsWith('http') %}{{ postImage }}{% elif '/' in postImage and postImage[0] == '/' %}{{ site.url }}{{ postImage }}{% else %}{{ site.url }}/{{ postImage }}{% endif %}"]{% endif %} } + + {# Lightbox overlay for article images #} +
{# Comments section #} diff --git a/js/lightbox.js b/js/lightbox.js new file mode 100644 index 0000000..d8df430 --- /dev/null +++ b/js/lightbox.js @@ -0,0 +1,80 @@ +/** + * Alpine.js lightbox component for article images. + * Registers via alpine:init so it's available before Alpine starts. + * Click any image inside .e-content to view fullscreen. + * Navigate with arrow keys, close with Escape or click outside. + */ +document.addEventListener("alpine:init", () => { + Alpine.data("lightbox", () => ({ + open: false, + src: "", + alt: "", + images: [], + currentIndex: 0, + + init() { + const container = this.$root; + const imgs = container.querySelectorAll( + ".e-content img:not(.u-photo)" + ); + this.images = Array.from(imgs); + + this.images.forEach((img, i) => { + img.style.cursor = "zoom-in"; + img.addEventListener("click", (e) => { + e.preventDefault(); + this.show(i); + }); + }); + }, + + show(index) { + this.currentIndex = index; + const img = this.images[index]; + // Use the largest source available + const picture = img.closest("picture"); + if (picture) { + const source = picture.querySelector("source"); + if (source) { + // Extract the URL from srcset (strip width descriptor) + const srcset = source.getAttribute("srcset") || ""; + this.src = srcset.split(/\s+/)[0] || img.src; + } else { + this.src = img.src; + } + } else { + this.src = img.src; + } + this.alt = img.alt || ""; + this.open = true; + document.body.style.overflow = "hidden"; + }, + + close() { + this.open = false; + this.src = ""; + document.body.style.overflow = ""; + }, + + next() { + if (this.images.length > 1) { + this.show((this.currentIndex + 1) % this.images.length); + } + }, + + prev() { + if (this.images.length > 1) { + this.show( + (this.currentIndex - 1 + this.images.length) % this.images.length + ); + } + }, + + onKeydown(e) { + if (!this.open) return; + if (e.key === "Escape") this.close(); + if (e.key === "ArrowRight") this.next(); + if (e.key === "ArrowLeft") this.prev(); + }, + })); +});