feat: add hero image processing and API for serving variants
Some checks failed
Code Analysis and Production Deploy / analyze (push) Failing after 5m44s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Has been skipped

- Introduced a new script `prepare-hero-variants.mjs` to generate responsive hero image variants in WebP format.
- Added a fallback image `hero_fallback.png` for each variant.
- Created an API endpoint `hero-images.get.js` to retrieve available hero image variants and their fallback images.
- Implemented directory and file checks to ensure the existence of required images before serving.
This commit is contained in:
Torsten Schulz (local)
2026-05-31 14:07:14 +02:00
parent 7c93966878
commit 6983186caf
45 changed files with 422 additions and 28 deletions

View File

@@ -9,12 +9,13 @@
<div class="absolute bottom-0 left-0 w-96 h-96 bg-gray-300/30 rounded-full blur-3xl" />
<picture class="absolute inset-0 opacity-10">
<source
v-if="heroImage.mobileWebp && heroImage.desktopWebp"
type="image/webp"
srcset="/images/club_about_us_hero_960.webp 960w, /images/club_about_us_hero_1600.webp 1600w"
:srcset="`${heroImage.mobileWebp} 960w, ${heroImage.desktopWebp} 1600w`"
sizes="(max-width: 1024px) 960px, 1600px"
>
<img
src="/images/club_about_us.png"
:src="heroImage.fallback"
alt=""
aria-hidden="true"
class="w-full h-full object-cover"
@@ -42,6 +43,80 @@
</template>
<script setup>
import { computed } from 'vue'
import { useFetch, useHead, useState } from '#imports'
function buildInlineFallback() {
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 900" preserveAspectRatio="xMidYMid slice">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#eef2f7" />
<stop offset="100%" stop-color="#d8e0ea" />
</linearGradient>
</defs>
<rect width="1600" height="900" fill="url(#g)" />
</svg>`
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg)}`
}
const DEFAULT_HERO_IMAGE = {
key: 'fallback',
mobileWebp: '',
desktopWebp: '',
fallback: buildInlineFallback()
}
const { data: heroImagesResponse } = await useFetch('/api/hero-images')
const heroVariants = computed(() => {
const variants = heroImagesResponse.value?.variants
return Array.isArray(variants) && variants.length ? variants : [DEFAULT_HERO_IMAGE]
})
function pickRandomHeroImage(variants) {
const list = Array.isArray(variants) && variants.length ? variants : [DEFAULT_HERO_IMAGE]
const index = Math.floor(Math.random() * list.length)
return list[index]
}
const heroImageState = useState('home-hero-image', () => pickRandomHeroImage(heroVariants.value))
if (!heroVariants.value.some((variant) => variant.key === heroImageState.value?.key)) {
heroImageState.value = pickRandomHeroImage(heroVariants.value)
}
const heroImage = computed(() => heroImageState.value)
const preloadLinks = computed(() => {
const links = []
if (heroImage.value.mobileWebp) {
links.push({
rel: 'preload',
as: 'image',
href: heroImage.value.mobileWebp,
type: 'image/webp',
media: '(max-width: 1024px)'
})
}
if (heroImage.value.desktopWebp) {
links.push({
rel: 'preload',
as: 'image',
href: heroImage.value.desktopWebp,
type: 'image/webp',
media: '(min-width: 1025px)'
})
}
return links
})
useHead(() => ({
link: preloadLinks.value
}))
const foundingYear = 1954
const yearsSinceFounding = new Date().getFullYear() - foundingYear
</script>