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:
+3
-1
@@ -28,7 +28,9 @@
|
|||||||
"@fontsource-variable/rubik": "^5.2.8",
|
"@fontsource-variable/rubik": "^5.2.8",
|
||||||
"@fontsource/inter": "^5.2.8",
|
"@fontsource/inter": "^5.2.8",
|
||||||
"@iconify/svelte": "^5.2.1",
|
"@iconify/svelte": "^5.2.1",
|
||||||
"marked": "^17.0.2"
|
"blurhash": "^2.0.5",
|
||||||
|
"marked": "^17.0.2",
|
||||||
|
"sharp": "^0.34.5"
|
||||||
},
|
},
|
||||||
"type": "module"
|
"type": "module"
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-16
@@ -9,7 +9,7 @@ import {
|
|||||||
} from "./cms";
|
} from "./cms";
|
||||||
import type { RowContentLayout } from "./block-types";
|
import type { RowContentLayout } from "./block-types";
|
||||||
import type { CalendarItemData } 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). */
|
/** Aus der Tag-API: Anzeigename plus optionales Icon und Farbe (CMS-Felder icon / color). */
|
||||||
export type TagMeta = {
|
export type TagMeta = {
|
||||||
@@ -124,18 +124,28 @@ export function filterPostsByTag(
|
|||||||
export function getPostImageUrl(
|
export function getPostImageUrl(
|
||||||
postImage: PostEntry["postImage"],
|
postImage: PostEntry["postImage"],
|
||||||
): string | null {
|
): 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 {
|
const obj = postImage as {
|
||||||
src?: string;
|
|
||||||
file?: { url?: string };
|
|
||||||
fields?: { file?: { url?: string } };
|
fields?: { file?: { url?: string } };
|
||||||
};
|
};
|
||||||
const url = obj?.src ?? obj?.file?.url ?? obj?.fields?.file?.url;
|
const legacy = obj?.fields?.file?.url;
|
||||||
return typeof url === "string"
|
if (typeof legacy === "string" && legacy.trim()) {
|
||||||
? url.startsWith("//")
|
return { url: legacy.startsWith("//") ? `https:${legacy}` : legacy };
|
||||||
? `https:${url}`
|
}
|
||||||
: url
|
return null;
|
||||||
: null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Filtert Posts: behält nur solche, die mindestens einen der Tag-Slugs haben. */
|
/** 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();
|
const tagsMap = await getTagsMap();
|
||||||
for (const p of posts) resolvePostTagsInPost(p, tagsMap);
|
for (const p of posts) resolvePostTagsInPost(p, tagsMap);
|
||||||
for (const post of posts) {
|
for (const post of posts) {
|
||||||
const raw = getPostImageUrl(post.postImage);
|
const field = getPostImageField(post.postImage);
|
||||||
if (raw) {
|
if (field) {
|
||||||
try {
|
try {
|
||||||
const url = await ensureTransformedImage(raw, {
|
const url = await ensureTransformedImage(field.url, {
|
||||||
width: 400,
|
width: 400,
|
||||||
height: 267,
|
height: 267,
|
||||||
fit: "cover",
|
fit: "cover",
|
||||||
format: "webp",
|
format: "webp",
|
||||||
|
focalX: field.focal?.x,
|
||||||
|
focalY: field.focal?.y,
|
||||||
});
|
});
|
||||||
(post as PostEntry & { _resolvedImageUrl?: string })._resolvedImageUrl = url;
|
(post as PostEntry & { _resolvedImageUrl?: string })._resolvedImageUrl = url;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -417,14 +429,16 @@ export async function resolvePostOverviewBlocks(
|
|||||||
for (const post of posts) resolvePostTagsInPost(post, tagsMap);
|
for (const post of posts) resolvePostTagsInPost(post, tagsMap);
|
||||||
}
|
}
|
||||||
for (const post of item.postsResolved as PostEntry[]) {
|
for (const post of item.postsResolved as PostEntry[]) {
|
||||||
const raw = getPostImageUrl(post.postImage);
|
const field = getPostImageField(post.postImage);
|
||||||
if (raw) {
|
if (field) {
|
||||||
try {
|
try {
|
||||||
const url = await ensureTransformedImage(raw, {
|
const url = await ensureTransformedImage(field.url, {
|
||||||
width: 400,
|
width: 400,
|
||||||
height: 267,
|
height: 267,
|
||||||
fit: "cover",
|
fit: "cover",
|
||||||
format: "webp",
|
format: "webp",
|
||||||
|
focalX: field.focal?.x,
|
||||||
|
focalY: field.focal?.y,
|
||||||
});
|
});
|
||||||
(
|
(
|
||||||
post as PostEntry & { _resolvedImageUrl?: string }
|
post as PostEntry & { _resolvedImageUrl?: string }
|
||||||
|
|||||||
@@ -1,23 +1,67 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
/**
|
/**
|
||||||
* TopBanner: Fullwidth-Banner mit Bild (über RustyCMS-Transform-API aufgelöst), optionalem Text
|
* TopBanner: Fullwidth-Banner mit Bild (über /cms-images aufgelöst), optionalem Text
|
||||||
* und Titel/Subtitle der aktuellen Page (Markdown).
|
* 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 { marked } from "marked";
|
||||||
import type { FullwidthBannerEntry } from "$lib/cms";
|
import type { FullwidthBannerEntry } from "$lib/cms";
|
||||||
|
|
||||||
marked.setOptions({ gfm: true });
|
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 {
|
interface Props {
|
||||||
banner: FullwidthBannerEntry | null;
|
banner: FullwidthBannerEntry | null;
|
||||||
|
/** Einfache (legacy) einzelne URL(s) — wenn `responsive` fehlt. */
|
||||||
resolvedImages?: string[];
|
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 der Page (Markdown). */
|
||||||
headline?: string;
|
headline?: string;
|
||||||
/** Subheadline der Page (Markdown). */
|
/** Subheadline der Page (Markdown). */
|
||||||
subheadline?: string;
|
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(
|
const headlineHtml = $derived(
|
||||||
headline?.trim() ? (marked.parseInline(headline) as string) : "",
|
headline?.trim() ? (marked.parseInline(headline) as string) : "",
|
||||||
@@ -26,25 +70,54 @@
|
|||||||
subheadline?.trim() ? (marked.parseInline(subheadline) as string) : "",
|
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 hasText = $derived(banner?.text != null && banner.text !== "");
|
||||||
const hasPageTitle = $derived((headline != null && headline !== "") || (subheadline != null && subheadline !== ""));
|
const hasPageTitle = $derived((headline != null && headline !== "") || (subheadline != null && subheadline !== ""));
|
||||||
const showBanner = $derived(hasImage || hasText || hasPageTitle);
|
const showBanner = $derived(hasImage || hasText || hasPageTitle);
|
||||||
|
const altText = $derived(banner?.headline ?? headline ?? "");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if showBanner}
|
{#if showBanner}
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
{#if hasImage}
|
{#if hasImage}
|
||||||
<div class="w-full overflow-hidden relative flex justify-center items-center shadow-lg border-b border-white">
|
<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
|
<img
|
||||||
src={resolvedImages[0]}
|
src={resolvedImages[0]}
|
||||||
alt={banner?.headline ?? headline ?? ""}
|
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"
|
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}
|
width={1920}
|
||||||
height={600}
|
height={600}
|
||||||
|
style={imgStyle}
|
||||||
loading="eager"
|
loading="eager"
|
||||||
fetchpriority="high"
|
fetchpriority="high"
|
||||||
/>
|
/>
|
||||||
|
{/if}
|
||||||
{#if hasPageTitle}
|
{#if hasPageTitle}
|
||||||
<div class="absolute inset-0 flex items-center justify-start ">
|
<div class="absolute inset-0 flex items-center justify-start ">
|
||||||
<div class="container-custom mx-auto px-6 relative">
|
<div class="container-custom mx-auto px-6 relative">
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export interface ImageTransformParams {
|
|||||||
format?: string;
|
format?: string;
|
||||||
fit?: string;
|
fit?: string;
|
||||||
ar?: string;
|
ar?: string;
|
||||||
|
fx?: number;
|
||||||
|
fy?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const memoryCache = new Map<string, CachedImage>();
|
const memoryCache = new Map<string, CachedImage>();
|
||||||
@@ -45,6 +47,10 @@ function ensureCacheDir(): string {
|
|||||||
* Deterministischer Cache-Key aus src + allen Transform-Parametern.
|
* Deterministischer Cache-Key aus src + allen Transform-Parametern.
|
||||||
* Gleiche Eingabe = gleicher Key, unabhängig von Reihenfolge.
|
* Gleiche Eingabe = gleicher Key, unabhängig von Reihenfolge.
|
||||||
*/
|
*/
|
||||||
|
function roundFocal(v: number): number {
|
||||||
|
return Math.round(v * 10000) / 10000;
|
||||||
|
}
|
||||||
|
|
||||||
export function buildCacheKey(src: string, params: ImageTransformParams): string {
|
export function buildCacheKey(src: string, params: ImageTransformParams): string {
|
||||||
const parts = [src];
|
const parts = [src];
|
||||||
if (params.w != null) parts.push(`w=${params.w}`);
|
if (params.w != null) parts.push(`w=${params.w}`);
|
||||||
@@ -53,6 +59,8 @@ export function buildCacheKey(src: string, params: ImageTransformParams): string
|
|||||||
if (params.format) parts.push(`f=${params.format}`);
|
if (params.format) parts.push(`f=${params.format}`);
|
||||||
if (params.fit) parts.push(`fit=${params.fit}`);
|
if (params.fit) parts.push(`fit=${params.fit}`);
|
||||||
if (params.ar) parts.push(`ar=${params.ar}`);
|
if (params.ar) parts.push(`ar=${params.ar}`);
|
||||||
|
if (params.fx != null) parts.push(`fx=${roundFocal(params.fx)}`);
|
||||||
|
if (params.fy != null) parts.push(`fy=${roundFocal(params.fy)}`);
|
||||||
return createHash('sha256').update(parts.join('|')).digest('hex').slice(0, 16);
|
return createHash('sha256').update(parts.join('|')).digest('hex').slice(0, 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
/**
|
||||||
|
* Server-only Bild-Helfer: Asset-Meta vom CMS, Blurhash-LQIP, Responsive-Srcset.
|
||||||
|
* Nur in `+page.server.ts` / `+layout.server.ts` importieren — nutzt sharp + blurhash + process.env.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { env } from '$env/dynamic/public';
|
||||||
|
import {
|
||||||
|
getCmsImageUrl,
|
||||||
|
type RustyImageTransformParams,
|
||||||
|
type CmsImageField,
|
||||||
|
} from './rusty-image';
|
||||||
|
|
||||||
|
function getCmsBaseUrl(): string {
|
||||||
|
return (env.PUBLIC_CMS_URL || process.env.PUBLIC_CMS_URL || 'http://localhost:3000').replace(
|
||||||
|
/\/$/,
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssetMeta {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
hasAlpha: boolean;
|
||||||
|
mimeType: string;
|
||||||
|
blurhash?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metaCache = new Map<string, Promise<AssetMeta | null>>();
|
||||||
|
|
||||||
|
function toMetaEndpoint(url: string): string | null {
|
||||||
|
try {
|
||||||
|
const u = new URL(url);
|
||||||
|
const base = getCmsBaseUrl();
|
||||||
|
const baseHost = new URL(base).host;
|
||||||
|
if (u.host !== baseHost && !/^127\.0\.0\.1|localhost/.test(u.host)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
u.searchParams.set('meta', 'true');
|
||||||
|
return `${base}${u.pathname}${u.search}`;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holt AssetInfo vom CMS: `/api/assets/<path>?meta=true`.
|
||||||
|
* Cached per URL für die Prozess-Lebensdauer.
|
||||||
|
*/
|
||||||
|
export async function fetchAssetMeta(url: string): Promise<AssetMeta | null> {
|
||||||
|
const endpoint = toMetaEndpoint(url);
|
||||||
|
if (!endpoint) return null;
|
||||||
|
const cached = metaCache.get(endpoint);
|
||||||
|
if (cached) return cached;
|
||||||
|
const p = (async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(endpoint);
|
||||||
|
if (!res.ok) return null;
|
||||||
|
const json = (await res.json()) as {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
has_alpha?: boolean;
|
||||||
|
mime_type?: string;
|
||||||
|
blurhash?: string;
|
||||||
|
};
|
||||||
|
if (
|
||||||
|
typeof json?.width !== 'number' ||
|
||||||
|
typeof json?.height !== 'number' ||
|
||||||
|
json.width <= 0 ||
|
||||||
|
json.height <= 0
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
width: json.width,
|
||||||
|
height: json.height,
|
||||||
|
hasAlpha: Boolean(json.has_alpha),
|
||||||
|
mimeType: json.mime_type ?? 'image/jpeg',
|
||||||
|
...(typeof json.blurhash === 'string' && json.blurhash.trim()
|
||||||
|
? { blurhash: json.blurhash }
|
||||||
|
: {}),
|
||||||
|
} satisfies AssetMeta;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
metaCache.set(endpoint, p);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blurhashCache = new Map<string, Promise<string | null>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dekodiert Blurhash → 32×32 RGBA → JPEG Q40 → base64 data-URL.
|
||||||
|
* Liefert kompakten LQIP (~300–500 B) für CSS-`background-image`.
|
||||||
|
*/
|
||||||
|
export async function blurhashToDataUrl(
|
||||||
|
hash: string,
|
||||||
|
width = 32,
|
||||||
|
height = 32,
|
||||||
|
): Promise<string | null> {
|
||||||
|
const key = `${hash}|${width}x${height}`;
|
||||||
|
const cached = blurhashCache.get(key);
|
||||||
|
if (cached) return cached;
|
||||||
|
const p = (async () => {
|
||||||
|
try {
|
||||||
|
const [{ decode }, sharpMod] = await Promise.all([
|
||||||
|
import('blurhash'),
|
||||||
|
import('sharp'),
|
||||||
|
]);
|
||||||
|
const pixels = decode(hash, width, height);
|
||||||
|
const sharp = sharpMod.default;
|
||||||
|
const jpeg = await sharp(Buffer.from(pixels), {
|
||||||
|
raw: { width, height, channels: 4 },
|
||||||
|
})
|
||||||
|
.jpeg({ quality: 40 })
|
||||||
|
.toBuffer();
|
||||||
|
return `data:image/jpeg;base64,${jpeg.toString('base64')}`;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
blurhashCache.set(key, p);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResponsiveImage {
|
||||||
|
sources: { type: string; srcset: string }[];
|
||||||
|
fallback: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
widths: number[];
|
||||||
|
lqip?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResponsiveImageOptions {
|
||||||
|
widths?: number[];
|
||||||
|
ar?: string;
|
||||||
|
fit?: RustyImageTransformParams['fit'];
|
||||||
|
formats?: Array<'avif' | 'webp' | 'jpeg'>;
|
||||||
|
quality?: number;
|
||||||
|
focal?: CmsImageField['focal'];
|
||||||
|
lqip?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_WIDTHS = [640, 960, 1280, 1600, 1920, 2400];
|
||||||
|
const DEFAULT_FORMATS: Array<'avif' | 'webp' | 'jpeg'> = ['avif', 'webp', 'jpeg'];
|
||||||
|
const MIME_FOR_FORMAT: Record<'avif' | 'webp' | 'jpeg', string> = {
|
||||||
|
avif: 'image/avif',
|
||||||
|
webp: 'image/webp',
|
||||||
|
jpeg: 'image/jpeg',
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseAr(ar: string | undefined): number | null {
|
||||||
|
if (!ar) return null;
|
||||||
|
const m = ar.match(/^(\d+(?:\.\d+)?)\s*[/:x]\s*(\d+(?:\.\d+)?)$/);
|
||||||
|
if (!m) return null;
|
||||||
|
const w = Number(m[1]);
|
||||||
|
const h = Number(m[2]);
|
||||||
|
if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) return null;
|
||||||
|
return w / h;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut `<picture>`-Daten (AVIF/WebP/JPEG srcset + Fallback + LQIP).
|
||||||
|
* Breiten werden gegen die echte Bildbreite aus `fetchAssetMeta` geclampt (kein Upscaling).
|
||||||
|
* Alle URLs zeigen auf `/cms-images?…` (erst bei Client-Request wird gerendert & gecached).
|
||||||
|
*/
|
||||||
|
export async function buildResponsiveImage(
|
||||||
|
url: string,
|
||||||
|
opts: ResponsiveImageOptions = {},
|
||||||
|
): Promise<ResponsiveImage | null> {
|
||||||
|
const meta = await fetchAssetMeta(url);
|
||||||
|
|
||||||
|
const widthsRaw = (opts.widths ?? DEFAULT_WIDTHS).filter((w) => Number.isFinite(w) && w > 0);
|
||||||
|
if (widthsRaw.length === 0) return null;
|
||||||
|
|
||||||
|
const maxW = meta?.width ?? Math.max(...widthsRaw);
|
||||||
|
const widths = [...new Set(widthsRaw.map((w) => Math.min(w, maxW)).filter((w) => w > 0))].sort(
|
||||||
|
(a, b) => a - b,
|
||||||
|
);
|
||||||
|
if (widths.length === 0) return null;
|
||||||
|
|
||||||
|
const ar =
|
||||||
|
parseAr(opts.ar) ??
|
||||||
|
(meta?.width && meta?.height ? meta.width / meta.height : null);
|
||||||
|
|
||||||
|
const formats = opts.formats ?? DEFAULT_FORMATS;
|
||||||
|
const fit = opts.fit ?? 'cover';
|
||||||
|
const quality = opts.quality ?? 78;
|
||||||
|
|
||||||
|
const baseParams: RustyImageTransformParams = {
|
||||||
|
fit,
|
||||||
|
quality,
|
||||||
|
...(opts.ar ? { ar: opts.ar } : {}),
|
||||||
|
...(opts.focal
|
||||||
|
? { focalX: opts.focal.x, focalY: opts.focal.y }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const sources = formats.map((fmt) => {
|
||||||
|
const srcset = widths
|
||||||
|
.map((w) => {
|
||||||
|
const u = getCmsImageUrl(url, { ...baseParams, width: w, format: fmt });
|
||||||
|
return `${u} ${w}w`;
|
||||||
|
})
|
||||||
|
.join(', ');
|
||||||
|
return { type: MIME_FOR_FORMAT[fmt], srcset };
|
||||||
|
});
|
||||||
|
|
||||||
|
const fallbackWidth = widths[widths.length - 1] ?? maxW;
|
||||||
|
const fallbackFormat: 'jpeg' | 'webp' = formats.includes('jpeg') ? 'jpeg' : 'webp';
|
||||||
|
const fallback = getCmsImageUrl(url, {
|
||||||
|
...baseParams,
|
||||||
|
width: fallbackWidth,
|
||||||
|
format: fallbackFormat,
|
||||||
|
});
|
||||||
|
|
||||||
|
const width = fallbackWidth;
|
||||||
|
const height = ar ? Math.round(width / ar) : meta?.height ?? Math.round(width * 0.5625);
|
||||||
|
|
||||||
|
const lqip =
|
||||||
|
opts.lqip !== false && meta?.blurhash
|
||||||
|
? ((await blurhashToDataUrl(meta.blurhash)) ?? undefined)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
sources,
|
||||||
|
fallback,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
widths,
|
||||||
|
...(lqip ? { lqip } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
+81
-22
@@ -3,30 +3,60 @@
|
|||||||
* Der Endpoint holt bei Bedarf vom CMS /api/transform und legt Bytes unter IMAGE_CACHE_DIR ab.
|
* Der Endpoint holt bei Bedarf vom CMS /api/transform und legt Bytes unter IMAGE_CACHE_DIR ab.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export interface CmsFocal {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CmsImageField {
|
||||||
|
url: string;
|
||||||
|
focal?: CmsFocal;
|
||||||
|
alt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalisiert CMS-Loopback-URLs (127.0.0.1) zur konfigurierten PUBLIC_CMS_URL.
|
* Extrahiert URL + Focal + Alt aus einem CMS-Image-Field (string oder {src, alt?, focal?}).
|
||||||
* Für Browser-seitigen Code (kein Node.js/fs nötig).
|
* Normalisiert Protokoll-lose URLs zu https:.
|
||||||
*/
|
*/
|
||||||
export function absoluteUrlFromCmsImageField(field: unknown): string | null {
|
export function extractCmsImageField(field: unknown): CmsImageField | null {
|
||||||
if (typeof field === 'string') {
|
if (typeof field === 'string') {
|
||||||
const s = field.trim();
|
const s = field.trim();
|
||||||
if (!s) return null;
|
if (!s) return null;
|
||||||
return s.startsWith('//') ? `https:${s}` : s;
|
return { url: s.startsWith('//') ? `https:${s}` : s };
|
||||||
}
|
}
|
||||||
if (field && typeof field === 'object') {
|
if (field && typeof field === 'object') {
|
||||||
const o = field as { src?: string; file?: { url?: string } };
|
const o = field as {
|
||||||
const u =
|
src?: string;
|
||||||
|
file?: { url?: string };
|
||||||
|
focal?: { x?: number; y?: number };
|
||||||
|
alt?: string;
|
||||||
|
};
|
||||||
|
const rawUrl =
|
||||||
typeof o.src === 'string' && o.src.trim()
|
typeof o.src === 'string' && o.src.trim()
|
||||||
? o.src
|
? o.src
|
||||||
: typeof o.file?.url === 'string' && o.file.url.trim()
|
: typeof o.file?.url === 'string' && o.file.url.trim()
|
||||||
? o.file.url
|
? o.file.url
|
||||||
: undefined;
|
: undefined;
|
||||||
if (!u) return null;
|
if (!rawUrl) return null;
|
||||||
return u.startsWith('//') ? `https:${u}` : u;
|
const url = rawUrl.startsWith('//') ? `https:${rawUrl}` : rawUrl;
|
||||||
|
const fx = typeof o.focal?.x === 'number' ? o.focal.x : undefined;
|
||||||
|
const fy = typeof o.focal?.y === 'number' ? o.focal.y : undefined;
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
...(fx != null && fy != null ? { focal: { x: fx, y: fy } } : {}),
|
||||||
|
...(typeof o.alt === 'string' ? { alt: o.alt } : {}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy: nur URL. Bevorzuge extractCmsImageField für Focal/Alt.
|
||||||
|
*/
|
||||||
|
export function absoluteUrlFromCmsImageField(field: unknown): string | null {
|
||||||
|
return extractCmsImageField(field)?.url ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RustyImageTransformParams {
|
export interface RustyImageTransformParams {
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
@@ -34,6 +64,14 @@ export interface RustyImageTransformParams {
|
|||||||
fit?: 'fill' | 'contain' | 'cover';
|
fit?: 'fill' | 'contain' | 'cover';
|
||||||
format?: 'jpeg' | 'png' | 'webp' | 'avif';
|
format?: 'jpeg' | 'png' | 'webp' | 'avif';
|
||||||
quality?: number;
|
quality?: number;
|
||||||
|
/** Focal-X in [0,1] (Default 0.5) — für fit:cover-Crops. */
|
||||||
|
focalX?: number;
|
||||||
|
/** Focal-Y in [0,1] (Default 0.5). */
|
||||||
|
focalY?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function roundFocal(v: number): number {
|
||||||
|
return Math.round(v * 10000) / 10000;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,6 +87,8 @@ export function getCmsImageUrl(src: string, params: RustyImageTransformParams =
|
|||||||
if (params.fit != null) searchParams.set('fit', params.fit);
|
if (params.fit != null) searchParams.set('fit', params.fit);
|
||||||
if (params.format != null) searchParams.set('format', params.format);
|
if (params.format != null) searchParams.set('format', params.format);
|
||||||
if (params.quality != null) searchParams.set('q', String(params.quality));
|
if (params.quality != null) searchParams.set('q', String(params.quality));
|
||||||
|
if (params.focalX != null) searchParams.set('fx', String(roundFocal(params.focalX)));
|
||||||
|
if (params.focalY != null) searchParams.set('fy', String(roundFocal(params.focalY)));
|
||||||
return `/cms-images?${searchParams.toString()}`;
|
return `/cms-images?${searchParams.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +104,7 @@ export async function ensureTransformedImage(url: string, params: RustyImageTran
|
|||||||
return getCmsImageUrl(url, params);
|
return getCmsImageUrl(url, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageArrayItem = string | { src?: string };
|
type ImageArrayItem = string | { src?: string; focal?: { x?: number; y?: number } };
|
||||||
|
|
||||||
function toImageUrl(item: ImageArrayItem): string | null {
|
function toImageUrl(item: ImageArrayItem): string | null {
|
||||||
if (typeof item === 'string') return item.startsWith('//') ? `https:${item}` : item;
|
if (typeof item === 'string') return item.startsWith('//') ? `https:${item}` : item;
|
||||||
@@ -74,10 +114,20 @@ function toImageUrl(item: ImageArrayItem): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveImageUrls(items: ImageArrayItem[], params: RustyImageTransformParams = {}): Promise<string[]> {
|
export async function resolveImageUrls(items: ImageArrayItem[], params: RustyImageTransformParams = {}): Promise<string[]> {
|
||||||
return items
|
const out: string[] = [];
|
||||||
.map(toImageUrl)
|
for (const item of items) {
|
||||||
.filter((u): u is string => u !== null && u.startsWith('http'))
|
const field = extractCmsImageField(item);
|
||||||
.map((u) => getCmsImageUrl(u, params));
|
if (!field || !field.url.startsWith('http')) continue;
|
||||||
|
out.push(
|
||||||
|
getCmsImageUrl(field.url, {
|
||||||
|
...params,
|
||||||
|
...(field.focal
|
||||||
|
? { focalX: params.focalX ?? field.focal.x, focalY: params.focalY ?? field.focal.y }
|
||||||
|
: {}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RowContentLayoutLike {
|
export interface RowContentLayoutLike {
|
||||||
@@ -169,9 +219,12 @@ export async function resolveContentImages(layout: RowContentLayoutLike, transfo
|
|||||||
|
|
||||||
if (block._type === 'image') {
|
if (block._type === 'image') {
|
||||||
const raw = block.image ?? block.img;
|
const raw = block.image ?? block.img;
|
||||||
const url = absoluteUrlFromCmsImageField(raw);
|
const field = extractCmsImageField(raw);
|
||||||
if (url) {
|
if (field) {
|
||||||
block.resolvedImageSrc = getCmsImageUrl(url, transformParams);
|
block.resolvedImageSrc = getCmsImageUrl(field.url, {
|
||||||
|
...transformParams,
|
||||||
|
...(field.focal ? { focalX: field.focal.x, focalY: field.focal.y } : {}),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -179,9 +232,12 @@ export async function resolveContentImages(layout: RowContentLayoutLike, transfo
|
|||||||
if (block._type === 'image_gallery' && Array.isArray(block.images)) {
|
if (block._type === 'image_gallery' && Array.isArray(block.images)) {
|
||||||
for (const img of block.images) {
|
for (const img of block.images) {
|
||||||
if (!img || typeof img !== 'object') continue;
|
if (!img || typeof img !== 'object') continue;
|
||||||
const url = absoluteUrlFromCmsImageField(img);
|
const field = extractCmsImageField(img);
|
||||||
if (url) {
|
if (field) {
|
||||||
(img as { resolvedSrc?: string }).resolvedSrc = getCmsImageUrl(url, galleryParams);
|
(img as { resolvedSrc?: string }).resolvedSrc = getCmsImageUrl(field.url, {
|
||||||
|
...galleryParams,
|
||||||
|
...(field.focal ? { focalX: field.focal.x, focalY: field.focal.y } : {}),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@@ -191,9 +247,12 @@ export async function resolveContentImages(layout: RowContentLayoutLike, transfo
|
|||||||
for (const org of block.organisations) {
|
for (const org of block.organisations) {
|
||||||
if (!org || typeof org !== 'object') continue;
|
if (!org || typeof org !== 'object') continue;
|
||||||
const o = org as { logo?: unknown; resolvedLogoSrc?: string };
|
const o = org as { logo?: unknown; resolvedLogoSrc?: string };
|
||||||
const url = absoluteUrlFromCmsImageField(o.logo);
|
const field = extractCmsImageField(o.logo);
|
||||||
if (url) {
|
if (field) {
|
||||||
o.resolvedLogoSrc = getCmsImageUrl(url, orgLogoParams);
|
o.resolvedLogoSrc = getCmsImageUrl(field.url, {
|
||||||
|
...orgLogoParams,
|
||||||
|
...(field.focal ? { focalX: field.focal.x, focalY: field.focal.y } : {}),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -20,7 +20,20 @@
|
|||||||
seoDescription?: string;
|
seoDescription?: string;
|
||||||
socialImage?: string;
|
socialImage?: string;
|
||||||
breadcrumbItems?: { href?: string; label: 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 ?? '');
|
const baseUrl = $derived(data.siteBaseUrl ?? '');
|
||||||
@@ -153,6 +166,8 @@
|
|||||||
<TopBanner
|
<TopBanner
|
||||||
banner={topBannerData.banner as import('$lib/cms').FullwidthBannerEntry | null}
|
banner={topBannerData.banner as import('$lib/cms').FullwidthBannerEntry | null}
|
||||||
resolvedImages={topBannerData.resolvedImages}
|
resolvedImages={topBannerData.resolvedImages}
|
||||||
|
responsive={topBannerData.responsive ?? null}
|
||||||
|
focalCss={topBannerData.focalCss ?? null}
|
||||||
headline={topBannerData.headline}
|
headline={topBannerData.headline}
|
||||||
subheadline={topBannerData.subheadline}
|
subheadline={topBannerData.subheadline}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import { getPageBySlug, getPages, getHomePageSlugFromConfig } from '$lib/cms';
|
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 {
|
import {
|
||||||
resolvePostOverviewBlocks,
|
resolvePostOverviewBlocks,
|
||||||
resolveSearchableTextBlocks,
|
resolveSearchableTextBlocks,
|
||||||
@@ -8,6 +13,13 @@ import {
|
|||||||
} from '$lib/blog-utils';
|
} from '$lib/blog-utils';
|
||||||
import { DEFAULT_HOME_PAGE_SLUG, PAGE_RESOLVE, SITE_NAME } from '$lib/constants';
|
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 }) => {
|
export const load: PageServerLoad = async ({ locals }) => {
|
||||||
const translations = locals.translations ?? {};
|
const translations = locals.translations ?? {};
|
||||||
let homeSlug = DEFAULT_HOME_PAGE_SLUG;
|
let homeSlug = DEFAULT_HOME_PAGE_SLUG;
|
||||||
@@ -42,6 +54,8 @@ export const load: PageServerLoad = async ({ locals }) => {
|
|||||||
let topBanner: {
|
let topBanner: {
|
||||||
banner: import('$lib/cms').FullwidthBannerEntry | null;
|
banner: import('$lib/cms').FullwidthBannerEntry | null;
|
||||||
resolvedImages: string[];
|
resolvedImages: string[];
|
||||||
|
responsive: ResponsiveImage | null;
|
||||||
|
focalCss: string | null;
|
||||||
headline?: string;
|
headline?: string;
|
||||||
subheadline?: string;
|
subheadline?: string;
|
||||||
} | null = null;
|
} | null = null;
|
||||||
@@ -54,7 +68,20 @@ export const load: PageServerLoad = async ({ locals }) => {
|
|||||||
? [(bannerRaw as { image?: unknown }).image]
|
? [(bannerRaw as { image?: unknown }).image]
|
||||||
: [];
|
: [];
|
||||||
let resolvedImages: string[] = [];
|
let resolvedImages: string[] = [];
|
||||||
|
let responsive: ResponsiveImage | null = null;
|
||||||
|
let focalCss: string | null = null;
|
||||||
if (bannerImages.length > 0) {
|
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[], {
|
resolvedImages = await resolveImageUrls(bannerImages as string[], {
|
||||||
width: 2000,
|
width: 2000,
|
||||||
height: 750,
|
height: 750,
|
||||||
@@ -65,6 +92,8 @@ export const load: PageServerLoad = async ({ locals }) => {
|
|||||||
topBanner = {
|
topBanner = {
|
||||||
banner: bannerRaw as import('$lib/cms').FullwidthBannerEntry | null,
|
banner: bannerRaw as import('$lib/cms').FullwidthBannerEntry | null,
|
||||||
resolvedImages,
|
resolvedImages,
|
||||||
|
responsive,
|
||||||
|
focalCss,
|
||||||
headline: page.headline ?? undefined,
|
headline: page.headline ?? undefined,
|
||||||
subheadline: page.subheadline ?? undefined,
|
subheadline: page.subheadline ?? undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import { getPageBySlug } from '$lib/cms';
|
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 {
|
import {
|
||||||
resolvePostOverviewBlocks,
|
resolvePostOverviewBlocks,
|
||||||
resolveSearchableTextBlocks,
|
resolveSearchableTextBlocks,
|
||||||
@@ -9,6 +14,13 @@ import {
|
|||||||
import { PAGE_RESOLVE } from '$lib/constants';
|
import { PAGE_RESOLVE } from '$lib/constants';
|
||||||
import { error } from '@sveltejs/kit';
|
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 }) => {
|
export const load: PageServerLoad = async ({ params, locals }) => {
|
||||||
const { slug } = params;
|
const { slug } = params;
|
||||||
const translations = locals.translations ?? {};
|
const translations = locals.translations ?? {};
|
||||||
@@ -29,6 +41,8 @@ export const load: PageServerLoad = async ({ params, locals }) => {
|
|||||||
|
|
||||||
// Resolve top banner if present
|
// Resolve top banner if present
|
||||||
let topBannerResolvedImages: string[] = [];
|
let topBannerResolvedImages: string[] = [];
|
||||||
|
let topBannerResponsive: ResponsiveImage | null = null;
|
||||||
|
let topBannerFocalCss: string | null = null;
|
||||||
const bannerRaw = (page as { topFullwidthBanner?: unknown }).topFullwidthBanner;
|
const bannerRaw = (page as { topFullwidthBanner?: unknown }).topFullwidthBanner;
|
||||||
if (bannerRaw && typeof bannerRaw === 'object' && 'image' in bannerRaw) {
|
if (bannerRaw && typeof bannerRaw === 'object' && 'image' in bannerRaw) {
|
||||||
const bannerImages = Array.isArray((bannerRaw as { image?: unknown }).image)
|
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]
|
? [(bannerRaw as { image?: unknown }).image]
|
||||||
: [];
|
: [];
|
||||||
if (bannerImages.length > 0) {
|
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[], {
|
topBannerResolvedImages = await resolveImageUrls(bannerImages as string[], {
|
||||||
width: 2000,
|
width: 2000,
|
||||||
height: 750,
|
height: 750,
|
||||||
@@ -59,6 +84,8 @@ export const load: PageServerLoad = async ({ params, locals }) => {
|
|||||||
? {
|
? {
|
||||||
banner: bannerRaw as import('$lib/cms').FullwidthBannerEntry | null,
|
banner: bannerRaw as import('$lib/cms').FullwidthBannerEntry | null,
|
||||||
resolvedImages: topBannerResolvedImages,
|
resolvedImages: topBannerResolvedImages,
|
||||||
|
responsive: topBannerResponsive,
|
||||||
|
focalCss: topBannerFocalCss,
|
||||||
headline: page.headline ?? undefined,
|
headline: page.headline ?? undefined,
|
||||||
subheadline: page.subheadline ?? undefined,
|
subheadline: page.subheadline ?? undefined,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ export const GET: RequestHandler = async ({ url }) => {
|
|||||||
const format = url.searchParams.get('format');
|
const format = url.searchParams.get('format');
|
||||||
const fit = url.searchParams.get('fit');
|
const fit = url.searchParams.get('fit');
|
||||||
const ar = url.searchParams.get('ar');
|
const ar = url.searchParams.get('ar');
|
||||||
|
const fx = url.searchParams.get('fx');
|
||||||
|
const fy = url.searchParams.get('fy');
|
||||||
|
|
||||||
if (w) {
|
if (w) {
|
||||||
const n = Number(w);
|
const n = Number(w);
|
||||||
@@ -60,6 +62,14 @@ export const GET: RequestHandler = async ({ url }) => {
|
|||||||
if (format) params.format = format;
|
if (format) params.format = format;
|
||||||
if (fit) params.fit = fit;
|
if (fit) params.fit = fit;
|
||||||
if (ar) params.ar = ar;
|
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);
|
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.format) transformParams.set('format', params.format);
|
||||||
if (params.fit) transformParams.set('fit', params.fit);
|
if (params.fit) transformParams.set('fit', params.fit);
|
||||||
if (params.ar) transformParams.set('ar', params.ar);
|
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()}`;
|
const transformUrl = `${cmsBase}/api/transform?${transformParams.toString()}`;
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
resolvePostTagsInPost,
|
resolvePostTagsInPost,
|
||||||
resolvePostOverviewBlocks,
|
resolvePostOverviewBlocks,
|
||||||
resolveSearchableTextBlocks,
|
resolveSearchableTextBlocks,
|
||||||
getPostImageUrl,
|
getPostImageField,
|
||||||
formatPostDate,
|
formatPostDate,
|
||||||
} from '$lib/blog-utils';
|
} from '$lib/blog-utils';
|
||||||
import { POST_RESOLVE } from '$lib/constants';
|
import { POST_RESOLVE } from '$lib/constants';
|
||||||
@@ -36,12 +36,14 @@ export const load: PageServerLoad = async ({ params, locals, url }) => {
|
|||||||
await resolveSearchableTextBlocks(post, tagsMap);
|
await resolveSearchableTextBlocks(post, tagsMap);
|
||||||
resolvePostTagsInPost(post, tagsMap);
|
resolvePostTagsInPost(post, tagsMap);
|
||||||
|
|
||||||
const rawPostImageUrl = getPostImageUrl(post.postImage);
|
const postImageField = getPostImageField(post.postImage);
|
||||||
const postImageUrl = rawPostImageUrl
|
const postImageUrl = postImageField
|
||||||
? await ensureTransformedImage(rawPostImageUrl, {
|
? await ensureTransformedImage(postImageField.url, {
|
||||||
width: 150,
|
width: 150,
|
||||||
height: 200,
|
height: 200,
|
||||||
fit: 'cover',
|
fit: 'cover',
|
||||||
|
focalX: postImageField.focal?.x,
|
||||||
|
focalY: postImageField.focal?.y,
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,13 @@
|
|||||||
"@emnapi/wasi-threads" "1.2.0"
|
"@emnapi/wasi-threads" "1.2.0"
|
||||||
tslib "^2.4.0"
|
tslib "^2.4.0"
|
||||||
|
|
||||||
|
"@emnapi/runtime@^1.7.0":
|
||||||
|
version "1.10.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.10.0.tgz#4b260c0d3534204e98c6110b8db1a987d26ec87c"
|
||||||
|
integrity sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.4.0"
|
||||||
|
|
||||||
"@emnapi/runtime@^1.7.1", "@emnapi/runtime@^1.8.1":
|
"@emnapi/runtime@^1.7.1", "@emnapi/runtime@^1.8.1":
|
||||||
version "1.9.0"
|
version "1.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.9.0.tgz#91c54a6e77c36154c125e873409472e2b70efd5b"
|
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.9.0.tgz#91c54a6e77c36154c125e873409472e2b70efd5b"
|
||||||
@@ -202,6 +209,153 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@iconify/types/-/types-2.0.0.tgz#ab0e9ea681d6c8a1214f30cd741fe3a20cc57f57"
|
resolved "https://registry.yarnpkg.com/@iconify/types/-/types-2.0.0.tgz#ab0e9ea681d6c8a1214f30cd741fe3a20cc57f57"
|
||||||
integrity sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==
|
integrity sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==
|
||||||
|
|
||||||
|
"@img/colour@^1.0.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@img/colour/-/colour-1.1.0.tgz#b0c2c2fa661adf75effd6b4964497cd80010bb9d"
|
||||||
|
integrity sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==
|
||||||
|
|
||||||
|
"@img/sharp-darwin-arm64@0.34.5":
|
||||||
|
version "0.34.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz#6e0732dcade126b6670af7aa17060b926835ea86"
|
||||||
|
integrity sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==
|
||||||
|
optionalDependencies:
|
||||||
|
"@img/sharp-libvips-darwin-arm64" "1.2.4"
|
||||||
|
|
||||||
|
"@img/sharp-darwin-x64@0.34.5":
|
||||||
|
version "0.34.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz#19bc1dd6eba6d5a96283498b9c9f401180ee9c7b"
|
||||||
|
integrity sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==
|
||||||
|
optionalDependencies:
|
||||||
|
"@img/sharp-libvips-darwin-x64" "1.2.4"
|
||||||
|
|
||||||
|
"@img/sharp-libvips-darwin-arm64@1.2.4":
|
||||||
|
version "1.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz#2894c0cb87d42276c3889942e8e2db517a492c43"
|
||||||
|
integrity sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==
|
||||||
|
|
||||||
|
"@img/sharp-libvips-darwin-x64@1.2.4":
|
||||||
|
version "1.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz#e63681f4539a94af9cd17246ed8881734386f8cc"
|
||||||
|
integrity sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-arm64@1.2.4":
|
||||||
|
version "1.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz#b1b288b36864b3bce545ad91fa6dadcf1a4ad318"
|
||||||
|
integrity sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-arm@1.2.4":
|
||||||
|
version "1.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz#b9260dd1ebe6f9e3bdbcbdcac9d2ac125f35852d"
|
||||||
|
integrity sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-ppc64@1.2.4":
|
||||||
|
version "1.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz#4b83ecf2a829057222b38848c7b022e7b4d07aa7"
|
||||||
|
integrity sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-riscv64@1.2.4":
|
||||||
|
version "1.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz#880b4678009e5a2080af192332b00b0aaf8a48de"
|
||||||
|
integrity sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-s390x@1.2.4":
|
||||||
|
version "1.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz#74f343c8e10fad821b38f75ced30488939dc59ec"
|
||||||
|
integrity sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-x64@1.2.4":
|
||||||
|
version "1.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz#df4183e8bd8410f7d61b66859a35edeab0a531ce"
|
||||||
|
integrity sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64@1.2.4":
|
||||||
|
version "1.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz#c8d6b48211df67137541007ee8d1b7b1f8ca8e06"
|
||||||
|
integrity sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64@1.2.4":
|
||||||
|
version "1.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz#be11c75bee5b080cbee31a153a8779448f919f75"
|
||||||
|
integrity sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==
|
||||||
|
|
||||||
|
"@img/sharp-linux-arm64@0.34.5":
|
||||||
|
version "0.34.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz#7aa7764ef9c001f15e610546d42fce56911790cc"
|
||||||
|
integrity sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==
|
||||||
|
optionalDependencies:
|
||||||
|
"@img/sharp-libvips-linux-arm64" "1.2.4"
|
||||||
|
|
||||||
|
"@img/sharp-linux-arm@0.34.5":
|
||||||
|
version "0.34.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz#5fb0c3695dd12522d39c3ff7a6bc816461780a0d"
|
||||||
|
integrity sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==
|
||||||
|
optionalDependencies:
|
||||||
|
"@img/sharp-libvips-linux-arm" "1.2.4"
|
||||||
|
|
||||||
|
"@img/sharp-linux-ppc64@0.34.5":
|
||||||
|
version "0.34.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz#9c213a81520a20caf66978f3d4c07456ff2e0813"
|
||||||
|
integrity sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==
|
||||||
|
optionalDependencies:
|
||||||
|
"@img/sharp-libvips-linux-ppc64" "1.2.4"
|
||||||
|
|
||||||
|
"@img/sharp-linux-riscv64@0.34.5":
|
||||||
|
version "0.34.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz#cdd28182774eadbe04f62675a16aabbccb833f60"
|
||||||
|
integrity sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==
|
||||||
|
optionalDependencies:
|
||||||
|
"@img/sharp-libvips-linux-riscv64" "1.2.4"
|
||||||
|
|
||||||
|
"@img/sharp-linux-s390x@0.34.5":
|
||||||
|
version "0.34.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz#93eac601b9f329bb27917e0e19098c722d630df7"
|
||||||
|
integrity sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==
|
||||||
|
optionalDependencies:
|
||||||
|
"@img/sharp-libvips-linux-s390x" "1.2.4"
|
||||||
|
|
||||||
|
"@img/sharp-linux-x64@0.34.5":
|
||||||
|
version "0.34.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz#55abc7cd754ffca5002b6c2b719abdfc846819a8"
|
||||||
|
integrity sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==
|
||||||
|
optionalDependencies:
|
||||||
|
"@img/sharp-libvips-linux-x64" "1.2.4"
|
||||||
|
|
||||||
|
"@img/sharp-linuxmusl-arm64@0.34.5":
|
||||||
|
version "0.34.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz#d6515ee971bb62f73001a4829b9d865a11b77086"
|
||||||
|
integrity sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==
|
||||||
|
optionalDependencies:
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64" "1.2.4"
|
||||||
|
|
||||||
|
"@img/sharp-linuxmusl-x64@0.34.5":
|
||||||
|
version "0.34.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz#d97978aec7c5212f999714f2f5b736457e12ee9f"
|
||||||
|
integrity sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==
|
||||||
|
optionalDependencies:
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64" "1.2.4"
|
||||||
|
|
||||||
|
"@img/sharp-wasm32@0.34.5":
|
||||||
|
version "0.34.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz#2f15803aa626f8c59dd7c9d0bbc766f1ab52cfa0"
|
||||||
|
integrity sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==
|
||||||
|
dependencies:
|
||||||
|
"@emnapi/runtime" "^1.7.0"
|
||||||
|
|
||||||
|
"@img/sharp-win32-arm64@0.34.5":
|
||||||
|
version "0.34.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz#3706e9e3ac35fddfc1c87f94e849f1b75307ce0a"
|
||||||
|
integrity sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==
|
||||||
|
|
||||||
|
"@img/sharp-win32-ia32@0.34.5":
|
||||||
|
version "0.34.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz#0b71166599b049e032f085fb9263e02f4e4788de"
|
||||||
|
integrity sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==
|
||||||
|
|
||||||
|
"@img/sharp-win32-x64@0.34.5":
|
||||||
|
version "0.34.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz#a81ffb00e69267cd0a1d626eaedb8a8430b2b2f8"
|
||||||
|
integrity sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==
|
||||||
|
|
||||||
"@jridgewell/gen-mapping@^0.3.5":
|
"@jridgewell/gen-mapping@^0.3.5":
|
||||||
version "0.3.13"
|
version "0.3.13"
|
||||||
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f"
|
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f"
|
||||||
@@ -683,6 +837,11 @@ balanced-match@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||||
|
|
||||||
|
blurhash@^2.0.5:
|
||||||
|
version "2.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-2.0.5.tgz#efde729fc14a2f03571a6aa91b49cba80d1abe4b"
|
||||||
|
integrity sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==
|
||||||
|
|
||||||
brace-expansion@^2.0.1:
|
brace-expansion@^2.0.1:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7"
|
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7"
|
||||||
@@ -727,7 +886,7 @@ deepmerge@^4.2.2, deepmerge@^4.3.1:
|
|||||||
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
|
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
|
||||||
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
|
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
|
||||||
|
|
||||||
detect-libc@^2.0.3:
|
detect-libc@^2.0.3, detect-libc@^2.1.2:
|
||||||
version "2.1.2"
|
version "2.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad"
|
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad"
|
||||||
integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==
|
integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==
|
||||||
@@ -1109,11 +1268,50 @@ rollup@^4.34.9, rollup@^4.59.0:
|
|||||||
"@rollup/rollup-win32-x64-msvc" "4.59.0"
|
"@rollup/rollup-win32-x64-msvc" "4.59.0"
|
||||||
fsevents "~2.3.2"
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
|
semver@^7.7.3:
|
||||||
|
version "7.7.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a"
|
||||||
|
integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==
|
||||||
|
|
||||||
set-cookie-parser@^3.0.0:
|
set-cookie-parser@^3.0.0:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-3.0.1.tgz#a902ea39d692837bcfd7160891f06b2427321a18"
|
resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-3.0.1.tgz#a902ea39d692837bcfd7160891f06b2427321a18"
|
||||||
integrity sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==
|
integrity sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==
|
||||||
|
|
||||||
|
sharp@^0.34.5:
|
||||||
|
version "0.34.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.34.5.tgz#b6f148e4b8c61f1797bde11a9d1cfebbae2c57b0"
|
||||||
|
integrity sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==
|
||||||
|
dependencies:
|
||||||
|
"@img/colour" "^1.0.0"
|
||||||
|
detect-libc "^2.1.2"
|
||||||
|
semver "^7.7.3"
|
||||||
|
optionalDependencies:
|
||||||
|
"@img/sharp-darwin-arm64" "0.34.5"
|
||||||
|
"@img/sharp-darwin-x64" "0.34.5"
|
||||||
|
"@img/sharp-libvips-darwin-arm64" "1.2.4"
|
||||||
|
"@img/sharp-libvips-darwin-x64" "1.2.4"
|
||||||
|
"@img/sharp-libvips-linux-arm" "1.2.4"
|
||||||
|
"@img/sharp-libvips-linux-arm64" "1.2.4"
|
||||||
|
"@img/sharp-libvips-linux-ppc64" "1.2.4"
|
||||||
|
"@img/sharp-libvips-linux-riscv64" "1.2.4"
|
||||||
|
"@img/sharp-libvips-linux-s390x" "1.2.4"
|
||||||
|
"@img/sharp-libvips-linux-x64" "1.2.4"
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64" "1.2.4"
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64" "1.2.4"
|
||||||
|
"@img/sharp-linux-arm" "0.34.5"
|
||||||
|
"@img/sharp-linux-arm64" "0.34.5"
|
||||||
|
"@img/sharp-linux-ppc64" "0.34.5"
|
||||||
|
"@img/sharp-linux-riscv64" "0.34.5"
|
||||||
|
"@img/sharp-linux-s390x" "0.34.5"
|
||||||
|
"@img/sharp-linux-x64" "0.34.5"
|
||||||
|
"@img/sharp-linuxmusl-arm64" "0.34.5"
|
||||||
|
"@img/sharp-linuxmusl-x64" "0.34.5"
|
||||||
|
"@img/sharp-wasm32" "0.34.5"
|
||||||
|
"@img/sharp-win32-arm64" "0.34.5"
|
||||||
|
"@img/sharp-win32-ia32" "0.34.5"
|
||||||
|
"@img/sharp-win32-x64" "0.34.5"
|
||||||
|
|
||||||
sirv@^3.0.0:
|
sirv@^3.0.0:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/sirv/-/sirv-3.0.2.tgz#f775fccf10e22a40832684848d636346f41cd970"
|
resolved "https://registry.yarnpkg.com/sirv/-/sirv-3.0.2.tgz#f775fccf10e22a40832684848d636346f41cd970"
|
||||||
|
|||||||
Reference in New Issue
Block a user