Repository: nuxt/image
Pull request #1179 (2024-01-04 20:01:38)

feat: add bunny provider
Add support for Bunny provider (https://bunny.net/)

nuxt-studio[bot]: 
### <span aria-hidden="true">✅</span> Live Preview ready!

| Name | Edit | Preview | Latest Commit |
| :--- | :----- | :------ | :------ |
| **Image** | [Edit on Studio ↗︎](https://nuxt.studio/@nuxt/image/studio/content?refId=%231179) | [View Live Preview](https://image.nuxt.com?preview=f2a4a667ca1221acdeefe81bf8adf40c) | 57c7c21d52a98530fc29ecbb51624729d88b6702 |

<!-- This comment is generated by [Nuxt Studio](https://nuxt.studio) -->
<!-- Do not remove or edit this comment. -->


Patch:
diff --git a/docs/content/3.providers/bunny.md b/docs/content/3.providers/bunny.md
new file mode 100644
index 0000000..b3fe127
--- /dev/null
+++ b/docs/content/3.providers/bunny.md
@@ -0,0 +1,37 @@
+---
+title: Bunny
+description: Nuxt Image with Bunny integration.
+links:
+  - label: Source
+    icon: i-simple-icons-github
+    to: https://github.com/nuxt/image/blob/main/src/runtime/providers/bunny.ts
+    size: xs
+---
+
+Integration between [Bunny](https://bunny.net/) and the image module.
+
+To use this provider you just need to specify the base URL of your project.
+
+```ts [nuxt.config.ts]
+export default defineNuxtConfig({
+  image: {
+    bunny: {
+      baseURL: "https://<your-hostname>.b-cdn.net",
+    },
+  },
+});
+```
+
+## Modifiers
+
+All the default modifiers from [Bunny's documentation](https://docs.bunny.net/docs/stream-image-processing) are available.
+
+```vue
+<NuxtImg
+  provider="bunny"
+  src="https://bunnyoptimizerdemo.b-cdn.net/bunny1.jpg"
+  height="512"
+  width="512"
+  :quality="90"
+/>
+```
diff --git a/docs/pages/index.vue b/docs/pages/index.vue
index 379225b..c41eba4 100644
--- a/docs/pages/index.vue
+++ b/docs/pages/index.vue
@@ -16,7 +16,7 @@ useSeoMeta({
 const source = ref('npm i @nuxt/image')
 const { copy, copied } = useClipboard({ source })
 
-const providers = ['caisy', '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', '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()
diff --git a/docs/public/providers/bunny.svg b/docs/public/providers/bunny.svg
new file mode 100644
index 0000000..07e9032
--- /dev/null
+++ b/docs/public/providers/bunny.svg
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 72 72"><path fill="#fff" d="M64 .227H8a8 8 0 0 0-8 8v56a8 8 0 0 0 8 8h56a8 8 0 0 0 8-8v-56a8 8 0 0 0-8-8Z"/><path fill="url(#a)" fill-rule="evenodd" d="m38.492 18.989 11.511 6.251L39.367 11a7.93 7.93 0 0 0-.875 7.989Z" clip-rule="evenodd"/><path fill="url(#b)" fill-rule="evenodd" d="M33.29 42.175a2.624 2.624 0 1 1 0 5.248 2.624 2.624 0 0 1 0-5.248Z" clip-rule="evenodd"/><path fill="url(#c)" fill-rule="evenodd" d="m25.278 13.088 32.236 17.494a1.562 1.562 0 0 1 0 2.81 25.063 25.063 0 0 1-7.896 3.08l-6.706 13.762s-2.123 4.828-7.966 2.974c2.45-2.45 5.412-4.665 5.412-8.432a7.08 7.08 0 0 0-14.159 0c0 4.921 4.852 6.997 7.546 10.414a4.432 4.432 0 0 1-.385 5.587c-3.347-3.312-9.82-8.899-12.479-12.56a10.613 10.613 0 0 1-2.274-6.474 12.187 12.187 0 0 1 8.607-11.033 14.054 14.054 0 0 1 4.525-.548c2.13.162 4.2.786 6.065 1.831 2.857 1.68 4.245 1.236 6.216-.42 1.166-.956 2.438-4.07.467-4.793a14.166 14.166 0 0 0-1.971-.49c-3.663-.712-10.065-1.388-12.421-2.73-3.744-2.063-6.263-6.32-4.817-10.472Z" clip-rule="evenodd"/><path fill="url(#d)" fill-rule="evenodd" d="M40.3 44.81c1.492-7.849-6.473-15.336-12.584-14.216l.408-.094c-.327.07-.642.152-.945.245a12.188 12.188 0 0 0-8.607 11.033 10.613 10.613 0 0 0 2.332 6.473c2.66 3.662 9.132 9.249 12.48 12.561a4.431 4.431 0 0 0 .385-5.587c-2.753-3.405-7.605-5.493-7.605-10.403a7.08 7.08 0 0 1 14.159 0l-.023-.011Z" clip-rule="evenodd"/><path fill="url(#e)" fill-rule="evenodd" d="M25.278 13.088 49.77 26.43l.7.385c.583.455 1.166 1.364.408 3.044-1.166 2.507-5.831 4.933-11.208 3.032 1.68.49 2.822-.07 4.292-1.306 1.166-.956 2.437-4.07.466-4.794a14.152 14.152 0 0 0-1.97-.49c-3.663-.71-10.066-1.387-12.422-2.728-3.685-2.076-6.204-6.333-4.758-10.485Z" clip-rule="evenodd"/><path fill="url(#f)" fill-rule="evenodd" d="M25.278 13.088c2.53 9.33 17.937 10.1 25.658 13.995L25.278 13.088Z" clip-rule="evenodd"/><path fill="url(#g)" fill-rule="evenodd" d="M33.71 55.225c-2.694-3.405-7.546-5.493-7.546-10.403a7.08 7.08 0 0 1 6.205-6.998A10.24 10.24 0 0 0 22.164 48.03c-.001.69.07 1.377.21 2.052 2.228 2.52 5.447 5.493 8.164 8.012 1.061.992 2.04 1.925 2.846 2.718a4.875 4.875 0 0 0 1.166-2.764 4.269 4.269 0 0 0-.84-2.823Z" clip-rule="evenodd"/><path fill="url(#h)" fill-rule="evenodd" d="M40.265 45.65c.036-.278.056-.559.058-.84 1.47-7.849-6.496-15.336-12.608-14.216a14.567 14.567 0 0 1 3.99-.397c8.012.327 10.25 8.887 8.56 15.453Z" clip-rule="evenodd"/><path fill="url(#i)" fill-rule="evenodd" d="M16.636 28.308a2.648 2.648 0 0 1 2.636 2.647v2.636h-2.636A2.636 2.636 0 0 1 14 30.955a2.647 2.647 0 0 1 2.636-2.647Z" clip-rule="evenodd"/><defs><linearGradient id="a" x1="33.652" x2="56.558" y1="18.126" y2="18.126" gradientUnits="userSpaceOnUse"><stop stop-color="#FBAA19"/><stop offset="1" stop-color="#EF3E23"/></linearGradient><linearGradient id="b" x1="32.392" x2="34.188" y1="47.248" y2="42.338" gradientUnits="userSpaceOnUse"><stop stop-color="#F78D1E"/><stop offset="1" stop-color="#F37121"/></linearGradient><linearGradient id="c" x1="18.152" x2="46.061" y1="48.939" y2="19.024" gradientUnits="userSpaceOnUse"><stop stop-color="#FEBE2D"/><stop offset="1" stop-color="#F04E23"/></linearGradient><linearGradient id="d" x1="37.874" x2="22.432" y1="31.457" y2="55.225" gradientUnits="userSpaceOnUse"><stop stop-color="#EA4425"/><stop offset="1" stop-color="#FDBB27"/></linearGradient><linearGradient id="e" x1="14.28" x2="63.031" y1="23.328" y2="23.328" gradientUnits="userSpaceOnUse"><stop stop-color="#F47920"/><stop offset="1" stop-color="#E93825"/></linearGradient><linearGradient id="f" x1="-11.472" x2="87.721" y1="20.074" y2="20.074" gradientUnits="userSpaceOnUse"><stop stop-color="#FDCA0B"/><stop offset="1" stop-color="#F5841F"/></linearGradient><linearGradient id="g" x1="23.96" x2="34.282" y1="32.075" y2="68.078" gradientUnits="userSpaceOnUse"><stop stop-color="#E73C25"/><stop offset="1" stop-color="#FAA21B"/></linearGradient><linearGradient id="h" x1="-45.912" x2="101.331" y1="37.918" y2="37.918" gradientUnits="userSpaceOnUse"><stop stop-color="#FDBA12"/><stop offset="1" stop-color="#F7921E"/></linearGradient><linearGradient id="i" x1="14.093" x2="19.61" y1="30.5" y2="31.48" gradientUnits="userSpaceOnUse"><stop stop-color="#FEBE2D"/><stop offset="1" stop-color="#F04E23"/></linearGradient></defs></svg>
diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts
index 7e3d793..15c6ae4 100644
--- a/playground/nuxt.config.ts
+++ b/playground/nuxt.config.ts
@@ -31,6 +31,9 @@ export default defineNuxtConfig({
     awsAmplify: {
       baseURL: 'https://example.amplifyapp.com/_amplify/image'
     },
+    bunny: {
+      baseURL: 'https://bunnyoptimizerdemo.b-cdn.net'
+    },
     twicpics: {
       baseURL: 'https://demo.twic.pics/'
     },
diff --git a/playground/providers.ts b/playground/providers.ts
index daf1dc6..23ecd35 100644
--- a/playground/providers.ts
+++ b/playground/providers.ts
@@ -119,6 +119,35 @@ export const providers: Provider[] = [
       }
     ]
   },
+  // Bunny
+  {
+    name: 'bunny',
+    samples: [
+      {
+        src: '/bunny1.jpg',
+        width: 1000,
+        quality: 80,
+        modifiers: {
+          sharpen: true,
+          aspectRatio: '16:9'
+        }
+      },
+      {
+        src: '/bunny2.jpg',
+        modifiers: {
+          crop: '750,750',
+          crop_gravity: 'north'
+        }
+      },
+      {
+        src: '/bunny3.jpg',
+        modifiers: {
+          flop: true,
+          auto_optimize: 'high'
+        }
+      }
+    ]
+  },
   // Cloudflare
   {
     name: 'cloudflare',
diff --git a/src/provider.ts b/src/provider.ts
index 3e18058..7eab191 100644
--- a/src/provider.ts
+++ b/src/provider.ts
@@ -13,6 +13,7 @@ import { ipxSetup } from './ipx'
 const BuiltInProviders = [
   'aliyun',
   'awsAmplify',
+  'bunny',
   'caisy',
   'cloudflare',
   'cloudimage',
diff --git a/src/runtime/providers/bunny.ts b/src/runtime/providers/bunny.ts
new file mode 100644
index 0000000..ba31162
--- /dev/null
+++ b/src/runtime/providers/bunny.ts
@@ -0,0 +1,33 @@
+import { joinURL } from 'ufo'
+import type { ProviderGetImage } from '../../types'
+import { createOperationsGenerator } from '#image'
+
+export const operationsGenerator = createOperationsGenerator({
+  keyMap: {
+    width: 'width',
+    height: 'height',
+    aspectRatio: 'aspect_ratio',
+    quality: 'quality',
+    sharpen: 'sharpen',
+    blur: 'blur',
+    crop: 'crop',
+    cropGravity: 'crop_gravity',
+    flip: 'flip',
+    flop: 'flop',
+    brightness: 'brightness',
+    saturation: 'saturation',
+    hue: 'hue',
+    contrast: 'contrast',
+    autoOptimize: 'auto_optimize',
+    sepia: 'sepia'
+  },
+  joinWith: '&',
+  formatter: (key, value) => `${key}=${value}`
+})
+
+export const getImage: ProviderGetImage = (src, { modifiers = {}, baseURL } = {}) => {
+  const operations = operationsGenerator(modifiers)
+  return {
+    url: joinURL(baseURL, src + (operations ? '?' + operations : ''))
+  }
+}


Test patch:
diff --git a/test/e2e/__snapshots__/no-ssr.test.ts.snap b/test/e2e/__snapshots__/no-ssr.test.ts.snap
index 7f953a4..ab97f93 100644
--- a/test/e2e/__snapshots__/no-ssr.test.ts.snap
+++ b/test/e2e/__snapshots__/no-ssr.test.ts.snap
@@ -30,6 +30,22 @@ exports[`browser (ssr: false) > awsAmplify should render images 2`] = `
 ]
 `;
 
+exports[`browser (ssr: false) > bunny should render images 1`] = `
+[
+  "https://bunnyoptimizerdemo.b-cdn.net/bunny1.jpg?sharpen=true&aspect_ratio=16:9&width=1000&quality=80",
+  "https://bunnyoptimizerdemo.b-cdn.net/bunny2.jpg?crop=750,750&crop_gravity=north",
+  "https://bunnyoptimizerdemo.b-cdn.net/bunny3.jpg?flop=true&auto_optimize=high",
+]
+`;
+
+exports[`browser (ssr: false) > bunny should render images 2`] = `
+[
+  "https://bunnyoptimizerdemo.b-cdn.net/bunny1.jpg?sharpen=true&aspect_ratio=16:9&width=1000&quality=80",
+  "https://bunnyoptimizerdemo.b-cdn.net/bunny2.jpg?crop=750,750&crop_gravity=north",
+  "https://bunnyoptimizerdemo.b-cdn.net/bunny3.jpg?flop=true&auto_optimize=high",
+]
+`;
+
 exports[`browser (ssr: false) > caisy should render images 1`] = `
 [
   "https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=500&h=500&q=90",
diff --git a/test/e2e/__snapshots__/ssr.test.ts.snap b/test/e2e/__snapshots__/ssr.test.ts.snap
index 41f6f12..ba6e3bd 100644
--- a/test/e2e/__snapshots__/ssr.test.ts.snap
+++ b/test/e2e/__snapshots__/ssr.test.ts.snap
@@ -30,6 +30,22 @@ exports[`browser (ssr: true) > awsAmplify should render images 2`] = `
 ]
 `;
 
+exports[`browser (ssr: true) > bunny should render images 1`] = `
+[
+  "https://bunnyoptimizerdemo.b-cdn.net/bunny1.jpg?sharpen=true&aspect_ratio=16:9&width=1000&quality=80",
+  "https://bunnyoptimizerdemo.b-cdn.net/bunny2.jpg?crop=750,750&crop_gravity=north",
+  "https://bunnyoptimizerdemo.b-cdn.net/bunny3.jpg?flop=true&auto_optimize=high",
+]
+`;
+
+exports[`browser (ssr: true) > bunny should render images 2`] = `
+[
+  "https://bunnyoptimizerdemo.b-cdn.net/bunny1.jpg?sharpen=true&aspect_ratio=16:9&width=1000&quality=80",
+  "https://bunnyoptimizerdemo.b-cdn.net/bunny2.jpg?crop=750,750&crop_gravity=north",
+  "https://bunnyoptimizerdemo.b-cdn.net/bunny3.jpg?flop=true&auto_optimize=high",
+]
+`;
+
 exports[`browser (ssr: true) > caisy should render images 1`] = `
 [
   "https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=500&h=500&q=90",
diff --git a/test/providers.ts b/test/providers.ts
index 5ec1db0..c1f8ff7 100644
--- a/test/providers.ts
+++ b/test/providers.ts
@@ -30,7 +30,8 @@ export const images = [
     weserv: { url: 'https://wsrv.nl/?filename=test.png&we&url=https://my-website.com/test.png' },
     sirv: { url: 'https://demo.sirv.com/test.png' },
     hygraph: { url: 'https://media.graphassets.com/JL6e2yJERUyQtTiZIzPb' },
-    caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg' }
+    caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg' },
+    bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net' }
   },
   {
     args: ['/test.png', { width: 200 }],
@@ -63,7 +64,8 @@ export const images = [
     weserv: { url: 'https://wsrv.nl/?filename=test.png&we&w=200&url=https://my-website.com/test.png' },
     sirv: { url: 'https://demo.sirv.com/test.png?w=200' },
     hygraph: { url: 'https://media.graphassets.com/auto_image/resize=width:200/JL6e2yJERUyQtTiZIzPb' },
-    caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=200' }
+    caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=200' },
+    bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net?width=200' }
   },
   {
     args: ['/test.png', { height: 200 }],
@@ -96,7 +98,8 @@ export const images = [
     weserv: { url: 'https://wsrv.nl/?filename=test.png&we&h=200&url=https://my-website.com/test.png' },
     sirv: { url: 'https://demo.sirv.com/test.png?h=200' },
     hygraph: { url: 'https://media.graphassets.com/auto_image/resize=height:200/JL6e2yJERUyQtTiZIzPb' },
-    caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?h=200' }
+    caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?h=200' },
+    bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net?height=200' }
   },
   {
     args: ['/test.png', { width: 200, height: 200 }],
@@ -129,7 +132,8 @@ export const images = [
     weserv: { url: 'https://wsrv.nl/?filename=test.png&we&w=200&h=200&url=https://my-website.com/test.png' },
     sirv: { url: 'https://demo.sirv.com/test.png?w=200&h=200' },
     hygraph: { url: 'https://media.graphassets.com/auto_image/resize=width:200,height:200/JL6e2yJERUyQtTiZIzPb' },
-    caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=200&h=200' }
+    caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=200&h=200' },
+    bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net?width=200&height=200' }
   },
   {
     args: ['/test.png', { width: 200, height: 200, fit: 'contain' }],
@@ -162,7 +166,8 @@ export const images = [
     weserv: { url: 'https://wsrv.nl/?filename=test.png&we&w=200&h=200&fit=contain&url=https://my-website.com/test.png' },
     sirv: { url: 'https://demo.sirv.com/test.png?w=200&h=200&scale.option=fit' },
     hygraph: { url: 'https://media.graphassets.com/auto_image/resize=width:200,height:200,fit:max/JL6e2yJERUyQtTiZIzPb' },
-    caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=200&h=200' }
+    caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=200&h=200' },
+    bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net?width=200&height=200' }
   },
   {
     args: ['/test.png', { width: 200, height: 200, fit: 'contain', format: 'jpeg' }],
@@ -195,7 +200,8 @@ export const images = [
     weserv: { url: 'https://wsrv.nl/?filename=test.png&we&w=200&h=200&fit=contain&output=jpg&url=https://my-website.com/test.png' },
     sirv: { url: 'https://demo.sirv.com/test.png?w=200&h=200&scale.option=fit&format=jpg' },
     hygraph: { url: 'https://media.graphassets.com/output=format:jpg/resize=width:200,height:200,fit:max/JL6e2yJERUyQtTiZIzPb' },
-    caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=200&h=200' }
+    caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=200&h=200' },
+    bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net?width=200&height=200' }
   }
 ] as const