Skip to content

Commit

Permalink
feat: support Netlify Image CDN (#1234)
Browse files Browse the repository at this point in the history
Co-authored-by: Kristen Lavavej <[email protected]>
  • Loading branch information
ascorbic and klavavej authored Feb 15, 2024
1 parent 9982e17 commit a872822
Show file tree
Hide file tree
Showing 12 changed files with 214 additions and 29 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ sw.*
.idea
.vercel
.output

# Local Netlify folder
.netlify
67 changes: 59 additions & 8 deletions docs/content/3.providers/netlify.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,72 @@ links:
size: xs
---

When deploying your Nuxt applications to [Netlify's composable platform](https://docs.netlify.com/platform/overview/), the image module uses [Netlify Image CDN](https://docs.netlify.com/image-cdn/overview/) to optimize and transform images on demand without impacting build times. Netlify Image CDN also handles content negotiation to use the most efficient image format for the requesting client.

This provider is automatically enabled in Netlify deployments, and also when running locally using [the Netlify CLI](https://docs.netlify.com/cli/local-development/).

You can also manually enable this provider. To do so, set the provider to `netlify` or add the following to your Nuxt configuration:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
image: {
provider: 'netlify',
}
})
```

## Local development

To test image transformations locally, use [Netlify Dev](https://docs.netlify.com/cli/local-development/). This feature of the Netlify CLI runs a local development server that mimics the Netlify production environment, including Netlify Image CDN.

## Remote images

To transform a source image hosted on another domain, you must first configure allowed domains in your `netlify.toml` file.

```toml [netlify.toml]
[images]
remote_images = ["https://my-images.com/.*", "https://animals.more-images.com/[bcr]at/.*"]
```

The `remote_images` property accepts an array of regex. If your images are in specific subdomains or directories, you can use regex to allow just those subdomains or directories.

## Modifiers

Beyond the [standard properties](https://image.nuxt.com/usage/nuxt-img), you can use the [Netlify Image CDN `position` parameter](https://docs.netlify.com/image-cdn/overview/#position) as a modifier for Nuxt Image.

```vue
<NuxtImg
provider="netlify"
src="owl.jpg"
height="400"
width="600"
fit="cover"
format="webp"
quality="80"
:modifiers="{ position: 'left' }"
/>
```

## Deprecated Netlify Large Media option

::callout{color="amber" icon="i-ph-warning-duotone"}
Netlify’s Large Media service is [deprecated](https://answers.netlify.com/t/large-media-feature-deprecated-but-not-removed/100804). If this feature is already enabled, Large Media will continue to work on these sites as usual. New Large Media configuration is not recommended.
Netlify’s Large Media service is [deprecated](https://answers.netlify.com/t/large-media-feature-deprecated-but-not-removed/100804). If this feature is already enabled for your site on Netlify and you have already set `provider: 'netlify'` in your Nuxt configuration, then this will be detected at build time and Large Media continues to work on your site as usual. You can also explicitly enable it by setting `provider: 'netlifyLargeMedia'`. However, new Large Media configuration is not recommended.
::

Netlify offers dynamic image transformation for all JPEG, PNG, and GIF files you have set to be tracked with [Netlify Large Media](https://docs.netlify.com/large-media/overview/).
### Migrate to Netlify Image CDN

::callout
Before setting `provider: 'netlify'`, make sure you have followed the steps to enable [Netlify Large Media](https://docs.netlify.com/large-media/overview/).
::
To migrate from the deprecated Netlify Large Media option to the more robust Netlify Image CDN option, change `provider: 'netlify'` to `provider: 'netlifyImageCdn'`. This will enable the Netlify Image CDN service, even if large media is enabled on your site.

## Modifiers

In addition to `height` and `width`, the Netlify provider supports the following modifiers:
### Use deprecated Netlify Large Media option

If you're not ready to migrate to the more robust Netlify Image CDN option, Netlify continues to support dynamic image transformation for all JPEG, PNG, and GIF files you have set to be tracked with [Netlify Large Media](https://docs.netlify.com/large-media/overview/).

#### Large Media Modifiers

In addition to `height` and `width`, the deprecated Netlify Large Media provider supports the following modifiers:

### `fit`
##### `fit`

* **Default**: `contain`
* **Valid options**: `contain` (equivalent to `nf_resize=fit`) and `fill` (equivalent to `nf_resize=smartcrop`)
2 changes: 1 addition & 1 deletion docs/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ useSeoMeta({
const source = ref('npm i @nuxt/image')
const { copy, copied } = useClipboard({ source })
const providers = ['caisy', 'bunny', 'cloudflare', 'cloudimage', 'cloudinary', 'directus', 'edgio', 'fastly', 'glide', 'gumlet', 'hygraph', 'imageengine', 'imagekit', 'imgix', 'ipx', 'netlify', 'prepr', 'prismic', 'sanity', 'storyblok', 'strapi', 'twicpics', 'unsplash', 'uploadcare', 'vercel', 'weserv']
const providers = ['caisy', 'bunny', 'cloudflare', 'cloudimage', 'cloudinary', 'directus', 'edgio', 'fastly', 'glide', 'gumlet', 'hygraph', 'imageengine', 'imagekit', 'imgix', 'ipx', 'netlify', 'netlifyImageCdn', 'netlifyLargeMedia', 'prepr', 'prismic', 'sanity', 'storyblok', 'strapi', 'twicpics', 'unsplash', 'uploadcare', 'vercel', 'weserv']
// Disabling because svg to png does not work now with SSG
// Related issue: https://github.com/unjs/ipx/issues/160
// const img = useImage()
Expand Down
5 changes: 4 additions & 1 deletion playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ export default defineNuxtConfig({
imagekit: {
baseURL: 'https://ik.imagekit.io/demo'
},
netlify: {
netlifyImageCdn: {
baseURL: 'https://netlify-photo-gallery.netlify.app/.netlify/images'
},
netlifyLargeMedia: {
baseURL: 'https://netlify-photo-gallery.netlify.app'
},
layer0: {
Expand Down
19 changes: 17 additions & 2 deletions playground/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,9 +333,24 @@ export const providers: Provider[] = [
}
]
},
// Netlify
{
name: 'netlify',
name: 'netlifyImageCdn',
samples: [
{
src: '/images/apple.jpg',
width: 100,
height: 100,
fit: 'cover'
},
{
src: '/images/apple.jpg',
width: 400,
height: 300
}
]
},
{
name: 'netlifyLargeMedia',
samples: [
{
src: '/images/apple.jpg',
Expand Down
15 changes: 14 additions & 1 deletion src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const BuiltInProviders = [
'ipxStatic',
'layer0',
'netlify',
'netlifyLargeMedia',
'netlifyImageCdn',
'prepr',
'none',
'prismic',
Expand Down Expand Up @@ -118,6 +120,10 @@ export async function resolveProvider (_nuxt: any, key: string, input: InputProv
input.provider = input.name
}

if (input.provider in normalizableProviders) {
input.provider = normalizableProviders[input.provider]!()
}

const resolver = createResolver(import.meta.url)
input.provider = BuiltInProviders.includes(input.provider as ImageProviderName)
? await resolver.resolve('./runtime/providers/' + input.provider)
Expand All @@ -136,7 +142,14 @@ export async function resolveProvider (_nuxt: any, key: string, input: InputProv

const autodetectableProviders: Partial<Record<ProviderName, ImageProviderName>> = {
vercel: 'vercel',
aws_amplify: 'awsAmplify'
aws_amplify: 'awsAmplify',
netlify: 'netlify'
}

const normalizableProviders: Partial<Record<string, () => ImageProviderName>> = {
netlify: () => {
return process.env.NETLIFY_LFS_ORIGIN_URL ? 'netlifyLargeMedia' : 'netlifyImageCdn'
}
}

export function detectProvider (userInput: string = '') {
Expand Down
53 changes: 53 additions & 0 deletions src/runtime/providers/netlifyImageCdn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { encodeQueryItem } from 'ufo'
import type { ProviderGetImage } from '../../types'
import { createOperationsGenerator } from '#image'

// https://docs.netlify.com/image-cdn/overview/
export const operationsGenerator = createOperationsGenerator({
keyMap: {
width: 'w',
height: 'h',
format: 'fm',
quality: 'q',
position: 'position',
fit: 'fit'
},
valueMap: {
fit: {
fill: 'fill',
cover: 'cover',
contain: 'contain'
},
format: {
avif: 'avif',
gif: 'gif',
jpg: 'jpg',
png: 'png',
webp: 'webp'
},
position: {
top: 'top',
right: 'right',
bottom: 'bottom',
left: 'left',
center: 'center'
}
},
joinWith: '&',
formatter: (key, value) => encodeQueryItem(key, value)
})

export const getImage: ProviderGetImage = (src, { modifiers = {}, baseURL } = {}) => {
const mods: Record<string, string> = { ...modifiers }
mods.url = src
if (modifiers.width) {
mods.width = modifiers.width.toString()
}
if (modifiers.height) {
mods.height = modifiers.height.toString()
}
const operations = operationsGenerator(mods)
return {
url: `${baseURL || '/.netlify/images'}?${operations}`
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { joinURL } from 'ufo'
import { encodeQueryItem, joinURL } from 'ufo'
import type { ProviderGetImage } from '../../types'
import { createOperationsGenerator } from '#image'

Expand All @@ -15,7 +15,7 @@ export const operationsGenerator = createOperationsGenerator({
}
},
joinWith: '&',
formatter: (key, value) => `${key}=${value}`
formatter: (key, value) => encodeQueryItem(key, value)
})

const isDev = process.env.NODE_ENV === 'development'
Expand Down
18 changes: 16 additions & 2 deletions test/e2e/__snapshots__/no-ssr.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -286,14 +286,28 @@ exports[`browser (ssr: false) > layer0 should render images 2`] = `
]
`;

exports[`browser (ssr: false) > netlify should render images 1`] = `
exports[`browser (ssr: false) > netlifyImageCdn should render images 1`] = `
[
"https://netlify-photo-gallery.netlify.app/.netlify/images?w=100&h=100&fit=cover&url=%2Fimages%2Fapple.jpg",
"https://netlify-photo-gallery.netlify.app/.netlify/images?w=400&h=300&url=%2Fimages%2Fapple.jpg",
]
`;

exports[`browser (ssr: false) > netlifyImageCdn should render images 2`] = `
[
"https://netlify-photo-gallery.netlify.app/.netlify/images?w=100&h=100&fit=cover&url=%2Fimages%2Fapple.jpg",
"https://netlify-photo-gallery.netlify.app/.netlify/images?w=400&h=300&url=%2Fimages%2Fapple.jpg",
]
`;

exports[`browser (ssr: false) > netlifyLargeMedia should render images 1`] = `
[
"https://netlify-photo-gallery.netlify.app/images/apple.jpg?w=101&nf_resize=fit",
"https://netlify-photo-gallery.netlify.app/images/apple.jpg?w=200&h=200&nf_resize=smartcrop",
]
`;

exports[`browser (ssr: false) > netlify should render images 2`] = `
exports[`browser (ssr: false) > netlifyLargeMedia should render images 2`] = `
[
"https://netlify-photo-gallery.netlify.app/images/apple.jpg?w=101&nf_resize=fit",
"https://netlify-photo-gallery.netlify.app/images/apple.jpg?w=200&h=200&nf_resize=smartcrop",
Expand Down
18 changes: 16 additions & 2 deletions test/e2e/__snapshots__/ssr.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -286,14 +286,28 @@ exports[`browser (ssr: true) > layer0 should render images 2`] = `
]
`;

exports[`browser (ssr: true) > netlify should render images 1`] = `
exports[`browser (ssr: true) > netlifyImageCdn should render images 1`] = `
[
"https://netlify-photo-gallery.netlify.app/.netlify/images?w=100&h=100&fit=cover&url=%2Fimages%2Fapple.jpg",
"https://netlify-photo-gallery.netlify.app/.netlify/images?w=400&h=300&url=%2Fimages%2Fapple.jpg",
]
`;

exports[`browser (ssr: true) > netlifyImageCdn should render images 2`] = `
[
"https://netlify-photo-gallery.netlify.app/.netlify/images?w=400&h=300&url=%2Fimages%2Fapple.jpg",
"https://netlify-photo-gallery.netlify.app/.netlify/images?w=100&h=100&fit=cover&url=%2Fimages%2Fapple.jpg",
]
`;

exports[`browser (ssr: true) > netlifyLargeMedia should render images 1`] = `
[
"https://netlify-photo-gallery.netlify.app/images/apple.jpg?w=101&nf_resize=fit",
"https://netlify-photo-gallery.netlify.app/images/apple.jpg?w=200&h=200&nf_resize=smartcrop",
]
`;

exports[`browser (ssr: true) > netlify should render images 2`] = `
exports[`browser (ssr: true) > netlifyLargeMedia should render images 2`] = `
[
"https://netlify-photo-gallery.netlify.app/images/apple.jpg?w=101&nf_resize=fit",
"https://netlify-photo-gallery.netlify.app/images/apple.jpg?w=200&h=200&nf_resize=smartcrop",
Expand Down
18 changes: 12 additions & 6 deletions test/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export const images = [
imageengine: { url: '/test.png' },
unsplash: { url: '/test.png' },
imagekit: { url: '/test.png' },
netlify: { url: '/test.png' },
netlifyImageCdn: { url: '/.netlify/images?url=%2Ftest.png' },
netlifyLargeMedia: { url: '/test.png' },
prepr: { url: 'https://projectName.stream.prepr.io/image-test-300x450-png' },
prismic: { url: '/test.png?auto=compress,format&rect=0,0,200,200&w=100&h=100' },
sanity: { url: 'https://cdn.sanity.io/images/projectid/production/test-300x450.png?auto=format' },
Expand Down Expand Up @@ -49,7 +50,8 @@ export const images = [
imageengine: { url: '/test.png?imgeng=/w_200' },
unsplash: { url: '/test.png?w=200' },
imagekit: { url: '/test.png?tr=w-200' },
netlify: { url: '/test.png?w=200&nf_resize=fit' },
netlifyImageCdn: { url: '/.netlify/images?w=200&url=%2Ftest.png' },
netlifyLargeMedia: { url: '/test.png?w=200&nf_resize=fit' },
prepr: { url: 'https://projectName.stream.prepr.io/w_200/image-test-300x450-png' },
prismic: { url: '/test.png?auto=compress,format&rect=0,0,200,200&w=200&h=100' },
sanity: { url: 'https://cdn.sanity.io/images/projectid/production/test-300x450.png?w=200&auto=format' },
Expand Down Expand Up @@ -83,7 +85,8 @@ export const images = [
imageengine: { url: '/test.png?imgeng=/h_200' },
unsplash: { url: '/test.png?h=200' },
imagekit: { url: '/test.png?tr=h-200' },
netlify: { url: '/test.png?h=200&nf_resize=fit' },
netlifyImageCdn: { url: '/.netlify/images?h=200&url=%2Ftest.png' },
netlifyLargeMedia: { url: '/test.png?h=200&nf_resize=fit' },
prepr: { url: 'https://projectName.stream.prepr.io/h_200/image-test-300x450-png' },
prismic: { url: '/test.png?auto=compress,format&rect=0,0,200,200&w=100&h=200' },
sanity: { url: 'https://cdn.sanity.io/images/projectid/production/test-300x450.png?h=200&auto=format' },
Expand Down Expand Up @@ -117,7 +120,8 @@ export const images = [
imageengine: { url: '/test.png?imgeng=/w_200/h_200' },
unsplash: { url: '/test.png?w=200&h=200' },
imagekit: { url: '/test.png?tr=w-200,h-200' },
netlify: { url: '/test.png?w=200&h=200&nf_resize=fit' },
netlifyImageCdn: { url: '/.netlify/images?w=200&h=200&url=%2Ftest.png' },
netlifyLargeMedia: { url: '/test.png?w=200&h=200&nf_resize=fit' },
prismic: { url: '/test.png?auto=compress,format&rect=0,0,200,200&w=200&h=200' },
prepr: { url: 'https://projectName.stream.prepr.io/w_200,h_200/image-test-300x450-png' },
sanity: { url: 'https://cdn.sanity.io/images/projectid/production/test-300x450.png?w=200&h=200&auto=format' },
Expand Down Expand Up @@ -151,7 +155,8 @@ export const images = [
imageengine: { url: '/test.png?imgeng=/w_200/h_200/m_letterbox' },
unsplash: { url: '/test.png?w=200&h=200&fit=fill' },
imagekit: { url: '/test.png?tr=w-200,h-200,cm-pad_resize' },
netlify: { url: '/test.png?w=200&h=200&nf_resize=fit' },
netlifyImageCdn: { url: '/.netlify/images?w=200&h=200&fit=contain&url=%2Ftest.png' },
netlifyLargeMedia: { url: '/test.png?w=200&h=200&nf_resize=fit' },
prismic: { url: '/test.png?auto=compress,format&rect=0,0,200,200&w=200&h=200&fit=fill' },
prepr: { url: 'https://projectName.stream.prepr.io/w_200,h_200,fit_contain/image-test-300x450-png' },
sanity: { url: 'https://cdn.sanity.io/images/projectid/production/test-300x450.png?w=200&h=200&fit=fill&auto=format&bg=ffffff' },
Expand Down Expand Up @@ -185,7 +190,8 @@ export const images = [
imageengine: { url: '/test.png?imgeng=/w_200/h_200/m_letterbox/f_jpg' },
unsplash: { url: '/test.png?w=200&h=200&fit=fill&fm=jpeg' },
imagekit: { url: '/test.png?tr=w-200,h-200,cm-pad_resize,f-jpeg' },
netlify: { url: '/test.png?w=200&h=200&nf_resize=fit' },
netlifyImageCdn: { url: '/.netlify/images?w=200&h=200&fit=contain&fm=jpeg&url=%2Ftest.png' },
netlifyLargeMedia: { url: '/test.png?w=200&h=200&nf_resize=fit' },
prismic: { url: '/test.png?auto=compress,format&rect=0,0,200,200&w=200&h=200&fit=fill&fm=jpeg' },
sanity: { url: 'https://cdn.sanity.io/images/projectid/production/test-300x450.png?w=200&h=200&fit=fill&fm=jpg&bg=ffffff' },
prepr: { url: 'https://projectName.stream.prepr.io/w_200,h_200,fit_contain,format_jpg/image-test-300x450-png' },
Expand Down
Loading

0 comments on commit a872822

Please sign in to comment.