Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(fix): optimise images #36

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/lib/components/ui/ArticleCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import type { ArticleMetadata } from '$lib/types/article';
import { slide } from 'svelte/transition';
import Badge from './badge/badge.svelte';
import { optimizeCloudinaryUrl } from '$lib/utils/optimise-cloudinary';
losndu marked this conversation as resolved.
Show resolved Hide resolved

const { article }: { article: ArticleMetadata } = $props();
</script>
Expand All @@ -13,7 +14,14 @@
style={`background-color: ${article.isSponsored ? article.sponsorColor : 'transparent'}; color: ${article.isSponsored ? article.sponsorTextColor : 'inherit'};`}
>
<div class="flex flex-col w-full">
<img src={article.thumb} alt={article.title} class="aspect-square w-full object-cover" />
<img
src={optimizeCloudinaryUrl(article.thumb, { type: 'card' })}
loading="lazy"
alt={article.title}
class="aspect-square w-full object-cover"
width={464}
height={464}
/>
Comment on lines +17 to +24
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Good use of lazy loading and explicit dimensions.
Specifying width/height prevents layout shifts, and “lazy” loading optimizes performance. For an even more responsive approach, consider adding a srcset to leverage varying screen resolutions and reduce bandwidth usage for smaller devices.

</div>

<div class="flex flex-col py-6 w-full" class:px-6={article.isSponsored}>
Expand Down
14 changes: 13 additions & 1 deletion src/lib/components/ui/ArticleSpotlight.svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
<script lang="ts">
import type { ArticleMetadata } from '$lib/types/article';
import Badge from './badge/badge.svelte';
import { optimizeCloudinaryUrl } from '$lib/utils/optimise-cloudinary';

const { article }: { article: ArticleMetadata } = $props();
</script>

<a href={`/${article.slug}`}>
<div class="flex flex-col lg:flex-row">
<img src={article.thumb} alt={article.title} class="w-full h-auto lg:w-4/6 object-cover" />
<img
src={optimizeCloudinaryUrl(article.thumb, { type: 'spotlight' })}
srcset={`
${optimizeCloudinaryUrl(article.thumb, { type: 'spotlight', width: 480 })} 480w,
${optimizeCloudinaryUrl(article.thumb, { type: 'spotlight', width: 960 })} 960w
`}
sizes="(max-width: 768px) 100vw, 66vw"
alt={article.title}
class="w-full h-auto lg:w-4/6 object-cover"
width="960"
height="712"
/>
losndu marked this conversation as resolved.
Show resolved Hide resolved
<div
class="flex flex-col gap-3 md:gap-6 w-full lg:w-2/6 flex flex-col p-6 md:p-10 text-base tracking-normal bg-secondary max-md:px-5 max-md:max-w-full"
>
Expand Down
63 changes: 63 additions & 0 deletions src/lib/utils/optimise-cloudinary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
type ImageType = 'card' | 'spotlight' | 'hero';

interface TransformOptions {
width?: number;
height?: number;
type?: ImageType;
dpr?: number;
}
losndu marked this conversation as resolved.
Show resolved Hide resolved

const TRANSFORM_PRESETS: Record<ImageType, string> = {
card: 'w_464,h_464,c_fill,ar_1:1,g_center,q_auto,f_auto',
spotlight: 'w_640,h_475,c_fill,f_auto,q_auto',
hero: 'w_720,h_542,c_fill,g_auto,fl_progressive,q_auto:best,f_auto'
};
Comment on lines +10 to +14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider clarifying the hero preset.
The hero preset uses relatively large dimensions and a combination of progressive loading plus q_auto:best. This is ideal for high-resolution hero images. However, be cautious with large file sizes for slower connections. Consider offering multiple sizes via responsive techniques or limiting q_auto:best for data-constrained users.


export function optimizeCloudinaryUrl(url: string, options: TransformOptions = {}): string {
if (!url || !url.includes('cloudinary.com')) return url;

let baseUrl: string;
let path: string;

const uploadIndex = url.lastIndexOf('/upload/');
const coverImageIndex = url.lastIndexOf('/coverImage/');

if (uploadIndex !== -1) {
baseUrl = url.substring(0, uploadIndex + '/upload'.length);
path = url.substring(uploadIndex + '/upload/'.length);
} else if (coverImageIndex !== -1) {
baseUrl = url.substring(0, coverImageIndex);
path = url.substring(coverImageIndex + '/coverImage/'.length);
} else {
return url;
}

if (!path) return url;

Comment on lines +19 to +36
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Path extraction logic is intuitive.
Your fallback to directly return the original URL if neither '/upload/' nor '/coverImage/' is found helps avoid unintended breakage. Just watch out for future expansions if other paths (e.g., '/someOtherPath/') become relevant.

const transformParams: string[] = [];

if (options.type && options.type in TRANSFORM_PRESETS) {
transformParams.push(TRANSFORM_PRESETS[options.type]);
} else if (options.width && options.height) {
transformParams.push(`w_${options.width},h_${options.height},c_fill,g_center`);
} else {
transformParams.push(TRANSFORM_PRESETS.card);
}

if (options.dpr) {
const sanitizedDpr = Math.max(1, Math.min(Math.abs(options.dpr), 3));
transformParams.push(`dpr_${sanitizedDpr.toFixed(1)}`);
}
Comment on lines +47 to +50
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Smart DPR sanitization.
Clamping the DPR between 1 and 3 is a nice safeguard. However, in some edge cases (e.g., “super retina” devices), a higher DPR might be desired. Keep an eye on future device evolutions and be prepared to bump the upper limit if needed.


const pathPrefix = url.includes('/upload/') ? '' : 'coverImage/';
return `${baseUrl}/${transformParams.join(',')}/${pathPrefix}${path}`;
}

export const optimizeSpotlightImage = (url: string) =>
optimizeCloudinaryUrl(url, { type: 'spotlight' });

export const optimizeHeroImage = (url: string, dpr = 1) =>
optimizeCloudinaryUrl(url, {
type: 'hero',
dpr
});
40 changes: 35 additions & 5 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
import BaseHead from '$lib/components/server/BaseHead.svelte';
import Button from '$lib/components/ui/button/button.svelte';
import { Mail } from 'lucide-svelte';
import { optimizeCloudinaryUrl, optimizeHeroImage } from '$lib/utils/optimise-cloudinary';

const heroImage =
'https://res.cloudinary.com/dc2iz5j1c/image/upload/v1734510500/hero_choeao.webp';

const { data }: { data: PageData } = $props();
const articles = $derived(data.articles);
Expand All @@ -32,11 +36,37 @@
State of the art research on Ethereum and the broader crypto ecosystem
</h1>

<img
src="/hero.png"
class="absolute top-0 right-0 w-1/2 animate-float"
alt="Hero illustration"
/>
<picture>
<source
srcset="{optimizeHeroImage(heroImage, 2)} 2x,
{optimizeHeroImage(heroImage, 1)} 1x"
media="(min-width: 768px)"
type="image/webp"
/>
<source
srcset="{optimizeCloudinaryUrl(heroImage, {
type: 'hero',
width: 360,
height: 271,
dpr: 2
})} 2x,
{optimizeCloudinaryUrl(heroImage, {
type: 'hero',
width: 360,
height: 271,
dpr: 1
})} 1x"
type="image/webp"
/>
<img
src={optimizeHeroImage(heroImage)}
class="absolute top-0 right-0 w-1/2 animate-float"
alt="Hero illustration"
width="720"
height="542"
loading="eager"
/>
</picture>
</div>

<div class="flex justify-center md:hidden">
Expand Down
Binary file removed static/hero.png
Binary file not shown.