Skip to content

Instantly share code, notes, and snippets.

@TheRusskiy
Last active February 27, 2022 15:21
Show Gist options
  • Save TheRusskiy/e00ce85b63bfa22e38b043c77c124f06 to your computer and use it in GitHub Desktop.
Save TheRusskiy/e00ce85b63bfa22e38b043c77c124f06 to your computer and use it in GitHub Desktop.
Next.js Cloudinary Image
import NextImage from 'next/image'
import { useCallback } from 'react'
import compact from 'lodash/compact'
const CLOUDINARY_HOST = 'https://some-domain.mo.cloudinary.net'
type ImageLoaderProps = {
src: string
width: number
quality?: number
root?: string
}
// strip any leading slashes
function normalizeSrc(src: string): string {
return src[0] === '/' ? src.slice(1) : src
}
type Props = Parameters<typeof NextImage>[0] & {
// https://cloudinary.com/documentation/media_optimizer_transformations#automatic_quality
quality?:
| 'auto'
| 'auto:best'
| 'auth:good'
| 'auto:eco'
| 'auto:low'
| number
gravity?:
| 'auto'
| 'center'
| 'north_east'
| 'north'
| 'north_west'
| 'west'
| 'south_west'
| 'south'
| 'south_east'
| 'east'
crop?:
| 'crop'
| 'fill'
| 'fill_pad'
| 'fit'
| 'lfill'
| 'limit'
| 'lpad'
| 'mfit'
| 'mpad'
| 'pad'
| 'scale'
| 'thumb'
}
// https://cloudinary.com/documentation/transformation_reference
const Image = ({
quality = 'auto',
gravity = 'center',
crop = 'limit',
width: initialWidth,
height: initialHeight,
...props
}: Props): ReturnType<typeof NextImage> => {
const aspectRatio: number | null =
typeof initialWidth === 'number' && typeof initialHeight === 'number'
? initialWidth / initialHeight
: null
const cloudinaryLoader = useCallback(
({ src, width }: ImageLoaderProps): string => {
const height =
gravity === 'auto' && aspectRatio ? width / aspectRatio : null
const params = compact([
'f_auto',
'c_' + crop,
'g_' + gravity,
'w_' + width,
height ? 'h_' + height : '',
'q_' + (quality || 'auto')
])
if (
gravity === 'auto' &&
!['fill', 'lfill', 'fill_pad', 'thumb', 'crop'].includes(crop)
) {
console.error(
'Automatic cropping is supported for the fill, lfill, fill_pad, thumb and crop modes.'
)
}
const paramsString = `tx=${params.join(',')}&resource_type=image`
const normalizedSrc = normalizeSrc(src)
const parsedUrl = new URL(src)
const joiner = parsedUrl.search && parsedUrl.search.length ? '&' : '?'
return `${CLOUDINARY_HOST}/${normalizedSrc}${joiner}${paramsString}`
},
[crop, quality, gravity, aspectRatio] // height
)
let imageSrc: string | null = null
// Next image accepts different types of imports
if (!props.src) {
imageSrc = null
} else if (typeof props.src === 'string') {
imageSrc = props.src
} else if (typeof props.src.default !== 'undefined') {
imageSrc = props.src.default.src
} else if (typeof props.src.src !== 'undefined') {
imageSrc = props.src.src
}
// disable optimization for dev environment and images present in source code
const isNextImage = imageSrc?.includes('_next/static') || process.env.NODE_ENV === 'development'
return (
<NextImage
{...props}
height={initialHeight}
loader={isNextImage ? undefined : cloudinaryLoader}
width={initialWidth}
/>
)
}
export default Image
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment