Skip to content

Instantly share code, notes, and snippets.

@iamriajul
Last active February 2, 2024 08:23
Show Gist options
  • Save iamriajul/1ec2b195cf371d8fecf674945aad2196 to your computer and use it in GitHub Desktop.
Save iamriajul/1ec2b195cf371d8fecf674945aad2196 to your computer and use it in GitHub Desktop.
An example for Qwik Image component which tries to be SEO Friendly. The image.tsx is the full-featured component, and the image-raw.tsx is bare minimum component.
import {component$, type PropsOf} from "@builder.io/qwik";
import {imgSizes, type ImageSize} from "~/utils/img";
type ImageAttributes = Omit<PropsOf<'img'>, 'children' | 'sizes'>;
export interface ImageRawProps extends ImageAttributes {
sizes?: string | ImageSize[];
defaultSize?: boolean;
sizeMargin?: string | null;
}
/**
* Image which add some tying sugar to the `sizes` attribute.
* This component should be used instead of the native `img` tag.
*/
export default component$<ImageRawProps>(({
sizes = [],
defaultSize = true,
sizeMargin = '30px',
decoding = "async",
loading = "lazy",
...props
}) => {
return <img
decoding={decoding}
loading={loading}
sizes={
typeof sizes as unknown == 'string' || sizes.length
? imgSizes(sizes, defaultSize, sizeMargin)
: undefined
}
{...props}
/>;
});
import {component$} from "@builder.io/qwik";
import {buildImageFillCropUrl, buildImageFillUrl, buildImageFitUrl} from "~/services/img-service";
import ImageRaw, {type ImageRawProps} from "~/components/image/image-raw";
interface ImageResolutionCrop {
width: number;
height: number;
}
// height is optional for fit and fill
type ImageResolution = number | ImageResolutionCrop;
export interface ImageProps extends Omit<ImageRawProps, 'src' | 'srcSet'> {
uri: string;
resolutions: ImageResolution[];
type?: 'fit' | 'fill' | 'crop';
alt: string,
hash?: string;
}
/**
* More sophisticated version of ImageRaw.
* which handles generating the `srcSet` attribute with ease.
* It is only useful when you use the IMG CDN service.
*/
export default component$<ImageProps>(({
uri: _uri,
type = 'crop',
resolutions: _resolutions,
hash: _hash,
...props
}) => {
let uri = _uri;
let resolutions = _resolutions;
let hash = _hash;
const buildSrc = (res: ImageResolution) => {
if (type === 'fit') {
const width = typeof res === 'number' ? res : res.width;
return buildImageFitUrl(uri, width);
}
if (type === 'fill') {
const width = typeof res === 'number' ? res : res.width;
return buildImageFillUrl(uri, width);
}
if (type === 'crop') {
if (typeof res === 'number') {
// if (import.meta.env.DEV) {
// throw new Error(`Invalid image resolution: ${res}, when type is crop it must be an object`);
// }
return buildImageFitUrl(uri, res);
}
return buildImageFillCropUrl(uri, res.width, res.height);
}
throw new Error(`Invalid image type: ${type}`);
}
if (uri.includes('#')) {
// Ensure better CLS without the need to specify the height or using fill or crop.
// API should provide the source resolution, to ensure the best CLS without loosing image information (by using crop).
// eg: {url}#r=500x500,hash=abc
const uriMeta = uri.split('#')[1].split('&')
.reduce((acc, optionString) => {
const [key, value] = optionString.split('=');
acc[key] = value;
return acc;
}, {} as Record<string, string>);
// Remove the hash from the uri. We don't need it anymore. It has conveyed its information.
uri = uri.split('#')[0];
if (uriMeta['size']) {
const sourceResolution = (() => {
const [width, height] = uriMeta['size'].split('x');
return {width: parseInt(width), height: parseInt(height)};
})();
const sourceRatio = sourceResolution.width / sourceResolution.height;
resolutions = resolutions.map((resolution) => {
if (typeof resolution === 'number') {
return {
width: resolution,
height: Math.round(resolution / sourceRatio),
};
}
return resolution;
});
}
// Use the hash from the uri if available.
if (uriMeta['hash']) {
hash = uriMeta['hash'];
}
}
const firstRes = resolutions[0];
const lastRes = resolutions[resolutions.length - 1];
// data-hash property is handled by src/image-hash.ts which is inlined in the SSR entry point.
return <ImageRaw
src={buildSrc(firstRes)}
srcset={
resolutions.length > 1
? resolutions.map((res) => {
if (typeof res === 'number') {
return `${buildSrc(res)} ${res}w`;
}
return `${buildSrc(res)} ${res.width}w`;
}).join(', ')
: undefined
}
width={typeof lastRes === 'number' ? lastRes : lastRes.width}
height={typeof lastRes === 'number' ? undefined : lastRes.height}
data-hash={hash}
{...props}
/>;
});
import Breakpoints, {type Breakpoint} from "~/utils/breakpoints";
interface ImageSizeBase {
size: string;
}
interface ImageSizeByMin extends ImageSizeBase {
min: Breakpoint | string;
}
interface ImageSizeByMax extends ImageSizeBase {
max: Breakpoint | string;
}
export type ImageSize = ImageSizeByMin | ImageSizeByMax | string;
export const imgSizes = (
sizes: string | ImageSize[],
defaultSize: boolean = true,
sizeMargin: string | null = '30px',
): string => {
const buildSize = (size: string) => sizeMargin ? `calc(${size} - ${sizeMargin})` : size;
defaultSize = defaultSize && typeof sizes !== 'string';
sizes = typeof sizes === 'string'
? [sizes]
: sizes.map(size => {
if (typeof size === 'string') {
return size;
}
if ('max' in size) {
const breakpoint = size.max in Breakpoints ? Breakpoints[size.max as Breakpoint] : size.max;
return `(max-width: ${breakpoint}) ${buildSize(size.size)}`;
}
const breakpoint = size.min in Breakpoints ? Breakpoints[size.min as Breakpoint] : size.min;
return `(min-width: ${breakpoint}) ${buildSize(size.size)}`;
});
if (defaultSize) {
sizes.push(`calc(100vw - ${sizeMargin || '30px'})`);
}
return sizes.join(', ');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment