I'm building a Nuxt 3 application using @nuxt/image with a custom provider for imgproxy. Imgproxy requires URLs to be signed using HMAC-SHA256 with a secret key and salt.
The Problem
I need to sign image URLs server-side because:
- The crypto module doesn't work in the browser
- The signing key/salt must stay secret (not exposed to client)
- Allow only the server to process images with imgproxy (no public endpoint)
However, every approach I've tried has significant drawbacks.
My Setup
Custom imgproxy provider (~/providers/imgproxy.ts):
import { createHmac } from "crypto";
import { defineProvider } from "@nuxt/image/runtime";
export default defineProvider<{ baseURL: string; key?: string; salt?: string }>({
getImage(src, { modifiers = {}, baseURL, key, salt }) {
// Sign URL using HMAC-SHA256
const signedPath = generateSignedPath(src, modifiers, key, salt);
return { url: `${baseURL}${signedPath}` };
},
});
Image component uses useImage() from @nuxt/image which calls the provider.
Approaches Tried
- Server Component (Image.server.vue) Made the Image component a server component so signing only happens on server. Problem: Nuxt Islands fetch their HTML payload during hydration (/__nuxt_island/...), which causes the browser to parse the HTML again and fetch every image twice.
- Redirect API Endpoint Provider returns /api/imgproxy-sign?src=..., API signs the URL and returns 302 redirect. Problem: Hydration mismatch warning because server renders /api/imgproxy-sign?... but client expects the same (or different) URL. Also, the redirect is visible in the network tab and adds latency.
Requirements
- Sign URLs on server only (can't use crypto in browser)
- Keep secrets (key/salt) on server
- No hydration mismatch warnings
- Images should only be fetched once (no duplicate requests)
- Ideally no redirect - direct signed URLs in HTML
- Works for both SSR initial load and client-side navigation
Question
Is there a way to use @nuxt/image with server-side URL signing that:
- Produces the same URL on server and client (no hydration mismatch)
- Doesn't cause double image fetches
- Keeps secrets on the server
I'm open to alternative architectures. Perhaps there's a Nuxt pattern I'm missing, or a different way to structure this that I haven't considered?
Environment:
- Nuxt 3.x
- @nuxt/image latest
- imgproxy (self-hosted)