From 1e921dfe3739d3df936feff613b4218876ff0d7f Mon Sep 17 00:00:00 2001 From: Linus Olofsson Date: Tue, 27 Oct 2026 19:13:24 +0100 Subject: [PATCH] fix(images): stop publishing EXIF/GPS originals, responsive images, crawlable gallery - Homepage images: from src/assets (AVIF/WebP/JPG srcset, width/ height, first image eager + fetchpriority high, rest lazy), Swedish alt texts. (The duplicate EXIF-laden copies in public/3d-scroll were deleted in 30e385e; README links now point at src/assets.) - Portfolio: originals are no longer referenced (no .src access), so Astro does not copy them to dist/. Small/Medium/Large downloads and the lightbox srcset are EXIF-free JPGs from getImage. Old -s/-m files are no longer imported. - Alt texts: Swedish fallback from date (EXIF or filename), never camera model or "undefined"; portfolio-meta.ts overrides per image. - Gallery items are to the large image (lightbox on click); download options are buttons; no empty H3, no H4 for the password box. - Stats beacon unchanged; item keys computed at build time (same format). - Script moved inside the layout so it is no longer rendered after . Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WvE3C43Uwtv2vtnoFBLDw7 --- README.md | 6 +- src/components/ThreeDScroll.astro | 34 +- src/pages/portfolio.astro | 781 ++++++++++++++++-------------- 3 files changed, 452 insertions(+), 369 deletions(-) diff --git a/README.md b/README.md index 1655dbb..d6ec4f1 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,15 @@ A modern, responsive photography portfolio website built with Astro, Tailwind CS diff --git a/src/components/ThreeDScroll.astro b/src/components/ThreeDScroll.astro index e5a00c2..20c3f50 100644 --- a/src/components/ThreeDScroll.astro +++ b/src/components/ThreeDScroll.astro @@ -1,13 +1,22 @@ --- // src/components/ThreeDScroll.astro +// Bilderna ligger i src/assets så att Astro skalar om dem (AVIF/WebP/JPG, srcset) +// och rensar bort EXIF (GPS, serienummer m.m.). Originalen publiceras inte. +import { Picture } from 'astro:assets'; +import img1 from '../assets/images/3d-scroll/img1.jpg'; +import img2 from '../assets/images/3d-scroll/img2.jpg'; +import img3 from '../assets/images/3d-scroll/img3.jpg'; +import img4 from '../assets/images/3d-scroll/img4.jpg'; +import img5 from '../assets/images/3d-scroll/img5.jpg'; const images = [ - { src: "/3d-scroll/img1.jpg", alt: "Architecture Dark" }, - { src: "/3d-scroll/img2.jpg", alt: "Shadows Light" }, - { src: "/3d-scroll/img3.jpg", alt: "Portrait High End" }, - { src: "/3d-scroll/img4.jpg", alt: "Minimalist Landscape" }, - { src: "/3d-scroll/img5.jpg", alt: "Foggy Forest" }, + { src: img1, alt: 'Närbild av en gammal läderinbunden bok med slitna pappersark i varmt motljus' }, + { src: img2, alt: 'Veteranbil med tända strålkastare på en stadsgata i kvällsljus' }, + { src: img3, alt: 'Överblommade tistlar bland höga grässtrån i dämpade bruna toner' }, + { src: img4, alt: 'Järnvägsspår som slingrar sig mellan träd i motljus, i sepiaton' }, + { src: img5, alt: 'Svartvit bild av en man som bär ett litet barn bland såpbubblor i motljus' }, ]; +const last = images.length - 1; ---
@@ -20,9 +29,20 @@ const images = [ class="gallery-item absolute transform-style-3d will-change-transform flex items-center justify-center" data-index={index} > - {img.alt}
diff --git a/src/pages/portfolio.astro b/src/pages/portfolio.astro index e1c8bc4..15e5df6 100644 --- a/src/pages/portfolio.astro +++ b/src/pages/portfolio.astro @@ -1,31 +1,32 @@ --- // src/pages/portfolio.astro import MainLayout from '../layouts/MainLayout.astro'; -import { Image } from 'astro:assets'; +import { Image, getImage } from 'astro:assets'; import exifr from 'exifr'; import fs from 'node:fs'; import path from 'node:path'; import WaveBackground from '../components/WaveBackground.astro'; +import { business, genreList } from '../data/business'; +import { portfolioMeta } from '../data/portfolio-meta'; -// --- SERVER-SIDE LOGIC --- +// SERVER-SIDE LOGIC (byggtid) -// 1. Hämta alla bilder -const allImageFiles = import.meta.glob('../assets/images/portfolio/*.{jpeg,jpg,png,webp,JPG,JPEG,PNG}', { eager: true }); -// -s (800px) och -m (1600px) ar NEDLADDNINGSVARIANTER, inte egna galleribilder. -// Utan filtret visas varje bild tre ganger. -const imageFiles = Object.fromEntries( - Object.entries(allImageFiles).filter(([pathKey]) => !/-[sm]\.(jpe?g|png|webp)$/i.test(pathKey)) +// 1. Hämta alla galleribilder. +// -s (800px) och -m (1600px) i mappen är gamla nedladdningsvarianter med EXIF kvar. +// De importeras INTE längre (då hamnar de inte i dist/); nedladdningsstorlekarna +// tas i stället fram av Astros bildtjänst nedan, vilket också rensar bort EXIF +// (GPS, kamerans serienummer, Artist m.m.). +const imageFiles = import.meta.glob( + [ + '../assets/images/portfolio/*.{jpeg,jpg,png,webp,JPG,JPEG,PNG}', + '!../assets/images/portfolio/*-{s,m}.{jpeg,jpg,png,webp,JPG,JPEG,PNG}', + ], + { eager: true } ); -// Karta assetId -> { s, m } med Astros hashade URL:er, sa klienten kan lanka -// till ratt nedladdningsvariant. Large ar galleribilden sjalv, original gar via proxy. -const downloadVariants: Record> = {}; -for (const [pathKey, file] of Object.entries(allImageFiles) as [string, any][]) { - const m = pathKey.match(/immich-([0-9a-f-]{36})-.*-([sm])\.(?:jpe?g|png|webp)$/i); - if (!m) continue; - const id = m[1].toLowerCase(); - (downloadVariants[id] ||= {})[m[2].toLowerCase()] = file.default.src; -} +// OBS: läs aldrig .src/.width/.height direkt på de importerade bilderna här. +// Då räknar Astro originalet som "använt" och kopierar originalfilen (med EXIF) +// till dist/. Gå alltid via /getImage. function formatShutterSpeed(exposureTime: number) { if (!exposureTime) return null; @@ -34,40 +35,78 @@ function formatShutterSpeed(exposureTime: number) { return `1/${fraction}`; } +// Datum ur filnamnet: "immich--20260905-..." eller "20250510-...". +function dateFromFilename(name: string): Date | null { + const m = name.match(/(?:^|-)((?:19|20)\d{2})(\d{2})(\d{2})-/); + if (!m) return null; + const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])); + return isNaN(d.getTime()) ? null : d; +} + +const widthOf = (descriptor: string) => parseInt(descriptor, 10) || 0; + const galleryItems = await Promise.all(Object.entries(imageFiles).map(async ([relativePath, file]: [string, any]) => { - let exifData: any = {}; - let dateTaken = null; + let exifData: any = {}; + let dateTaken: Date | null = null; const absolutePath = path.resolve('./src/pages', relativePath); + const fileName = path.basename(relativePath); + // Stabil nyckel = filnamn utan ändelse. Används för alt-texter (portfolio-meta.ts) och statistik. + const key = fileName.replace(/\.[^.]+$/, ''); try { const fileBuffer = fs.readFileSync(absolutePath); exifData = await exifr.parse(fileBuffer, [ 'ISO', 'Model', 'FNumber', 'ExposureTime', 'FocalLength', 'DateTimeOriginal', 'CreateDate' ]); - dateTaken = exifData?.DateTimeOriginal || exifData?.CreateDate; - if (!dateTaken) dateTaken = new Date(); + const exifDate = exifData?.DateTimeOriginal || exifData?.CreateDate; + if (exifDate) dateTaken = new Date(exifDate); } catch (e) { console.error(`Kunde inte läsa EXIF för ${relativePath}`); - dateTaken = new Date(); } + if (!dateTaken || isNaN(dateTaken.getTime())) dateTaken = dateFromFilename(fileName); + const hasDate = !!dateTaken; + const dateObj = dateTaken ?? new Date(); - const dateObj = new Date(dateTaken); const year = dateObj.getFullYear().toString(); const month = dateObj.toLocaleString('sv-SE', { month: 'long' }).toLowerCase(); const monthIndex = dateObj.getMonth(); - - const fullDate = dateObj.toLocaleDateString('sv-SE', { - day: 'numeric', month: 'long', year: 'numeric' - }); + + const fullDate = hasDate + ? dateObj.toLocaleDateString('sv-SE', { day: 'numeric', month: 'long', year: 'numeric' }) + : ''; const shutterSpeed = formatShutterSpeed(exifData?.ExposureTime); const focalLength = exifData?.FocalLength ? `${Math.round(exifData.FocalLength)}mm` : null; const fStop = exifData?.FNumber ? `f/${exifData.FNumber}` : null; const iso = exifData?.ISO ? `ISO ${exifData.ISO}` : null; + // Alt-text: riktig text från portfolio-meta.ts om den finns, annars en neutral + // svensk reservtext. Aldrig kameramodell, aldrig "undefined". + const meta = portfolioMeta[key] ?? {}; + const fallbackAlt = [ + meta.category ? `${meta.category}:` : null, + `fotografi av ${business.person.name}`, + fullDate ? `taget ${fullDate}` : null, + ].filter(Boolean).join(' '); + const alt = meta.alt?.trim() || fallbackAlt.charAt(0).toUpperCase() + fallbackAlt.slice(1); + + // Nedladdning/lightbox: nya, EXIF-fria JPG:er i 800/1600/3000 px + // (Astro hoppar över bredder större än originalet). + const jpg = await getImage({ src: file.default, format: 'jpg', quality: 88, width: 800, widths: [800, 1600, 3000] }); + const variants = [...jpg.srcSet.values].sort((a, b) => widthOf(a.descriptor ?? '') - widthOf(b.descriptor ?? '')); + const small = variants[0]?.url ?? jpg.src; + const large = variants[variants.length - 1]?.url ?? jpg.src; + const medium = variants.find((v) => widthOf(v.descriptor ?? '') === 1600)?.url ?? large; + + const assetId = key.match(/^immich-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})-/i)?.[1]?.toLowerCase() ?? null; + const statKey = ('portfolio/' + key.replace(/[^\w\-.]/g, '_')).slice(0, 150); + return { - ...file, year, month, monthIndex, fullDate, model: exifData?.Model, - fStop, shutterSpeed, iso, focalLength + img: file.default, key, alt, title: meta.title ?? null, + year, month, monthIndex, fullDate, model: exifData?.Model, + fStop, shutterSpeed, iso, focalLength, + dlS: small, dlM: medium, dlL: large, srcset: jpg.srcSet.attribute, + assetId, statKey, }; })); @@ -78,7 +117,13 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value .map((item: any) => item.name); --- - +
@@ -88,32 +133,44 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value

Portfolio

-

A curated selection of my work across different styles.

+

Ett urval av mina bilder i olika stilar – {genreList()}. + Vill du boka en fotografering? Läs om tjänster och bokning.

- + {uniqueYears.map(year => ( - + ))}
- + {uniqueMonths.map(month => ( - + ))}
+

Bilder

- Portfolio Grid + Portfolio Grid
More
- 3D Scroll Effect + 3D Scroll Effect
At
- Hero Section + Hero Section
Imagesbyolofsson.se