From d18319d5931f980903ffdb73104b0d7fca7d31ee Mon Sep 17 00:00:00 2001 From: Padreug Date: Wed, 27 May 2026 20:54:16 +0200 Subject: [PATCH] refactor: serve project images through Vite's asset pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves public/images/ → src/assets/projects// and switches projects.ts to resolve image URLs via import.meta.glob with the ?url query. Vite content-hashes every file (e.g. 08-DzZjAiN9.jpg); the HomeView hero img also imports through the pipeline. Why: the deploy serves /assets/* with `cache-control: public, immutable, max-age=31536000`. That's correct for content-hashed files but was being applied to /images/* too, so swapping an image in place left every cached visitor stuck on the old version for a year. Hashed filenames means every byte-level change produces a new URL, browsers fetch fresh automatically, and the immutable cache stays correct. The img(slug, file) helper throws at build time if a referenced asset is missing — typos surface immediately instead of producing a broken at runtime. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../assets/projects}/asheville/01-living.jpg | Bin .../assets/projects}/asheville/02-kitchen.jpg | Bin .../assets/projects}/asheville/03-bath.jpg | Bin .../projects}/asheville/04-dining-wide.jpg | Bin .../projects}/asheville/05-dining-detail.jpg | Bin .../assets/projects}/boulder/01.jpg | Bin .../assets/projects}/boulder/02.jpg | Bin .../assets/projects}/boulder/03.jpg | Bin .../assets/projects}/boulder/04.jpg | Bin .../assets/projects}/boulder/05.jpg | Bin .../assets/projects}/boulder/06.jpg | Bin .../assets/projects}/boulder/07.jpg | Bin .../assets/projects}/boulder/08.jpg | Bin .../assets/projects}/boulder/09.jpg | Bin .../assets/projects}/boulder/10.jpg | Bin .../assets/projects}/boulder/11.jpg | Bin .../assets/projects}/boulder/12.jpg | Bin src/data/projects.ts | 56 ++++++++++++------ src/views/HomeView.vue | 3 +- 19 files changed, 39 insertions(+), 20 deletions(-) rename {public/images => src/assets/projects}/asheville/01-living.jpg (100%) rename {public/images => src/assets/projects}/asheville/02-kitchen.jpg (100%) rename {public/images => src/assets/projects}/asheville/03-bath.jpg (100%) rename {public/images => src/assets/projects}/asheville/04-dining-wide.jpg (100%) rename {public/images => src/assets/projects}/asheville/05-dining-detail.jpg (100%) rename {public/images => src/assets/projects}/boulder/01.jpg (100%) rename {public/images => src/assets/projects}/boulder/02.jpg (100%) rename {public/images => src/assets/projects}/boulder/03.jpg (100%) rename {public/images => src/assets/projects}/boulder/04.jpg (100%) rename {public/images => src/assets/projects}/boulder/05.jpg (100%) rename {public/images => src/assets/projects}/boulder/06.jpg (100%) rename {public/images => src/assets/projects}/boulder/07.jpg (100%) rename {public/images => src/assets/projects}/boulder/08.jpg (100%) rename {public/images => src/assets/projects}/boulder/09.jpg (100%) rename {public/images => src/assets/projects}/boulder/10.jpg (100%) rename {public/images => src/assets/projects}/boulder/11.jpg (100%) rename {public/images => src/assets/projects}/boulder/12.jpg (100%) diff --git a/public/images/asheville/01-living.jpg b/src/assets/projects/asheville/01-living.jpg similarity index 100% rename from public/images/asheville/01-living.jpg rename to src/assets/projects/asheville/01-living.jpg diff --git a/public/images/asheville/02-kitchen.jpg b/src/assets/projects/asheville/02-kitchen.jpg similarity index 100% rename from public/images/asheville/02-kitchen.jpg rename to src/assets/projects/asheville/02-kitchen.jpg diff --git a/public/images/asheville/03-bath.jpg b/src/assets/projects/asheville/03-bath.jpg similarity index 100% rename from public/images/asheville/03-bath.jpg rename to src/assets/projects/asheville/03-bath.jpg diff --git a/public/images/asheville/04-dining-wide.jpg b/src/assets/projects/asheville/04-dining-wide.jpg similarity index 100% rename from public/images/asheville/04-dining-wide.jpg rename to src/assets/projects/asheville/04-dining-wide.jpg diff --git a/public/images/asheville/05-dining-detail.jpg b/src/assets/projects/asheville/05-dining-detail.jpg similarity index 100% rename from public/images/asheville/05-dining-detail.jpg rename to src/assets/projects/asheville/05-dining-detail.jpg diff --git a/public/images/boulder/01.jpg b/src/assets/projects/boulder/01.jpg similarity index 100% rename from public/images/boulder/01.jpg rename to src/assets/projects/boulder/01.jpg diff --git a/public/images/boulder/02.jpg b/src/assets/projects/boulder/02.jpg similarity index 100% rename from public/images/boulder/02.jpg rename to src/assets/projects/boulder/02.jpg diff --git a/public/images/boulder/03.jpg b/src/assets/projects/boulder/03.jpg similarity index 100% rename from public/images/boulder/03.jpg rename to src/assets/projects/boulder/03.jpg diff --git a/public/images/boulder/04.jpg b/src/assets/projects/boulder/04.jpg similarity index 100% rename from public/images/boulder/04.jpg rename to src/assets/projects/boulder/04.jpg diff --git a/public/images/boulder/05.jpg b/src/assets/projects/boulder/05.jpg similarity index 100% rename from public/images/boulder/05.jpg rename to src/assets/projects/boulder/05.jpg diff --git a/public/images/boulder/06.jpg b/src/assets/projects/boulder/06.jpg similarity index 100% rename from public/images/boulder/06.jpg rename to src/assets/projects/boulder/06.jpg diff --git a/public/images/boulder/07.jpg b/src/assets/projects/boulder/07.jpg similarity index 100% rename from public/images/boulder/07.jpg rename to src/assets/projects/boulder/07.jpg diff --git a/public/images/boulder/08.jpg b/src/assets/projects/boulder/08.jpg similarity index 100% rename from public/images/boulder/08.jpg rename to src/assets/projects/boulder/08.jpg diff --git a/public/images/boulder/09.jpg b/src/assets/projects/boulder/09.jpg similarity index 100% rename from public/images/boulder/09.jpg rename to src/assets/projects/boulder/09.jpg diff --git a/public/images/boulder/10.jpg b/src/assets/projects/boulder/10.jpg similarity index 100% rename from public/images/boulder/10.jpg rename to src/assets/projects/boulder/10.jpg diff --git a/public/images/boulder/11.jpg b/src/assets/projects/boulder/11.jpg similarity index 100% rename from public/images/boulder/11.jpg rename to src/assets/projects/boulder/11.jpg diff --git a/public/images/boulder/12.jpg b/src/assets/projects/boulder/12.jpg similarity index 100% rename from public/images/boulder/12.jpg rename to src/assets/projects/boulder/12.jpg diff --git a/src/data/projects.ts b/src/data/projects.ts index f33eff6..bcf88b8 100644 --- a/src/data/projects.ts +++ b/src/data/projects.ts @@ -18,6 +18,24 @@ export interface Project { images: ProjectImage[] } +// Eager-glob every project image through Vite's asset pipeline so each +// file gets a content-hashed URL (e.g. /assets/08-abc123.jpg). Swapping +// any image's bytes changes its hash, which busts the immutable cache +// the deploy sets on /assets/*. Keys are paths relative to this file. +const assetMap = import.meta.glob( + '../assets/projects/**/*.jpg', + { eager: true, query: '?url', import: 'default' }, +) + +function img(slug: string, file: string): string { + const key = `../assets/projects/${slug}/${file}` + const url = assetMap[key] + if (!url) { + throw new Error(`Missing project asset: ${key} — check src/assets/projects/`) + } + return url +} + export const boulder: Project = { slug: 'boulder', name: 'Boulder', @@ -26,77 +44,77 @@ export const boulder: Project = { 'A mid-century ranch reimagined around warm woods, reclaimed barnwood walls, ' + 'live-edge counters, and emerald glazed tile. The palette stays in conversation ' + 'with the trees outside — sunlight does most of the work.', - cover: '/images/boulder/05.jpg', + cover: img('boulder', '05.jpg'), coverAlt: 'Walnut kitchen with white embossed tile and warm pendant light', images: [ { - src: '/images/boulder/03.jpg', + src: img('boulder', '03.jpg'), alt: 'Kitchen with reclaimed barnwood walls and live-edge bar top', orientation: 'landscape', feature: 'hero', }, { - src: '/images/boulder/01.jpg', + src: img('boulder', '01.jpg'), alt: 'Oil-rubbed bronze faucet over a quartz counter, reclaimed wood backsplash', orientation: 'portrait', feature: 'narrow', }, { - src: '/images/boulder/07.jpg', + src: img('boulder', '07.jpg'), alt: 'Kitchen alternate angle showing live-edge counter and Asian carving', orientation: 'landscape', feature: 'wide', }, { - src: '/images/boulder/02.jpg', + src: img('boulder', '02.jpg'), alt: 'Dining room with reclaimed barnwood feature wall and rush-seat chairs', orientation: 'landscape', feature: 'wide', }, { - src: '/images/boulder/08.jpg', + src: img('boulder', '08.jpg'), alt: 'Living room with mid-century sofa and emerald tile fireplace surround', orientation: 'landscape', feature: 'wide', }, { - src: '/images/boulder/05.jpg', + src: img('boulder', '05.jpg'), alt: 'Walnut cabinets with white embossed tile and glass pendant', orientation: 'portrait', feature: 'narrow', }, { - src: '/images/boulder/09.jpg', + src: img('boulder', '09.jpg'), alt: 'Bathroom with emerald subway tile shower and walnut vanity', orientation: 'portrait', feature: 'paired', }, { - src: '/images/boulder/06.jpg', + src: img('boulder', '06.jpg'), alt: 'Emerald subway tile shower with bronze fittings and white niche', orientation: 'landscape', feature: 'paired', }, { - src: '/images/boulder/12.jpg', + src: img('boulder', '12.jpg'), alt: 'Walnut bath vanity with reclaimed wood mirror and white tile backsplash', orientation: 'portrait', feature: 'narrow', }, { - src: '/images/boulder/10.jpg', + src: img('boulder', '10.jpg'), alt: 'Rear deck and patio at golden hour with mountain view', orientation: 'landscape', feature: 'wide', }, { - src: '/images/boulder/11.jpg', + src: img('boulder', '11.jpg'), alt: 'Rear deck at dusk with warm lit windows', orientation: 'landscape', feature: 'wide', }, { - src: '/images/boulder/04.jpg', + src: img('boulder', '04.jpg'), alt: 'Front of the home under a winter moon at dusk', orientation: 'landscape', feature: 'wide', @@ -112,35 +130,35 @@ export const asheville: Project = { 'A counterpoint to Boulder — black vertical slats, matte black appliances, ' + 'and large picture windows held in balance by warm wood floors and layered ' + 'textiles. A space that is restrained but never cold.', - cover: '/images/asheville/01-living.jpg', + cover: img('asheville', '01-living.jpg'), coverAlt: 'Living room with black vertical slat wall and picture window', images: [ { - src: '/images/asheville/01-living.jpg', + src: img('asheville', '01-living.jpg'), alt: 'Living room with black vertical slat wall and oversized window', orientation: 'portrait', feature: 'hero', }, { - src: '/images/asheville/02-kitchen.jpg', + src: img('asheville', '02-kitchen.jpg'), alt: 'Galley kitchen with matte black appliances and layered Persian rug', orientation: 'portrait', feature: 'narrow', }, { - src: '/images/asheville/04-dining-wide.jpg', + src: img('asheville', '04-dining-wide.jpg'), alt: 'Dining nook with vertical slat wall and trailing monstera', orientation: 'portrait', feature: 'narrow', }, { - src: '/images/asheville/05-dining-detail.jpg', + src: img('asheville', '05-dining-detail.jpg'), alt: 'Dining table beneath a Nelson Saucer pendant against vertical slat wall', orientation: 'portrait', feature: 'narrow', }, { - src: '/images/asheville/03-bath.jpg', + src: img('asheville', '03-bath.jpg'), alt: 'Powder bath with layered glazed tile and matte black accents', orientation: 'portrait', feature: 'narrow', diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index dd4a22b..134ffea 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -6,6 +6,7 @@ import { AspectRatio } from '@/components/ui/aspect-ratio' import { Button } from '@/components/ui/button' import Wordmark from '@/components/layout/Wordmark.vue' import { projects } from '@/data/projects' +import heroImage from '@/assets/projects/boulder/08.jpg' const { t } = useI18n({ useScope: 'global' }) @@ -25,7 +26,7 @@ const projectCards = computed(() =>