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

feat: support Netlify Image CDN #1234

Merged
merged 21 commits into from
Feb 15, 2024
Merged
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
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'
Copy link
Member

Choose a reason for hiding this comment

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

Adding a comment that we may change the logic for these providers in a subsequent PR. I'm discussing with @pi0 whether it's a breaking change to enable runtime support when users are performing fully static builds (e.g. nuxt generate).

context: #1224

}

const normalizableProviders: Partial<Record<string, () => ImageProviderName>> = {
netlify: () => {
return process.env.NETLIFY_LFS_ORIGIN_URL ? 'netlifyLargeMedia' : 'netlifyImageCdn'
}
}
Comment on lines +149 to 153
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've added this as a new way for providers to resolve at build time. It alows an alias or detected platform to provide logic to resolve to one or other defined provider types.

Copy link
Member

Choose a reason for hiding this comment

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

Seems good to me, and it's internal so we can always refactor in future.


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)
danielroe marked this conversation as resolved.
Show resolved Hide resolved
})

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