From e3a93bdc0037a202d476334de24788d7dc2babfd Mon Sep 17 00:00:00 2001 From: soloturn Date: Wed, 11 Nov 2026 20:31:27 +0100 Subject: [PATCH 1/2] feat: gallery key controls, og:type and og:image for pages and posts Lightbox: arrows/h/l step, Esc closes; modifier combos are left to the browser. Layout gains og:type and og:image (logo by default); posts pass their cover as a 1200px jpg and type article. Refs #222, #25. Co-Authored-By: soloturn --- src/components/Gallery.jsx | 15 ++++++++++++++- src/layouts/Layout.astro | 10 ++++++++-- src/pages/blog/[slug].astro | 12 ++++++++++-- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/components/Gallery.jsx b/src/components/Gallery.jsx index b51a99ce..8a46945f 100644 --- a/src/components/Gallery.jsx +++ b/src/components/Gallery.jsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { GiPlainSquare, GiSquare } from "react-icons/gi"; import { ImCross } from "react-icons/im"; import { formatDate } from "../lib/format"; @@ -35,6 +35,19 @@ export default function Gallery({ images, currentPage, lastPage, total, perPage else window.location.href = `${pageUrl(currentPage - 1)}?index=${perPage - 1}`; }; + // Re-bound every render so the handlers always see the current image. + useEffect(() => { + if (active === null) return undefined; + const onKey = (e) => { + if (e.ctrlKey || e.metaKey || e.altKey) return; + if (e.key === "ArrowRight" || e.key === "l") next(); + else if (e.key === "ArrowLeft" || e.key === "h") prev(); + else if (e.key === "Escape") show(null); + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }); + const current = active === null ? null : images[active]; return ( diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index 09453d3d..d80c5939 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -11,12 +11,16 @@ interface Props { /** Document title prefix; " | Terasology". Omit for the plain site title. */ title?: string; description?: string; + /** Social preview image, as a site path or absolute URL; defaults to the logo. */ + image?: string; + /** Open Graph type; "article" for blog posts. */ + type?: "website" | "article"; } -const { heading, title, description = site.description } = Astro.props; +const { heading, title, description = site.description, type = "website" } = Astro.props; const fullTitle = title ? `${title} | ${site.title}` : site.title; const url = new URL(Astro.url.pathname, site.url).href; -const image = new URL(site.logo, site.url).href; +const image = new URL(Astro.props.image ?? site.logo, site.url).href; --- <!doctype html> @@ -35,9 +39,11 @@ const image = new URL(site.logo, site.url).href; /> <title>{fullTitle} + + diff --git a/src/pages/blog/[slug].astro b/src/pages/blog/[slug].astro index 9acfd1ab..976af1dc 100644 --- a/src/pages/blog/[slug].astro +++ b/src/pages/blog/[slug].astro @@ -1,5 +1,5 @@ --- -import { Image } from "astro:assets"; +import { getImage, Image } from "astro:assets"; import { render } from "astro:content"; import Layout from "../../layouts/Layout.astro"; import SocialLinks from "../../components/SocialLinks.astro"; @@ -14,9 +14,17 @@ export async function getStaticPaths() { const { post } = Astro.props as { post: Post }; const { Content } = await render(post); const { title, author, date, cover, description } = post.data; +// jpg rather than webp: social crawlers handle it more reliably +const preview = await getImage({ src: cover, width: 1200, format: "jpg" }); --- - +
Posted by {author} on {formatDate(date)}
From e48a78d2b528a00f637c243cf05f882d2a5440b2 Mon Sep 17 00:00:00 2001 From: soloturn Date: Wed, 11 Nov 2026 21:00:40 +0100 Subject: [PATCH 2/2] feat: arrow and h/l keys page through the gallery overview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the lightbox closed, ←/h and →/l go to the previous and next gallery page, wrapping at the ends like the lightbox does. Refs #222. Co-Authored-By: soloturn --- src/components/Gallery.jsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/components/Gallery.jsx b/src/components/Gallery.jsx index 8a46945f..263e0d0e 100644 --- a/src/components/Gallery.jsx +++ b/src/components/Gallery.jsx @@ -36,13 +36,21 @@ export default function Gallery({ images, currentPage, lastPage, total, perPage }; // Re-bound every render so the handlers always see the current image. + // Lightbox open: step through images. Overview: step through gallery pages (wrapping). useEffect(() => { - if (active === null) return undefined; const onKey = (e) => { if (e.ctrlKey || e.metaKey || e.altKey) return; - if (e.key === "ArrowRight" || e.key === "l") next(); - else if (e.key === "ArrowLeft" || e.key === "h") prev(); - else if (e.key === "Escape") show(null); + const forward = e.key === "ArrowRight" || e.key === "l"; + const back = e.key === "ArrowLeft" || e.key === "h"; + if (active !== null) { + if (forward) next(); + else if (back) prev(); + else if (e.key === "Escape") show(null); + } else if (forward) { + window.location.href = pageUrl(currentPage === lastPage ? 1 : currentPage + 1); + } else if (back) { + window.location.href = pageUrl(currentPage === 1 ? lastPage : currentPage - 1); + } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey);