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:
@@ -20,7 +20,20 @@
|
||||
seoDescription?: string;
|
||||
socialImage?: string;
|
||||
breadcrumbItems?: { href?: string; label: string }[];
|
||||
topBanner?: { banner: unknown; resolvedImages: string[]; headline?: string; subheadline?: string } | null;
|
||||
topBanner?: {
|
||||
banner: unknown;
|
||||
resolvedImages: string[];
|
||||
headline?: string;
|
||||
subheadline?: string;
|
||||
responsive?: {
|
||||
sources: { type: string; srcset: string }[];
|
||||
fallback: string;
|
||||
width: number;
|
||||
height: number;
|
||||
lqip?: string;
|
||||
} | null;
|
||||
focalCss?: string | null;
|
||||
} | null;
|
||||
});
|
||||
|
||||
const baseUrl = $derived(data.siteBaseUrl ?? '');
|
||||
@@ -153,6 +166,8 @@
|
||||
<TopBanner
|
||||
banner={topBannerData.banner as import('$lib/cms').FullwidthBannerEntry | null}
|
||||
resolvedImages={topBannerData.resolvedImages}
|
||||
responsive={topBannerData.responsive ?? null}
|
||||
focalCss={topBannerData.focalCss ?? null}
|
||||
headline={topBannerData.headline}
|
||||
subheadline={topBannerData.subheadline}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getPageBySlug, getPages, getHomePageSlugFromConfig } from '$lib/cms';
|
||||
import { resolveContentImages, resolveImageUrls } from '$lib/rusty-image';
|
||||
import {
|
||||
extractCmsImageField,
|
||||
resolveContentImages,
|
||||
resolveImageUrls,
|
||||
} from '$lib/rusty-image';
|
||||
import { buildResponsiveImage, type ResponsiveImage } from '$lib/rusty-image.server';
|
||||
import {
|
||||
resolvePostOverviewBlocks,
|
||||
resolveSearchableTextBlocks,
|
||||
@@ -8,6 +13,13 @@ import {
|
||||
} from '$lib/blog-utils';
|
||||
import { DEFAULT_HOME_PAGE_SLUG, PAGE_RESOLVE, SITE_NAME } from '$lib/constants';
|
||||
|
||||
const BANNER_WIDTHS = [640, 960, 1280, 1600, 1920, 2400];
|
||||
|
||||
function focalToCss(focal: { x: number; y: number } | undefined): string | null {
|
||||
if (!focal) return null;
|
||||
return `${(focal.x * 100).toFixed(2)}% ${(focal.y * 100).toFixed(2)}%`;
|
||||
}
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
const translations = locals.translations ?? {};
|
||||
let homeSlug = DEFAULT_HOME_PAGE_SLUG;
|
||||
@@ -42,6 +54,8 @@ export const load: PageServerLoad = async ({ locals }) => {
|
||||
let topBanner: {
|
||||
banner: import('$lib/cms').FullwidthBannerEntry | null;
|
||||
resolvedImages: string[];
|
||||
responsive: ResponsiveImage | null;
|
||||
focalCss: string | null;
|
||||
headline?: string;
|
||||
subheadline?: string;
|
||||
} | null = null;
|
||||
@@ -54,7 +68,20 @@ export const load: PageServerLoad = async ({ locals }) => {
|
||||
? [(bannerRaw as { image?: unknown }).image]
|
||||
: [];
|
||||
let resolvedImages: string[] = [];
|
||||
let responsive: ResponsiveImage | null = null;
|
||||
let focalCss: string | null = null;
|
||||
if (bannerImages.length > 0) {
|
||||
const primary = extractCmsImageField(bannerImages[0]);
|
||||
if (primary?.url) {
|
||||
focalCss = focalToCss(primary.focal);
|
||||
responsive = await buildResponsiveImage(primary.url, {
|
||||
widths: BANNER_WIDTHS,
|
||||
fit: 'contain',
|
||||
formats: ['avif', 'webp', 'jpeg'],
|
||||
quality: 78,
|
||||
focal: primary.focal,
|
||||
});
|
||||
}
|
||||
resolvedImages = await resolveImageUrls(bannerImages as string[], {
|
||||
width: 2000,
|
||||
height: 750,
|
||||
@@ -65,6 +92,8 @@ export const load: PageServerLoad = async ({ locals }) => {
|
||||
topBanner = {
|
||||
banner: bannerRaw as import('$lib/cms').FullwidthBannerEntry | null,
|
||||
resolvedImages,
|
||||
responsive,
|
||||
focalCss,
|
||||
headline: page.headline ?? undefined,
|
||||
subheadline: page.subheadline ?? undefined,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getPageBySlug } from '$lib/cms';
|
||||
import { resolveContentImages, resolveImageUrls } from '$lib/rusty-image';
|
||||
import {
|
||||
extractCmsImageField,
|
||||
resolveContentImages,
|
||||
resolveImageUrls,
|
||||
} from '$lib/rusty-image';
|
||||
import { buildResponsiveImage, type ResponsiveImage } from '$lib/rusty-image.server';
|
||||
import {
|
||||
resolvePostOverviewBlocks,
|
||||
resolveSearchableTextBlocks,
|
||||
@@ -9,6 +14,13 @@ import {
|
||||
import { PAGE_RESOLVE } from '$lib/constants';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
const BANNER_WIDTHS = [640, 960, 1280, 1600, 1920, 2400];
|
||||
|
||||
function focalToCss(focal: { x: number; y: number } | undefined): string | null {
|
||||
if (!focal) return null;
|
||||
return `${(focal.x * 100).toFixed(2)}% ${(focal.y * 100).toFixed(2)}%`;
|
||||
}
|
||||
|
||||
export const load: PageServerLoad = async ({ params, locals }) => {
|
||||
const { slug } = params;
|
||||
const translations = locals.translations ?? {};
|
||||
@@ -29,6 +41,8 @@ export const load: PageServerLoad = async ({ params, locals }) => {
|
||||
|
||||
// Resolve top banner if present
|
||||
let topBannerResolvedImages: string[] = [];
|
||||
let topBannerResponsive: ResponsiveImage | null = null;
|
||||
let topBannerFocalCss: string | null = null;
|
||||
const bannerRaw = (page as { topFullwidthBanner?: unknown }).topFullwidthBanner;
|
||||
if (bannerRaw && typeof bannerRaw === 'object' && 'image' in bannerRaw) {
|
||||
const bannerImages = Array.isArray((bannerRaw as { image?: unknown }).image)
|
||||
@@ -37,6 +51,17 @@ export const load: PageServerLoad = async ({ params, locals }) => {
|
||||
? [(bannerRaw as { image?: unknown }).image]
|
||||
: [];
|
||||
if (bannerImages.length > 0) {
|
||||
const primary = extractCmsImageField(bannerImages[0]);
|
||||
if (primary?.url) {
|
||||
topBannerFocalCss = focalToCss(primary.focal);
|
||||
topBannerResponsive = await buildResponsiveImage(primary.url, {
|
||||
widths: BANNER_WIDTHS,
|
||||
fit: 'contain',
|
||||
formats: ['avif', 'webp', 'jpeg'],
|
||||
quality: 78,
|
||||
focal: primary.focal,
|
||||
});
|
||||
}
|
||||
topBannerResolvedImages = await resolveImageUrls(bannerImages as string[], {
|
||||
width: 2000,
|
||||
height: 750,
|
||||
@@ -59,6 +84,8 @@ export const load: PageServerLoad = async ({ params, locals }) => {
|
||||
? {
|
||||
banner: bannerRaw as import('$lib/cms').FullwidthBannerEntry | null,
|
||||
resolvedImages: topBannerResolvedImages,
|
||||
responsive: topBannerResponsive,
|
||||
focalCss: topBannerFocalCss,
|
||||
headline: page.headline ?? undefined,
|
||||
subheadline: page.subheadline ?? undefined,
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ export const GET: RequestHandler = async ({ url }) => {
|
||||
const format = url.searchParams.get('format');
|
||||
const fit = url.searchParams.get('fit');
|
||||
const ar = url.searchParams.get('ar');
|
||||
const fx = url.searchParams.get('fx');
|
||||
const fy = url.searchParams.get('fy');
|
||||
|
||||
if (w) {
|
||||
const n = Number(w);
|
||||
@@ -60,6 +62,14 @@ export const GET: RequestHandler = async ({ url }) => {
|
||||
if (format) params.format = format;
|
||||
if (fit) params.fit = fit;
|
||||
if (ar) params.ar = ar;
|
||||
if (fx) {
|
||||
const n = Number(fx);
|
||||
if (Number.isFinite(n)) params.fx = n;
|
||||
}
|
||||
if (fy) {
|
||||
const n = Number(fy);
|
||||
if (Number.isFinite(n)) params.fy = n;
|
||||
}
|
||||
|
||||
const key = buildCacheKey(src, params);
|
||||
|
||||
@@ -80,6 +90,8 @@ export const GET: RequestHandler = async ({ url }) => {
|
||||
if (params.format) transformParams.set('format', params.format);
|
||||
if (params.fit) transformParams.set('fit', params.fit);
|
||||
if (params.ar) transformParams.set('ar', params.ar);
|
||||
if (params.fx != null) transformParams.set('fx', String(params.fx));
|
||||
if (params.fy != null) transformParams.set('fy', String(params.fy));
|
||||
|
||||
const transformUrl = `${cmsBase}/api/transform?${transformParams.toString()}`;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
resolvePostTagsInPost,
|
||||
resolvePostOverviewBlocks,
|
||||
resolveSearchableTextBlocks,
|
||||
getPostImageUrl,
|
||||
getPostImageField,
|
||||
formatPostDate,
|
||||
} from '$lib/blog-utils';
|
||||
import { POST_RESOLVE } from '$lib/constants';
|
||||
@@ -36,12 +36,14 @@ export const load: PageServerLoad = async ({ params, locals, url }) => {
|
||||
await resolveSearchableTextBlocks(post, tagsMap);
|
||||
resolvePostTagsInPost(post, tagsMap);
|
||||
|
||||
const rawPostImageUrl = getPostImageUrl(post.postImage);
|
||||
const postImageUrl = rawPostImageUrl
|
||||
? await ensureTransformedImage(rawPostImageUrl, {
|
||||
const postImageField = getPostImageField(post.postImage);
|
||||
const postImageUrl = postImageField
|
||||
? await ensureTransformedImage(postImageField.url, {
|
||||
width: 150,
|
||||
height: 200,
|
||||
fit: 'cover',
|
||||
focalX: postImageField.focal?.x,
|
||||
focalY: postImageField.focal?.y,
|
||||
})
|
||||
: null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user