feat(image): enhance image handling with focal points and responsive support
Deploy / verify (push) Failing after 14s
Deploy / deploy (push) Has been skipped

- 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:
Peter Meier
2026-04-18 00:42:33 +02:00
parent 4d79bf08da
commit a139372ce2
12 changed files with 734 additions and 61 deletions
+30 -16
View File
@@ -9,7 +9,7 @@ import {
} from "./cms";
import type { RowContentLayout } from "./block-types";
import type { CalendarItemData } from "./block-types";
import { ensureTransformedImage } from "./rusty-image";
import { ensureTransformedImage, extractCmsImageField, type CmsImageField } from "./rusty-image";
/** Aus der Tag-API: Anzeigename plus optionales Icon und Farbe (CMS-Felder icon / color). */
export type TagMeta = {
@@ -124,18 +124,28 @@ export function filterPostsByTag(
export function getPostImageUrl(
postImage: PostEntry["postImage"],
): string | null {
if (!postImage || typeof postImage === "string") return null;
return getPostImageField(postImage)?.url ?? null;
}
/**
* URL + Focal + Alt aus post.postImage. Unterstützt RustyCMS-image-Typ
* und legacy file.url / fields.file.url.
*/
export function getPostImageField(
postImage: PostEntry["postImage"],
): CmsImageField | null {
if (!postImage) return null;
if (typeof postImage === "string") return extractCmsImageField(postImage);
const field = extractCmsImageField(postImage);
if (field) return field;
const obj = postImage as {
src?: string;
file?: { url?: string };
fields?: { file?: { url?: string } };
};
const url = obj?.src ?? obj?.file?.url ?? obj?.fields?.file?.url;
return typeof url === "string"
? url.startsWith("//")
? `https:${url}`
: url
: null;
const legacy = obj?.fields?.file?.url;
if (typeof legacy === "string" && legacy.trim()) {
return { url: legacy.startsWith("//") ? `https:${legacy}` : legacy };
}
return null;
}
/** Filtert Posts: behält nur solche, die mindestens einen der Tag-Slugs haben. */
@@ -237,14 +247,16 @@ export async function loadPostsList(opts: {
const tagsMap = await getTagsMap();
for (const p of posts) resolvePostTagsInPost(p, tagsMap);
for (const post of posts) {
const raw = getPostImageUrl(post.postImage);
if (raw) {
const field = getPostImageField(post.postImage);
if (field) {
try {
const url = await ensureTransformedImage(raw, {
const url = await ensureTransformedImage(field.url, {
width: 400,
height: 267,
fit: "cover",
format: "webp",
focalX: field.focal?.x,
focalY: field.focal?.y,
});
(post as PostEntry & { _resolvedImageUrl?: string })._resolvedImageUrl = url;
} catch {
@@ -417,14 +429,16 @@ export async function resolvePostOverviewBlocks(
for (const post of posts) resolvePostTagsInPost(post, tagsMap);
}
for (const post of item.postsResolved as PostEntry[]) {
const raw = getPostImageUrl(post.postImage);
if (raw) {
const field = getPostImageField(post.postImage);
if (field) {
try {
const url = await ensureTransformedImage(raw, {
const url = await ensureTransformedImage(field.url, {
width: 400,
height: 267,
fit: "cover",
format: "webp",
focalX: field.focal?.x,
focalY: field.focal?.y,
});
(
post as PostEntry & { _resolvedImageUrl?: string }