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

Add Uploadcare image loader #18688

Closed
wants to merge 10 commits into from
Closed
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
31 changes: 30 additions & 1 deletion docs/basic-features/image-optimization.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
description: Next.js supports built-in image optimization, as well as third party loaders for Imgix, Cloudinary, and more! Learn more here.
description: Next.js supports built-in image optimization, as well as third party loaders for Imgix, Cloudinary, Uploadcare, and more! Learn more here.
---

# Image Component and Image Optimization
Expand Down Expand Up @@ -88,8 +88,37 @@ The following Image Optimization cloud providers are supported:
- [Imgix](https://www.imgix.com): `loader: 'imgix'`
- [Cloudinary](https://cloudinary.com): `loader: 'cloudinary'`
- [Akamai](https://www.akamai.com): `loader: 'akamai'`
- [Uploadcare](https://uploadcare.com): `loader: 'uploadcare'`. See more info [here](#uploadcare-loader).
- Default: Works automatically with `next dev`, `next start`, or a custom server

### Uploadcare loader

Before all, you need to create an Uploadcare [account](https://uploadcare.com/accounts/signup/).

Then, to enable Uploadcare loader you need to configure your `next.config.js` in the following way:

```js
module.exports = {
images: {
loader: 'uploadcare',
path: 'https://YOUR_PUBLIC_KEY.ucr.io', // optional
},
}
```

`path` is the Uploadcare [Media Proxy](https://uploadcare.com/docs/delivery/media_proxy/) endpoint. It is optional and not needed unless you are going to optimize images hosted on external sites.

Uploadcare loader supports following types of image sources:

- Images hosted on Uploadcare CDN (like `https://ucarecdn.com/:uuid/`).
- Images hosted on external sites. They are automatically proxied through Media Proxy. `path` is required.

Relative images, statically served by Next.js, are not supported.

Custom CNAMEs also are not supported.

By default, loader uses `smart` quality. You can override it using `quality` property for Image component. Integer quality values are mapped onto Uploadcare ones. All available quality transformations described [here](https://uploadcare.com/docs/transformations/compression/#operation-quality).

## Caching

The following describes the caching algorithm for the default [loader](#loader). For all other loaders, please refer to your cloud provider's documentation.
Expand Down
2 changes: 1 addition & 1 deletion errors/invalid-images-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module.exports = {
// limit of 50 domains values
domains: [],
path: '/_next/image',
// loader can be 'default', 'imgix', 'cloudinary', or 'akamai'
// loader can be 'default', 'imgix', 'cloudinary', 'akamai', or 'uploadcare'
loader: 'default',
},
}
Expand Down
34 changes: 34 additions & 0 deletions examples/image-component-uploadcare/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local

# vercel
.vercel
23 changes: 23 additions & 0 deletions examples/image-component-uploadcare/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Uploadcare loader for Image Component Example

This example shows how to use Uploadcare loader with [Image Component in Next.js](https://nextjs.org/docs/api-reference/next/image).

The index page ([`pages/index.js`](pages/index.js)) has a couple images, one Uploadcare CDN hosted image and one external image. In [`next.config.js`](next.config.js), the `path` property is used to set Media Proxy endpoint. Run or deploy the app to see how it works!

## Deploy your own

Deploy the example using [Vercel](https://vercel.com):

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/vercel/next.js/tree/canary/examples/image-component)

## How to use

Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:

```bash
npx create-next-app --example image-component-uploadcare image-app
# or
yarn create next-app --example image-component-uploadcare image-app
```

Deploy it to the cloud with [Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
6 changes: 6 additions & 0 deletions examples/image-component-uploadcare/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
images: {
loader: 'uploadcare',
path: 'https://cd813a35e9a71d2f5125.ucr.io', // optional
},
Copy link
Author

@nd0ut nd0ut Nov 2, 2020

Choose a reason for hiding this comment

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

It would be nice to be able to pass custom options directly to the image loader. I could make another PR with this feature. What do you think?

}
15 changes: 15 additions & 0 deletions examples/image-component-uploadcare/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "image-component-uploadcare",
"version": "1.0.0",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "latest",
"react": "^16.13.1",
"react-dom": "^16.13.1"
},
"license": "MIT"
}
60 changes: 60 additions & 0 deletions examples/image-component-uploadcare/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import styles from '../styles.module.css'
import Image from 'next/image'

const Code = (p) => <code className={styles.inlineCode} {...p} />

const Index = () => (
<div className={styles.container}>
<div className={styles.card}>
<h1>Uploadcare loader for Image Component</h1>
<p>
The following is an example of a reference to an image from the{' '}
Uploadcare CDN at <Code>ucarecdn.com</Code>
</p>
<p>
It will be served directly from <Code>ucarecdn.com</Code>, without
proxying through Media Proxy.
</p>
<Image
alt="Vercel logo"
src="https://ucarecdn.com/a6f8abc8-f92e-460a-b7a1-c5cd70a18cdb/vercel.png"
width={1000}
height={1000}
/>
<hr className={styles.hr} />
<p>
The following is an example of a reference to an external image at{' '}
<Code>assets.vercel.com</Code>.
</p>
<p>It will be proxied through Media Proxy.</p>
<Image
alt="Next.js logo"
src="https://assets.vercel.com/image/upload/v1538361091/repositories/next-js/next-js.png"
width={1200}
height={400}
/>
<hr className={styles.hr} />
<p>SVGs and GIFs will be used without transformations</p>
<Image
alt="Next.js logo"
src="https://ucarecdn.com/375bba4b-35db-4cb8-8fc7-7540625f2181/next.svg"
width={64}
height={64}
/>
<Image
alt="Vercel logo"
src="https://ucarecdn.com/0f23a269-13eb-4fc9-b378-86f224380d26/vercel.gif"
width={64}
height={64}
/>
<hr className={styles.hr} />
Checkout the documentation for{' '}
<a href="https://nextjs.org/docs/basic-features/image-optimization#uploadcare-loader">
Image Optimization
</a>{' '}
to learn more.
</div>
</div>
)

export default Index
34 changes: 34 additions & 0 deletions examples/image-component-uploadcare/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
.container {
padding: 4rem 1rem;
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}

.container p {
margin: 1.5rem 0;
}

.card {
max-width: 50rem;
box-shadow: -10px 10px 80px rgba(0, 0, 0, 0.12);
border: 1px solid #eee;
border-radius: 8px;
padding: 2rem;
margin: 0 auto;
}

.inlineCode {
color: #be00ff;
font-size: 16px;
white-space: pre-wrap;
}

.inlineCode::before,
.inlineCode::after {
content: '`';
}

.hr {
border: 0;
border-top: 1px solid #eaeaea;
margin: 1.5rem 0;
}
63 changes: 63 additions & 0 deletions packages/next/client/image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const loaders = new Map<LoaderValue, (props: LoaderProps) => string>([
['imgix', imgixLoader],
['cloudinary', cloudinaryLoader],
['akamai', akamaiLoader],
['uploadcare', uploadcareLoader],
['default', defaultLoader],
])

Expand Down Expand Up @@ -433,6 +434,68 @@ function cloudinaryLoader({ root, src, width, quality }: LoaderProps): string {
return `${root}${paramsString}${normalizeSrc(src)}`
}

function uploadcareLoader({ root, src, width, quality }: LoaderProps): string {
const isOnCdn = /^https?:\/\/ucarecdn\.com/.test(src)
Copy link

Choose a reason for hiding this comment

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

what if i use custom cname like images.example.com for my uploadcare images?

Copy link
Author

@nd0ut nd0ut Nov 10, 2020

Choose a reason for hiding this comment

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

what if i use custom cname like images.example.com for my uploadcare images?

That's why we need ability to pass custom options directly to the image loader. I prefer not to change Image Loader API without discussion with Next.js team.

So, custom cnames aren't supported now. I've added a few words about it in readme.


if (process.env.NODE_ENV !== 'production') {
if (!isOnCdn && src.startsWith('/')) {
throw new Error(
`Failed to parse "${src}" in "next/image", Uploadcare loader doesn't support relative images`
)
}

if (!isOnCdn && !/^https?:\/\/.+\.ucr\.io\/?$/.test(root)) {
throw new Error(
`Failed to parse "${root}" in "next/image", Uploadcare loader expects proxy endpoint like "https://YOUR_PUBLIC_KEY.ucr.io".`
)
}
}

const filename = src.substring(1 + src.lastIndexOf('/'))
const extension = filename
.toLowerCase()
.split('?')[0]
.split('#')[0]
.split('.')[1]

if (['svg', 'gif'].includes(extension)) {
return isOnCdn ? src : `${root.replace(/\/$/, '')}${src}`
}

/**
* Output image dimensions is limited to 3000px
* It can be increased by explicitly setting /format/jpeg/
*/
const maxResizeWidth = Math.min(Math.max(width, 0), 3000)
// Demo: https://ucarecdn.com/a6f8abc8-f92e-460a-b7a1-c5cd70a18cdb/-/format/auto/-/resize/300x/vercel.png
const params = ['format/auto', 'stretch/off', `resize/${maxResizeWidth}x`]

if (quality) {
/**
* Uploadcare doesn't support integer-based quality modificators,
* so we need to map them onto uploadcare's equivalents
*/
const names = ['lightest', 'lighter', 'normal', 'better', 'best']
const intervals = [0, 38, 70, 80, 87, 100]
const nameIdx = intervals.findIndex((min, idx) => {
const max = intervals[idx + 1]
return min <= quality && quality <= max
})
params.push(`quality/${names[nameIdx]}`)
} else {
params.push('quality/smart')
}

const paramsString = '/-/' + params.join('/-/') + '/'

if (isOnCdn) {
const withoutFilename = src.slice(0, src.lastIndexOf('/'))
return `${withoutFilename}${paramsString}${filename}`
}

return `${root.replace(/\/$/, '')}${paramsString}${src}`
}

function defaultLoader({ root, src, width, quality }: LoaderProps): string {
if (process.env.NODE_ENV !== 'production') {
const missingValues = []
Expand Down
1 change: 1 addition & 0 deletions packages/next/next-server/server/image-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const VALID_LOADERS = [
'imgix',
'cloudinary',
'akamai',
'uploadcare',
] as const

export type LoaderValue = typeof VALID_LOADERS[number]
Expand Down
2 changes: 1 addition & 1 deletion test/integration/image-optimizer/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,7 @@ describe('Image Optimizer', () => {
await nextConfig.restore()

expect(stderr).toContain(
'Specified images.loader should be one of (default, imgix, cloudinary, akamai), received invalid value (notreal)'
'Specified images.loader should be one of (default, imgix, cloudinary, akamai, uploadcare), received invalid value (notreal)'
)
})
})
Expand Down