core icon indicating copy to clipboard operation
core copied to clipboard

Use `<NuxtImg>` with served blob image

Open lorenzofiamingo opened this issue 9 months ago • 3 comments

Is your feature request related to a problem? Please describe. I’m trying to display an image served from my NuxtHub blob, as explained here. While it works perfectly with a standard tag, the image doesn’t show up when I use <NuxtImg>. The default ipx provider of <NuxtImg> doesn't work since the image is returned from an api path. The none provider works fine like <img> but the image is returned as is. The cloudflare provider works but only in production, when the website served by cloudflare. (In deployed development could work but with any origin activated and specifying the production url as the base url of the provider)

Describe the solution you'd like Would be nice to have a nuxthub provider (or maybe a <NuxtHubImg>) that permits to leverage <NuxtImg> out-of-the-box.

Describe alternatives you've considered For now since Cloudflare Images doesn’t provide a way to set images when working locally, I created a <NuxtImg> provider that uses Cloudflare Images in production and falls back to the original R2 image in development.

nuxt.config.ts

image: {
  providers: {
    cloudflareOnProd: {
      provider: '~/providers/cloudflareOnProd.ts',
      options: {
        prodSiteURL: 'https://example.com'
      }
    }
  }
}

cloudflareOnProd.ts

import type { ProviderGetImage } from '@nuxt/image'
import { getImage as getImageWithCloudflare } from '#image/providers/cloudflare'
import { getImage as getImageWithNone } from '#image/providers/none'

export const getImage: ProviderGetImage = (src, options, ctx) => {
    if (useRuntimeConfig().public.siteURL === options.prodSiteURL) {
        return getImageWithCloudflare(src, options, ctx)
    } else {
        return getImageWithNone(src, options, ctx)
    }
}
Legacy

cloudflareOnProd.ts

import { joinURL, encodeQueryItem } from 'ufo'
import type { ProviderGetImage } from '@nuxt/image'
import { createOperationsGenerator } from '#image'

const operationsGenerator = createOperationsGenerator({
    keyMap: {
        width: 'w',
        height: 'h',
        dpr: 'dpr',
        fit: 'fit',
        gravity: 'g',
        quality: 'q',
        format: 'f',
        sharpen: 'sharpen',
    },
    valueMap: {
        fit: {
            cover: 'cover',
            contain: 'contain',
            fill: 'scale-down',
            outside: 'crop',
            inside: 'pad',
        },
        gravity: {
            auto: 'auto',
            side: 'side',
        },
    },
    joinWith: ',',
    formatter: (key, val) => encodeQueryItem(key, val),
})

const defaultModifiers = {}

// https://developers.cloudflare.com/images/image-resizing/url-format/
export const getImage: ProviderGetImage = (src, {
    modifiers = {},
    baseURL = '/',
    prodSiteURL
} = {}) => {
    const mergeModifiers = { ...defaultModifiers, ...modifiers }
    const operations = operationsGenerator(mergeModifiers as any)
    
    let url
    
    if (useRuntimeConfig().public.siteURL === prodSiteURL) {
        // https://<ZONE>/cdn-cgi/image/<OPTIONS>/<SOURCE-IMAGE>
        url = operations ? joinURL(baseURL, 'cdn-cgi/image', operations, src) : joinURL(baseURL, src)
    } else {
        url = joinURL(baseURL, src)
    }
    
    return {
        url
    }
}

lorenzofiamingo avatar Mar 28 '25 11:03 lorenzofiamingo

Thank you for pointing me in the direction of custom image provider for handling blobs @lorenzofiamingo Would be nice if this was initially handled by nuxtImg.

kamerat avatar Jul 03 '25 15:07 kamerat

I'm not sure if this is the best way to do this. But I managed to use NuxtImg without creating a custom provider. Basically, you need to add route rules for the blob server route and @nuxt/image alias. This works on deployed projects on Cloudflare too.

// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    '/blob/**': { ssr: false },
  },
  image: {
    domains: ['localhost:3000'],
    alias: {
      images: 'http://localhost:3000/blob',
    },
  },
})
// server/routes/blob/[...pathname].get.ts
export default eventHandler(async (event) => {
  const { pathname } = getRouterParams(event)

  if (!pathname) {
    return createError({
      statusCode: 404,
      statusMessage: 'Not Found',
    })
  }

  setHeader(event, 'Content-Security-Policy', 'default-src \'none\';')
  return hubBlob().serve(event, pathname)
})

larrasu avatar Oct 18 '25 23:10 larrasu

Thanks @larrasu and @lorenzofiamingo!

I guess this is something that will be fixed by #668 ?

onmax avatar Oct 20 '25 07:10 onmax