feat(image): enhance image handling with focal points and responsive support
- Added support for extracting focal points and alt text from CMS image fields. - Introduced responsive image handling in TopBanner component, allowing for AVIF/WebP sources and LQIP placeholders. - Updated image transformation functions to accommodate focal point parameters for better cropping. - Refactored image URL resolution logic across various components and routes to utilize new image field structure.
This commit is contained in:
@@ -1,23 +1,67 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* TopBanner: Fullwidth-Banner mit Bild (über RustyCMS-Transform-API aufgelöst), optionalem Text
|
||||
* und Titel/Subtitle der aktuellen Page (Markdown).
|
||||
* TopBanner: Fullwidth-Banner mit Bild (über /cms-images aufgelöst), optionalem Text
|
||||
* und Titel/Subtitle der aktuellen Page (Markdown). Bevorzugt `<picture>` mit AVIF/WebP-Sources
|
||||
* und srcset, wenn `responsive` geliefert wird; sonst Fallback auf einzelnes `<img>` (legacy).
|
||||
*/
|
||||
import { marked } from "marked";
|
||||
import type { FullwidthBannerEntry } from "$lib/cms";
|
||||
|
||||
marked.setOptions({ gfm: true });
|
||||
|
||||
interface ResponsiveBannerImage {
|
||||
sources: { type: string; srcset: string }[];
|
||||
fallback: string;
|
||||
width: number;
|
||||
height: number;
|
||||
/** Optionaler LQIP (data-URL) als Unschärfe-Placeholder beim Laden. */
|
||||
lqip?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
banner: FullwidthBannerEntry | null;
|
||||
/** Einfache (legacy) einzelne URL(s) — wenn `responsive` fehlt. */
|
||||
resolvedImages?: string[];
|
||||
/** Bevorzugter Pfad: fertig berechneter `<picture>`-Inhalt fürs erste Banner-Bild. */
|
||||
responsive?: ResponsiveBannerImage | null;
|
||||
/** `sizes`-Attribut fürs srcset (Default: volle Viewport-Breite). */
|
||||
sizes?: string;
|
||||
/** Focal-Point als CSS-`object-position` (z. B. "69.5% 40.38%"). */
|
||||
focalCss?: string | null;
|
||||
/** Headline der Page (Markdown). */
|
||||
headline?: string;
|
||||
/** Subheadline der Page (Markdown). */
|
||||
subheadline?: string;
|
||||
}
|
||||
|
||||
let { banner = null, resolvedImages = [], headline, subheadline }: Props = $props();
|
||||
let {
|
||||
banner = null,
|
||||
resolvedImages = [],
|
||||
responsive = null,
|
||||
sizes = "100vw",
|
||||
focalCss = null,
|
||||
headline,
|
||||
subheadline,
|
||||
}: Props = $props();
|
||||
|
||||
const imgStyle = $derived(
|
||||
focalCss ? `object-position: ${focalCss};` : undefined,
|
||||
);
|
||||
|
||||
/**
|
||||
* LQIP: Blurhash-Placeholder als `background-image` auf dem Wrapper.
|
||||
* Kein Blur-Filter nötig — der aus 32×32 hochskalierte JPEG ist intrinsisch weich.
|
||||
* `background-position` folgt dem Focal, damit der Placeholder identisch zum Image croppt.
|
||||
*/
|
||||
const wrapperStyle = $derived.by(() => {
|
||||
const parts: string[] = [];
|
||||
if (responsive?.lqip) {
|
||||
parts.push(`background-image: url("${responsive.lqip}")`);
|
||||
parts.push("background-size: cover");
|
||||
parts.push(`background-position: ${focalCss ?? "center"}`);
|
||||
}
|
||||
return parts.length > 0 ? parts.join("; ") : undefined;
|
||||
});
|
||||
|
||||
const headlineHtml = $derived(
|
||||
headline?.trim() ? (marked.parseInline(headline) as string) : "",
|
||||
@@ -26,25 +70,54 @@
|
||||
subheadline?.trim() ? (marked.parseInline(subheadline) as string) : "",
|
||||
);
|
||||
|
||||
const hasImage = $derived(resolvedImages.length > 0 && Boolean(resolvedImages[0]?.trim()));
|
||||
const hasResponsive = $derived(
|
||||
responsive != null && responsive.fallback.trim().length > 0,
|
||||
);
|
||||
const hasImage = $derived(
|
||||
hasResponsive || (resolvedImages.length > 0 && Boolean(resolvedImages[0]?.trim())),
|
||||
);
|
||||
const hasText = $derived(banner?.text != null && banner.text !== "");
|
||||
const hasPageTitle = $derived((headline != null && headline !== "") || (subheadline != null && subheadline !== ""));
|
||||
const showBanner = $derived(hasImage || hasText || hasPageTitle);
|
||||
const altText = $derived(banner?.headline ?? headline ?? "");
|
||||
</script>
|
||||
|
||||
{#if showBanner}
|
||||
<div class="w-full">
|
||||
{#if hasImage}
|
||||
<div class="w-full overflow-hidden relative flex justify-center items-center shadow-lg border-b border-white">
|
||||
<img
|
||||
src={resolvedImages[0]}
|
||||
alt={banner?.headline ?? headline ?? ""}
|
||||
class="w-full object-cover max-h-[400px] lg:max-h-[600px] max-w-[1920px] aspect-square sm:aspect-video lg:aspect-24/9"
|
||||
width={1920}
|
||||
height={600}
|
||||
loading="eager"
|
||||
fetchpriority="high"
|
||||
/>
|
||||
<div
|
||||
class="w-full overflow-hidden relative flex justify-center items-center shadow-lg border-b border-white bg-zinc-200"
|
||||
style={wrapperStyle}
|
||||
>
|
||||
{#if hasResponsive && responsive}
|
||||
<picture>
|
||||
{#each responsive.sources as s (s.type)}
|
||||
<source type={s.type} srcset={s.srcset} sizes={sizes} />
|
||||
{/each}
|
||||
<img
|
||||
src={responsive.fallback}
|
||||
alt={altText}
|
||||
class="w-full object-cover max-h-[400px] lg:max-h-[600px] max-w-[1920px] aspect-square sm:aspect-video lg:aspect-24/9"
|
||||
width={responsive.width}
|
||||
height={responsive.height}
|
||||
style={imgStyle}
|
||||
loading="eager"
|
||||
fetchpriority="high"
|
||||
decoding="async"
|
||||
/>
|
||||
</picture>
|
||||
{:else}
|
||||
<img
|
||||
src={resolvedImages[0]}
|
||||
alt={altText}
|
||||
class="w-full object-cover max-h-[400px] lg:max-h-[600px] max-w-[1920px] aspect-square sm:aspect-video lg:aspect-24/9"
|
||||
width={1920}
|
||||
height={600}
|
||||
style={imgStyle}
|
||||
loading="eager"
|
||||
fetchpriority="high"
|
||||
/>
|
||||
{/if}
|
||||
{#if hasPageTitle}
|
||||
<div class="absolute inset-0 flex items-center justify-start ">
|
||||
<div class="container-custom mx-auto px-6 relative">
|
||||
|
||||
Reference in New Issue
Block a user