Skip to content

Commit 5404d56

Browse files
committed
feat!: add a getBlurDataURL server-side helper to generate base64 blurred placeholder
BREAKING CHANGE: previously implemented `blurDataURL` auto-generation won't work in Next.js v13. It was a bad way because the blurry image was requested from the server in runtime. A New way is to generate a blurry image base64 string at build time.
1 parent a6577b7 commit 5404d56

File tree

11 files changed

+1730
-1273
lines changed

11 files changed

+1730
-1273
lines changed

README.md

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Add your public Uploadcare key to your `.env*` config file. You can copy it from
5656
NEXT_PUBLIC_UPLOADCARE_PUBLIC_KEY="YOUR_PUBLIC_KEY"
5757
```
5858

59-
Alternatively, in case you're using a custom proxy, set the proxy domain.
59+
Alternatively, in case you're using a [custom proxy endpoint][docs-custom-proxy-endpoint], set the proxy domain.
6060

6161
```ini
6262
#.env
@@ -118,7 +118,34 @@ import { uploadcareLoader } from '@uploadcare/nextjs-loader';
118118
/>
119119
```
120120

121-
**Option 3**. Use the [next-image-loader](https://www.npmjs.com/package/next-image-loader) plugin to enable Uploadcare image loader for all images by default
121+
**Option 3 (Next.js v13+ only)**. Use the [`loaderFile` setting][loader-file] to enable Uploadcare image loader for all images by default.
122+
123+
1. Configure the `loaderFile` in your `next.config.js` like the following:
124+
125+
```js
126+
module.exports = {
127+
images: {
128+
loader: 'custom',
129+
loaderFile: './node_modules/@uploadcare/nextjs-loader/build/loader.js',
130+
},
131+
}
132+
```
133+
134+
2. Use `Image` as usual, with Uploadcare loader enabled implicitly:
135+
136+
```tsx
137+
import Image from 'next/image';
138+
139+
<Image
140+
alt="A test image"
141+
src="https://your-domain/image.jpg"
142+
width="400"
143+
height="300"
144+
quality="80"
145+
/>
146+
```
147+
148+
**Option 4**. Use the [next-image-loader](https://www.npmjs.com/package/next-image-loader) plugin to enable Uploadcare image loader for all images by default
122149

123150
In that case, you may not need the `loader: "custom"` setting in your `next.config.js`.
124151

@@ -180,19 +207,45 @@ There are two possible use cases:
180207

181208
#### When `src` is a string
182209

183-
This options is available for the `UploadcareImage` component only. It won't work when you're using custom loader directly.
210+
If you pass `placeholder="blur"` to the `UploadcareImage` component, the `blurDataURL` property will be used as the placeholder. In this case you must provide the `blurDataURL` property using our `getBlurDataURL` server-side helper.
184211

185-
If you pass `placeholder="blur"` to the `UploadcareImage` component, it will generate `blurDataURL` with the URL of the placeholder image (not base64) and use it as a placeholder. You can override `blurDataURL`.
212+
Here is the ``getBlurDataURL` interface:
213+
214+
```ts
215+
function getBlurDataURL(
216+
src: string,
217+
width = 10,
218+
quality = 1
219+
): Promise<string>
220+
```
221+
222+
Usage example:
186223

187224
```tsx
188-
<UploadcareImage
189-
alt="A test image"
190-
src="https://your-domain/image.jpg"
191-
width="400"
192-
height="300"
193-
quality="80"
194-
placeholder="blur"
195-
/>
225+
import UploadcareImage, { getBlurDataURL } from '@uploadcare/nextjs-loader';
226+
227+
const BLUR_IMAGE_URL = "https://your-domain/image.jpg"
228+
229+
export const getStaticProps = async () => {
230+
const blurDataURL = await getBlurDataURL(BLUR_IMAGE_URL);
231+
232+
return {
233+
props: { blurDataURL }
234+
};
235+
};
236+
237+
export default ({ blurDataURL }) => {
238+
return (
239+
<UploadcareImage
240+
alt="Blurred image"
241+
src={BLUR_IMAGE_URL}
242+
width="400"
243+
height="300"
244+
placeholder="blur"
245+
blurDataURL={blurDataURL}
246+
/>
247+
)
248+
}
196249
```
197250

198251
#### When `src` is a static import
@@ -246,3 +299,6 @@ Next checks whether the image url which loader generates has the exact value whi
246299
[npm-link]: https://www.npmjs.com/package/@uploadcare/nextjs-loader
247300
[demo-link]: https://uploadcare-nextjs-loader.netlify.app/
248301
[uploadcare-transformation-image-compression-docs]: https://uploadcare.com/docs/transformations/image/compression/?utm_source=github&utm_campaign=nextjs-loader
302+
[docs-custom-proxy-endpoint]: https://uploadcare.com/docs/delivery/proxy/#usage-endpoint
303+
[last-next-12-release]: https://github.com/uploadcare/nextjs-loader/tree/v0.4.0
304+
[loader-file]: https://nextjs.org/docs/api-reference/next/image#loader-configuration

jest-setup.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
import '@testing-library/jest-dom'
2+
import 'whatwg-fetch';

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@
6262
"shipjs": "0.24.0",
6363
"ts-add-module-exports": "^1.0.0",
6464
"ts-node": "^10.2.1",
65-
"typescript": "^4.4.3"
65+
"typescript": "^4.4.3",
66+
"whatwg-fetch": "^3.6.2"
6667
},
6768
"peerDependencies": {
6869
"next": ">=10.0.5",
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
import { getBlurDataURL } from '../utils/getBlurDataURL';
5+
6+
describe('getBlurDataURL', () => {
7+
it('should return low quality image with the specified width', async () => {
8+
const dataURL = await getBlurDataURL(
9+
'https://ucarecdn.com/c768f1c2-891a-4f54-8e1e-7242df218b51/pinewatt2Hzmz15wGikunsplash.jpg',
10+
10
11+
);
12+
expect(dataURL).toBe(
13+
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4gIcSUNDX1BST0ZJTEUAAQEAAAIMbGNtcwIQAABtbnRyUkdCIFhZWiAH3AABABkAAwApADlhY3NwQVBQTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWxjbXMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApkZXNjAAAA/AAAAF5jcHJ0AAABXAAAAAt3dHB0AAABaAAAABRia3B0AAABfAAAABRyWFlaAAABkAAAABRnWFlaAAABpAAAABRiWFlaAAABuAAAABRyVFJDAAABzAAAAEBnVFJDAAABzAAAAEBiVFJDAAABzAAAAEBkZXNjAAAAAAAAAANjMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB0ZXh0AAAAAElYAABYWVogAAAAAAAA9tYAAQAAAADTLVhZWiAAAAAAAAADFgAAAzMAAAKkWFlaIAAAAAAAAG+iAAA49QAAA5BYWVogAAAAAAAAYpkAALeFAAAY2lhZWiAAAAAAAAAkoAAAD4QAALbPY3VydgAAAAAAAAAaAAAAywHJA2MFkghrC/YQPxVRGzQh8SmQMhg7kkYFUXdd7WtwegWJsZp8rGm/fdPD6TD////bAEMAChQVGRUSHBkXGSAeHCIrRy4rJycrVz5CNEdnW21rZVtkYnKApItyeZt7YmSOwpCbqa63ubduicnXx7LWpLS3sP/bAEMBEBQVGRUSHBkXGSAeHCIrRy4rJycrVz5CNEdnW21rZVtkYnKApItyeZt7YmSOwpCbqa63ubduicnXx7LWpLS3sP/CABEIAAcACgMBIgACEQEDEQH/xAAWAAEBAQAAAAAAAAAAAAAAAAAABAX/xAAVAQEBAAAAAAAAAAAAAAAAAAAAAf/aAAwDAQACEAMQAAABixyP/8QAFxAAAwEAAAAAAAAAAAAAAAAAAAISE//aAAgBAQABBQLZoP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQMBAT8Bf//EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQIBAT8Bf//EABQQAQAAAAAAAAAAAAAAAAAAABD/2gAIAQEABj8CP//EABkQAAIDAQAAAAAAAAAAAAAAAAABEUFRYf/aAAgBAQABPyFWOEvT/9oADAMBAAIAAwAAABCD/8QAFREBAQAAAAAAAAAAAAAAAAAAAQD/2gAIAQMBAT8QC//EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQIBAT8Qf//EABgQAAMBAQAAAAAAAAAAAAAAAAABIREx/9oACAEBAAE/EFYvafRteuz/2Q=='
14+
);
15+
});
16+
});

src/__tests__/uploadcare-image.spec.tsx

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ describe('UploadcareImage', () => {
4343

4444
render(
4545
<UploadcareImage
46+
alt="Test image"
4647
src={src}
4748
width={500}
4849
height={500}
@@ -56,36 +57,4 @@ describe('UploadcareImage', () => {
5657
'https://ucarecdn.com/a6f8abc8-f92e-460a-b7a1-c5cd70a18cdb/-/format/auto/-/stretch/off/-/progressive/yes/-/resize/1080x/-/quality/normal/'
5758
);
5859
});
59-
60-
it('should generate blurDataURL when placeholder=blur passed', () => {
61-
const src =
62-
'https://ucarecdn.com/a6f8abc8-f92e-460a-b7a1-c5cd70a18cdb/image.png';
63-
64-
render(
65-
<UploadcareImage src={src} width={500} height={500} placeholder="blur" />
66-
);
67-
68-
expect(screen.getByRole('img')).toHaveStyle(
69-
'background-image: url(https://ucarecdn.com/a6f8abc8-f92e-460a-b7a1-c5cd70a18cdb/-/format/auto/-/stretch/off/-/progressive/yes/-/resize/5x/-/quality/lightest/image.png)'
70-
);
71-
});
72-
73-
it('should not override passed blurDataURL', () => {
74-
const src =
75-
'https://ucarecdn.com/a6f8abc8-f92e-460a-b7a1-c5cd70a18cdb/image.png';
76-
77-
render(
78-
<UploadcareImage
79-
src={src}
80-
width={500}
81-
height={500}
82-
placeholder="blur"
83-
blurDataURL={src}
84-
/>
85-
);
86-
87-
expect(screen.getByRole('img')).toHaveStyle(
88-
`background-image: url(${src})`
89-
);
90-
});
9160
});

src/components/UploadcareImage.tsx

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,7 @@
11
import Image, { ImageProps } from 'next/image';
22
import React from 'react';
3-
import {
4-
PLACEHOLDER_SIZE_FALLBACK,
5-
PLACEHOLDER_SIZE_MULTIPLIER
6-
} from '../utils/constants';
7-
import { getInt } from '../utils/helpers';
83
import { uploadcareLoader } from '../utils/loader';
94

10-
const shouldOverrideBlurDataUrl = (props: ImageProps): boolean => {
11-
return (
12-
typeof props.src === 'string' &&
13-
props.placeholder === 'blur' &&
14-
!props.blurDataURL
15-
);
16-
};
17-
18-
const generateBlurDataUrl = (
19-
src: string,
20-
width: ImageProps['width']
21-
): string => {
22-
const imageWidth = getInt(width);
23-
const blurImageWidth = imageWidth
24-
? Math.ceil(imageWidth * PLACEHOLDER_SIZE_MULTIPLIER)
25-
: PLACEHOLDER_SIZE_FALLBACK;
26-
return uploadcareLoader({
27-
src: src,
28-
width: blurImageWidth,
29-
quality: 1
30-
});
31-
};
32-
335
export function UploadcareImage(props: ImageProps): JSX.Element {
34-
let blurDataURL = props.blurDataURL;
35-
if (shouldOverrideBlurDataUrl(props)) {
36-
blurDataURL = generateBlurDataUrl(props.src as string, props.width);
37-
}
38-
return (
39-
<Image loader={uploadcareLoader} blurDataURL={blurDataURL} {...props} />
40-
);
6+
return <Image loader={uploadcareLoader} {...props} />;
417
}

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { UploadcareImage } from './components/UploadcareImage';
22
import { uploadcareLoader } from './utils/loader';
3+
import { getBlurDataURL } from './utils/getBlurDataURL';
34

45
export default UploadcareImage;
5-
export { uploadcareLoader };
6+
export { uploadcareLoader, getBlurDataURL };

src/utils/constants.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,3 @@ export const MAX_OUTPUT_JPEG_IMAGE_DIMENSION = 5000;
33
export const NOT_PROCESSED_EXTENSIONS = ['svg', 'gif'];
44
export const DEFAULT_PARAMS = ['format/auto', 'stretch/off', 'progressive/yes'];
55
export const DEFAULT_CDN_DOMAIN = 'ucarecdn.com';
6-
export const PLACEHOLDER_SIZE_MULTIPLIER = 0.01;
7-
export const PLACEHOLDER_SIZE_FALLBACK = 10;

src/utils/getBlurDataURL.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { uploadcareLoader } from './loader';
2+
3+
export async function getBlurDataURL(
4+
src: string,
5+
width = 10,
6+
quality = 1
7+
): Promise<string> {
8+
const cdnUrl = uploadcareLoader({
9+
src: src,
10+
width,
11+
quality
12+
});
13+
14+
const response = await fetch(cdnUrl);
15+
const contentType = response.headers.get('content-type');
16+
const arrayBuffer = await response.arrayBuffer();
17+
const buffer = Buffer.from(arrayBuffer);
18+
const base64 = buffer.toString('base64');
19+
const dataURL = `data:${contentType};base64,${base64}`;
20+
return dataURL;
21+
}

src/utils/helpers.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -145,16 +145,6 @@ export function isJpegExtension(extension: string): boolean {
145145
return ['jpg', 'jpeg'].includes(extension.toLowerCase());
146146
}
147147

148-
export function getInt(x: unknown): number | undefined {
149-
if (typeof x === 'number') {
150-
return x;
151-
}
152-
if (typeof x === 'string') {
153-
return parseInt(x, 10);
154-
}
155-
return undefined;
156-
}
157-
158148
function _parseUploadcareTransformationParam(param: string): string[] {
159149
return param.split('/');
160150
}

0 commit comments

Comments
 (0)