Repository: nuxt/image
Pull request #842 (2023-05-31 20:55:04)

fix: emit synthetic load/error events on initial hydration
resolves https://github.com/nuxt/image/issues/682
resolves https://github.com/nuxt/image/issues/412

This PR emits load/error events on initial hydration for better DX if the load/error event has already occurred before hydration.

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *nuxt-image-v1* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 2819554e97b59ed112351da8a57a96a6ce5202ad |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/nuxt-image-v1/deploys/6477b42b4e950e000873a033 |

Patch:
diff --git a/docs/content/5.configuration.md b/docs/content/5.configuration.md
index 98b29e1..da8d2bf 100644
--- a/docs/content/5.configuration.md
+++ b/docs/content/5.configuration.md
@@ -19,6 +19,18 @@ export default defineNuxtConfig({
 })
 ```
 
+## `inject`
+
+By default Nuxt Image v1 adopts a composable approach. If you do not use the components no additional code will be added to your bundle. But if you wish to globally initialize an `$img` helper that will be available throughout your application, you can do so.
+
+```ts [nuxt.config.ts]
+export default defineNuxtConfig({
+  image: {
+    inject: true
+  }
+})
+```
+
 ## `screens`
 
 List of predefined screen sizes.
diff --git a/docs/content/5.providers/directus.md b/docs/content/5.providers/directus.md
new file mode 100644
index 0000000..0bca697
--- /dev/null
+++ b/docs/content/5.providers/directus.md
@@ -0,0 +1,62 @@
+# Directus
+
+Nuxt Image with Directus integration
+
+Integration between [Directus](https://directus.io/) 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: {
+    directus: {
+      // This URL needs to include the final `assets/` directory
+      baseURL: 'http://localhost:8055/assets/',
+    }
+  }
+})
+```
+
+You can easily override default options:
+
+```ts [nuxt.config.ts]
+export default defineNuxtConfig({
+  image: {
+    directus: {
+      baseURL: 'http://mydirectus-domain.com/assets/',
+      modifiers: {
+        withoutEnlargement: 'true',
+        transforms: [['blur', 4], ['negate']]
+      }
+    }
+  }
+})
+```
+
+## Modifiers
+All the default modifiers from [Directus documentation](https://docs.directus.io/reference/files.html#requesting-a-thumbnail) are available.
+
+```vue
+<nuxt-img
+  provider="directus"
+  src="ad514db1-eb90-4523-8183-46781437e7ee"
+  height="512"
+  fit="inside"
+  quality="20"
+  :modifiers="{ withoutEnlargement: 'true' }"
+/>
+```
+
+Since Directus exposes [the full sharp API](https://sharp.pixelplumbing.com/api-operation) through the `transforms` parameter, we can use it inside our `modifiers` prop:
+
+```vue
+<nuxt-img
+  provider="directus"
+  src="ad514db1-eb90-4523-8183-46781437e7ee"
+  :modifiers="{ height: '512', withoutEnlargement: 'true', transforms: [['blur', 4], ['negate']] }"
+/>
+```
+
+::alert{type="info"}
+Note that the `transforms` parameter, as stated in the [Directus documentation](https://docs.directus.io/reference/files.html#advanced-transformations), is basically a list of lists. This is to facilitate the use of those sharp functions, like [`normalise`](https://sharp.pixelplumbing.com/api-operation#normalise), that would need multiple values in input: `transforms: [['normalise', 1, 99], ['blur', 4], ['negate']]`.
+::
diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts
index 065beb2..062009a 100644
--- a/playground/nuxt.config.ts
+++ b/playground/nuxt.config.ts
@@ -3,6 +3,7 @@ import { defineNuxtConfig } from 'nuxt/config'
 export default defineNuxtConfig({
   modules: ['@nuxt/image-edge'],
   image: {
+    inject: true,
     domains: [
       'https://nuxtjs.org',
       'https://images.unsplash.com',
@@ -33,6 +34,9 @@ export default defineNuxtConfig({
       token: 'demo',
       baseURL: 'sample.li'
     },
+    directus: {
+      baseURL: 'http://localhost:8055/assets/'
+    },
     fastly: {
       baseURL: 'https://www.fastly.io'
     },
diff --git a/playground/providers.ts b/playground/providers.ts
index cc30b08..888bd2b 100644
--- a/playground/providers.ts
+++ b/playground/providers.ts
@@ -728,5 +728,36 @@ export const providers: Provider[] = [
         quality: 75
       }
     ]
+  },
+  // Directus
+  {
+    name: 'directus',
+    samples: [
+      {
+        src: 'ad514db1-eb90-4523-8183-46781437e7ee',
+        alt: 'Image 1'
+      },
+      {
+        src: 'ad514db1-eb90-4523-8183-46781437e7ee.jpg',
+        alt: '1024px width',
+        width: 1024,
+        height: 256,
+        fit: 'cover',
+        modifiers: { withoutEnlargement: 'true' }
+      },
+      {
+        src: 'ad514db1-eb90-4523-8183-46781437e7ee',
+        alt: '256px width, webp',
+        width: 256,
+        format: 'webp'
+      },
+      {
+        src: 'ad514db1-eb90-4523-8183-46781437e7ee',
+        alt: '256px width, webp',
+        width: 256,
+        format: 'webp',
+        modifiers: { withoutEnlargement: 'true', transforms: [['blur', 4], ['negate']] }
+      }
+    ]
   }
 ]
diff --git a/src/module.ts b/src/module.ts
index 68c8f11..ba2fd69 100644
--- a/src/module.ts
+++ b/src/module.ts
@@ -5,14 +5,15 @@ import { resolveProviders, detectProvider, resolveProvider } from './provider'
 import type { ImageProviders, ImageOptions, InputProvider, CreateImageOptions } from './types'
 
 export interface ModuleOptions extends ImageProviders {
-  staticFilename: string,
+  inject: boolean
+  staticFilename: string
   provider: CreateImageOptions['provider']
   presets: { [name: string]: ImageOptions }
   dir: string
   domains: string[]
   sharp: any
   alias: Record<string, string>
-  screens: CreateImageOptions['screens'],
+  screens: CreateImageOptions['screens']
   internalUrl: string
   providers: { [name: string]: InputProvider | any } & ImageProviders
   [key: string]: any
@@ -22,6 +23,7 @@ export * from './types'
 
 export default defineNuxtModule<ModuleOptions>({
   defaults: nuxt => ({
+    inject: false,
     staticFilename: '[publicPath]/image/[hash][ext]',
     provider: 'auto',
     dir: nuxt.options.dir.public,
@@ -139,8 +141,13 @@ ${providers.map(p => `  ['${p.name}']: { provider: ${p.importName}, defaults: ${
       }
     })
 
-    // Add runtime plugin
-    addPlugin({ src: resolver.resolve('./runtime/plugin') })
+    if (options.inject) {
+      // Add runtime plugin
+      addPlugin({ src: resolver.resolve('./runtime/plugin') })
+    } else if (nuxt.options.dev) {
+      // Add placeholder runtime plugin
+      addPlugin({ src: resolver.resolve('./runtime/plugin.placeholder') })
+    }
 
     // TODO: Transform asset urls that pass to `src` attribute on image components
   }
diff --git a/src/provider.ts b/src/provider.ts
index 2cb8221..f868f77 100644
--- a/src/provider.ts
+++ b/src/provider.ts
@@ -15,6 +15,7 @@ const BuiltInProviders = [
   'cloudimage',
   'cloudinary',
   'contentful',
+  'directus',
   'edgio',
   'fastly',
   'glide',
diff --git a/src/runtime/components/nuxt-img.ts b/src/runtime/components/nuxt-img.ts
index c752b06..a5b9475 100644
--- a/src/runtime/components/nuxt-img.ts
+++ b/src/runtime/components/nuxt-img.ts
@@ -3,7 +3,7 @@ import { useImage } from '../composables'
 import { parseSize } from '../utils'
 import { prerenderStaticImages } from '../utils/prerender'
 import { baseImageProps, useBaseImage } from './_base'
-import { useHead } from '#imports'
+import { useHead, useNuxtApp } from '#imports'
 
 export const imgProps = {
   ...baseImageProps,
@@ -13,7 +13,7 @@ export const imgProps = {
 export default defineComponent({
   name: 'NuxtImg',
   props: imgProps,
-  emits: ['load'],
+  emits: ['load', 'error'],
   setup: (props, ctx) => {
     const $img = useImage()
     const _base = useBaseImage(props)
@@ -95,6 +95,8 @@ export default defineComponent({
 
     const imgEl = ref<HTMLImageElement>()
 
+    const nuxtApp = useNuxtApp()
+    const initialLoad = nuxtApp.isHydrating
     onMounted(() => {
       if (placeholder.value) {
         const img = new Image()
@@ -109,17 +111,29 @@ export default defineComponent({
         return
       }
 
-      if (imgEl.value) {
-        imgEl.value.onload = (event) => {
-          ctx.emit('load', event)
+      if (!imgEl.value) { return }
+
+      if (imgEl.value.complete && initialLoad) {
+        if (imgEl.value.getAttribute('data-error')) {
+          ctx.emit('error', new Event('error'))
+        } else {
+          ctx.emit('load', new Event('load'))
         }
       }
+
+      imgEl.value.onload = (event) => {
+        ctx.emit('load', event)
+      }
+      imgEl.value.onerror = (event) => {
+        ctx.emit('error', event)
+      }
     })
 
     return () => h('img', {
       ref: imgEl,
       key: src.value,
       src: src.value,
+      ...process.server ? { onerror: 'this.setAttribute(\'data-error\', 1)' } : {},
       ...attrs.value,
       ...ctx.attrs
     })
diff --git a/src/runtime/components/nuxt-picture.ts b/src/runtime/components/nuxt-picture.ts
index ee28e32..53517fd 100644
--- a/src/runtime/components/nuxt-picture.ts
+++ b/src/runtime/components/nuxt-picture.ts
@@ -1,7 +1,7 @@
 import { h, defineComponent, ref, computed, onMounted } from 'vue'
 import { prerenderStaticImages } from '../utils/prerender'
 import { useBaseImage, baseImageProps } from './_base'
-import { useImage, useHead } from '#imports'
+import { useImage, useHead, useNuxtApp } from '#imports'
 import { getFileExtension } from '#image'
 
 export const pictureProps = {
@@ -81,11 +81,16 @@ export default defineComponent({
       }
     }
 
+    const nuxtApp = useNuxtApp()
+    const initialLoad = nuxtApp.isHydrating
     onMounted(() => {
-      if (imgEl.value) {
-        imgEl.value.onload = (event) => {
-          ctx.emit('load', event)
-        }
+      if (!imgEl.value) { return }
+
+      if (imgEl.value.complete && initialLoad && !imgEl.value.getAttribute('data-error')) {
+        ctx.emit('load', new Event('load'))
+      }
+      imgEl.value.onload = (event) => {
+        ctx.emit('load', event)
       }
     })
 
@@ -100,6 +105,7 @@ export default defineComponent({
       h('img', {
         ref: imgEl,
         ..._base.attrs.value,
+        ...process.server ? { onerror: 'this.setAttribute(\'data-error\', 1)' } : {},
         ...imgAttrs,
         src: sources.value[0].src,
         sizes: sources.value[0].sizes,
diff --git a/src/runtime/composables.ts b/src/runtime/composables.ts
index d7a6afc..1e01485 100644
--- a/src/runtime/composables.ts
+++ b/src/runtime/composables.ts
@@ -1,6 +1,18 @@
-import { $Img } from '../types'
-import { useNuxtApp } from '#imports'
+import type { $Img } from '../types'
 
-export const useImage = () => {
-  return useNuxtApp().$img as $Img
+import { createImage } from './image'
+// @ts-expect-error virtual file
+import { imageOptions } from '#build/image-options'
+import { useNuxtApp, useRuntimeConfig } from '#imports'
+
+export const useImage = (): $Img => {
+  const config = useRuntimeConfig()
+  const nuxtApp = useNuxtApp()
+
+  return nuxtApp.$img as $Img || nuxtApp._img || (nuxtApp._img = createImage({
+    ...imageOptions,
+    nuxt: {
+      baseURL: config.app.baseURL
+    }
+  }))
 }
diff --git a/src/runtime/plugin.placeholder.ts b/src/runtime/plugin.placeholder.ts
new file mode 100644
index 0000000..2730849
--- /dev/null
+++ b/src/runtime/plugin.placeholder.ts
@@ -0,0 +1,13 @@
+import { defineNuxtPlugin } from '#imports'
+
+export default defineNuxtPlugin((nuxtApp) => {
+  const $img = () => throwImgError()
+  $img.getImage = () => throwImgError()
+  $img.getSizes = () => throwImgError()
+  $img.getMeta = () => throwImgError()
+  nuxtApp.provide('img', $img)
+})
+
+function throwImgError () {
+  throw new Error('[nuxt] [image] `$img` global utility is not enabled. You can enable it by setting `image: { inject: true }` in `nuxt.config`.')
+}
diff --git a/src/runtime/plugin.ts b/src/runtime/plugin.ts
index 0ae1e70..c568735 100644
--- a/src/runtime/plugin.ts
+++ b/src/runtime/plugin.ts
@@ -1,21 +1,11 @@
 import type { Plugin } from 'nuxt/app'
 import type { $Img } from '../types'
-import { createImage } from '#image'
-// @ts-ignore
-import { imageOptions } from '#build/image-options'
-import { defineNuxtPlugin, useRuntimeConfig } from '#imports'
+import { defineNuxtPlugin, useImage } from '#imports'
 
 export default defineNuxtPlugin(() => {
-  const config = useRuntimeConfig()
-  const img = createImage({
-    ...imageOptions,
-    nuxt: {
-      baseURL: config.app.baseURL
-    }
-  })
   return {
     provide: {
-      img
+      img: useImage()
     }
   }
 }) as Plugin<{ img: $Img }>
diff --git a/src/runtime/providers/directus.ts b/src/runtime/providers/directus.ts
new file mode 100644
index 0000000..96756fb
--- /dev/null
+++ b/src/runtime/providers/directus.ts
@@ -0,0 +1,20 @@
+import { joinURL } from 'ufo'
+import type { ProviderGetImage } from '../../types'
+import { createOperationsGenerator } from '#image'
+
+export const operationsGenerator = createOperationsGenerator({
+  joinWith: '&'
+})
+
+export const getImage: ProviderGetImage = (src, { modifiers = {}, baseURL } = {}) => {
+  // Separating the transforms from the rest of the modifiers
+  const transforms = modifiers.transforms
+  if (transforms && transforms.length > 0) {
+    // We stringify and encode in URL the list of lists, then apply it back to the modifiers
+    modifiers.transforms = new URLSearchParams(JSON.stringify(transforms)).toString().replace(/=+$/, '') as unknown as (string | number)[][]
+  }
+  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 4df2025..fdc29f2 100644
--- a/test/e2e/__snapshots__/no-ssr.test.ts.snap
+++ b/test/e2e/__snapshots__/no-ssr.test.ts.snap
@@ -60,6 +60,24 @@ exports[`browser (ssr: false) > contentful should render images 2`] = `
 ]
 `;
 
+exports[`browser (ssr: false) > directus should render images 1`] = `
+[
+  "http://localhost:8055/assets/ad514db1-eb90-4523-8183-46781437e7ee",
+  "http://localhost:8055/assets/ad514db1-eb90-4523-8183-46781437e7ee.jpg?withoutEnlargement=true&width=1024&height=256&fit=cover",
+  "http://localhost:8055/assets/ad514db1-eb90-4523-8183-46781437e7ee?width=256&format=webp",
+  "http://localhost:8055/assets/ad514db1-eb90-4523-8183-46781437e7ee?withoutEnlargement=true&transforms=%5B%5B%22blur%22%2C4%5D%2C%5B%22negate%22%5D%5D&width=256&format=webp",
+]
+`;
+
+exports[`browser (ssr: false) > directus should render images 2`] = `
+[
+  "http://localhost:8055/assets/ad514db1-eb90-4523-8183-46781437e7ee",
+  "http://localhost:8055/assets/ad514db1-eb90-4523-8183-46781437e7ee.jpg?withoutEnlargement=true&width=1024&height=256&fit=cover",
+  "http://localhost:8055/assets/ad514db1-eb90-4523-8183-46781437e7ee?width=256&format=webp",
+  "http://localhost:8055/assets/ad514db1-eb90-4523-8183-46781437e7ee?withoutEnlargement=true&transforms=%5B%5B%22blur%22%2C4%5D%2C%5B%22negate%22%5D%5D&width=256&format=webp",
+]
+`;
+
 exports[`browser (ssr: false) > edgio should render images 1`] = `
 [
   "https://opt.moovweb.net/?img=https://i.imgur.com/LFtQeX2.jpeg&height=200",
diff --git a/test/e2e/__snapshots__/ssr.test.ts.snap b/test/e2e/__snapshots__/ssr.test.ts.snap
index ac87b1f..0b607d8 100644
--- a/test/e2e/__snapshots__/ssr.test.ts.snap
+++ b/test/e2e/__snapshots__/ssr.test.ts.snap
@@ -60,6 +60,24 @@ exports[`browser (ssr: true) > contentful should render images 2`] = `
 ]
 `;
 
+exports[`browser (ssr: true) > directus should render images 1`] = `
+[
+  "http://localhost:8055/assets/ad514db1-eb90-4523-8183-46781437e7ee",
+  "http://localhost:8055/assets/ad514db1-eb90-4523-8183-46781437e7ee.jpg?withoutEnlargement=true&width=1024&height=256&fit=cover",
+  "http://localhost:8055/assets/ad514db1-eb90-4523-8183-46781437e7ee?width=256&format=webp",
+  "http://localhost:8055/assets/ad514db1-eb90-4523-8183-46781437e7ee?withoutEnlargement=true&transforms=%5B%5B%22blur%22%2C4%5D%2C%5B%22negate%22%5D%5D&width=256&format=webp",
+]
+`;
+
+exports[`browser (ssr: true) > directus should render images 2`] = `
+[
+  "http://localhost:8055/assets/ad514db1-eb90-4523-8183-46781437e7ee",
+  "http://localhost:8055/assets/ad514db1-eb90-4523-8183-46781437e7ee.jpg?withoutEnlargement=true&width=1024&height=256&fit=cover",
+  "http://localhost:8055/assets/ad514db1-eb90-4523-8183-46781437e7ee?width=256&format=webp",
+  "http://localhost:8055/assets/ad514db1-eb90-4523-8183-46781437e7ee?withoutEnlargement=true&transforms=%5B%5B%22blur%22%2C4%5D%2C%5B%22negate%22%5D%5D&width=256&format=webp",
+]
+`;
+
 exports[`browser (ssr: true) > edgio should render images 1`] = `
 [
   "https://opt.moovweb.net/?img=https://i.imgur.com/LFtQeX2.jpeg&height=200",
diff --git a/test/e2e/generate.test.ts b/test/e2e/generate.test.ts
index 5c80d86..1805a32 100644
--- a/test/e2e/generate.test.ts
+++ b/test/e2e/generate.test.ts
@@ -10,6 +10,9 @@ await setup({
   rootDir: fileURLToPath(new URL('../../playground', import.meta.url)),
   build: true,
   nuxtConfig: {
+    image: {
+      inject: false
+    },
     hooks: {
       'modules:before' () {
         const nuxt = useNuxt()
diff --git a/test/e2e/no-ssr.test.ts b/test/e2e/no-ssr.test.ts
index 8f76793..b0e9f1b 100644
--- a/test/e2e/no-ssr.test.ts
+++ b/test/e2e/no-ssr.test.ts
@@ -11,6 +11,7 @@ await setup({
   nuxtConfig: {
     ssr: false,
     image: {
+      inject: false,
       provider: 'ipx'
     }
   }
@@ -39,9 +40,7 @@ describe('browser (ssr: false)', () => {
 
     expect(requests.map(r => r.replace(url('/'), '/')).filter(r => r !== providerPath && !r.match(/\.(js|css)/))).toMatchSnapshot()
   })
-})
 
-describe('browser (ssr: false) common', () => {
   it('should emit load and error events', async () => {
     const page = await createPage(url('/events'))
     const logs: string[] = []
diff --git a/test/e2e/ssr.test.ts b/test/e2e/ssr.test.ts
index c0eedda..b0e7b08 100644
--- a/test/e2e/ssr.test.ts
+++ b/test/e2e/ssr.test.ts
@@ -10,6 +10,7 @@ await setup({
   browser: true,
   nuxtConfig: {
     image: {
+      inject: false,
       provider: 'ipx'
     }
   }
@@ -41,6 +42,20 @@ describe('browser (ssr: true)', () => {
     expect(requests.map(r => r.replace(url('/'), '/')).filter(r => r !== providerPath && !r.match(/\.(js|css)/))).toMatchSnapshot()
   })
 
+  it('should emit load and error events', async () => {
+    const page = await createPage()
+    const logs: string[] = []
+
+    page.on('console', (msg) => { logs.push(msg.text()) })
+
+    page.goto(url('/events'))
+
+    await page.waitForLoadState('networkidle')
+
+    expect(logs.filter(log => log === 'Image was loaded').length).toBe(4)
+    expect(logs.filter(log => log === 'Error loading image').length).toBe(1)
+  })
+
   it('works with runtime ipx', async () => {
     const res = await fetch(url('/_ipx/s_300x300/images/colors.jpg'))
     expect(res.headers.get('content-type')).toBe('image/jpeg')
diff --git a/test/providers.ts b/test/providers.ts
index f7b368a..69946ea 100644
--- a/test/providers.ts
+++ b/test/providers.ts
@@ -20,7 +20,8 @@ export const images = [
     cloudimage: { url: 'https://demo.cloudimg.io/v7/test.png' },
     edgio: { url: 'https://opt.moovweb.net/?img=/test.png' },
     storyblok: { url: 'https://a.storyblok.com/test.png' },
-    vercel: { url: '/_vercel/image?url=/test.png&w=1536&q=100' }
+    vercel: { url: '/_vercel/image?url=/test.png&w=1536&q=100' },
+    directus: { url: '/assets/1ac73658-8b62-4dea-b6da-529fbc9d01a4' }
   },
   {
     args: ['/test.png', { width: 200 }],
@@ -43,7 +44,8 @@ export const images = [
     cloudimage: { url: 'https://demo.cloudimg.io/v7/test.png?width=200' },
     edgio: { url: 'https://opt.moovweb.net/?img=/test.png&width=200' },
     storyblok: { url: 'https://a.storyblok.com/test.png/m/200x0' },
-    vercel: { url: '/_vercel/image?url=/test.png&w=320&q=100' }
+    vercel: { url: '/_vercel/image?url=/test.png&w=320&q=100' },
+    directus: { url: '/assets/1ac73658-8b62-4dea-b6da-529fbc9d01a4?width=200' }
   },
   {
     args: ['/test.png', { height: 200 }],
@@ -66,7 +68,8 @@ export const images = [
     cloudimage: { url: 'https://demo.cloudimg.io/v7/test.png?height=200' },
     edgio: { url: 'https://opt.moovweb.net/?img=/test.png&height=200' },
     storyblok: { url: 'https://a.storyblok.com/test.png/m/0x200' },
-    vercel: { url: '/_vercel/image?url=/test.png&w=1536&q=100' }
+    vercel: { url: '/_vercel/image?url=/test.png&w=1536&q=100' },
+    directus: { url: '/assets/1ac73658-8b62-4dea-b6da-529fbc9d01a4?height=200' }
   },
   {
     args: ['/test.png', { width: 200, height: 200 }],
@@ -89,7 +92,8 @@ export const images = [
     cloudimage: { url: 'https://demo.cloudimg.io/v7/test.png?width=200&height=200' },
     edgio: { url: 'https://opt.moovweb.net/?img=/test.png&width=200&height=200' },
     storyblok: { url: 'https://a.storyblok.com/test.png/m/200x200' },
-    vercel: { url: '/_vercel/image?url=/test.png&w=320&q=100' }
+    vercel: { url: '/_vercel/image?url=/test.png&w=320&q=100' },
+    directus: { url: '/assets/1ac73658-8b62-4dea-b6da-529fbc9d01a4?width=200&height=200' }
   },
   {
     args: ['/test.png', { width: 200, height: 200, fit: 'contain' }],
@@ -112,7 +116,8 @@ export const images = [
     cloudimage: { url: 'https://demo.cloudimg.io/v7/test.png?width=200&height=200&func=fit' },
     edgio: { url: 'https://opt.moovweb.net/?img=/test.png&width=200&height=200&fit=contain' },
     storyblok: { url: 'https://a.storyblok.com/test.png/m/fit-contain/200x200' },
-    vercel: { url: '/_vercel/image?url=/test.png&w=320&q=100' }
+    vercel: { url: '/_vercel/image?url=/test.png&w=320&q=100' },
+    directus: { url: '/assets/1ac73658-8b62-4dea-b6da-529fbc9d01a4?width=200&height=200&fit=contain' }
   },
   {
     args: ['/test.png', { width: 200, height: 200, fit: 'contain', format: 'jpeg' }],
@@ -135,7 +140,8 @@ export const images = [
     cloudimage: { url: 'https://demo.cloudimg.io/v7/test.png?width=200&height=200&func=fit&force_format=jpeg' },
     edgio: { url: 'https://opt.moovweb.net/?img=/test.png&width=200&height=200&fit=contain&format=jpeg' },
     storyblok: { url: 'https://a.storyblok.com/test.png/m/fit-contain/200x200/filters:format(jpeg)' },
-    vercel: { url: '/_vercel/image?url=/test.png&w=320&q=100' }
+    vercel: { url: '/_vercel/image?url=/test.png&w=320&q=100' },
+    directus: { url: '/assets/1ac73658-8b62-4dea-b6da-529fbc9d01a4?width=200&height=200&fit=contain&format=jpg' }
   }
 ] as const