Skip to content

Integrations

Nuxt module

ts
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['vue-image-kit/nuxt'],
  vueImageKit: {
    breakpoints: {
      sm: '(max-width: 640px)',
      md: '(max-width: 1024px)',
    },
  },
})

After setup:

  • <VImage> and v-lazy-img are available in all templates without imports
  • All composables (useImage, useImagePreloader, etc.) are auto-imported
  • All utilities (generateSrcset, buildSizes, generatePreloadLink, etc.) are auto-imported

onDemandServer: on-demand images as a Nitro route

Set onDemandServer to register vue-image-kit/server's handler as a real Nitro server route via addServerHandler — no manual server/routes/... file needed:

ts
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['vue-image-kit/nuxt'],
  vueImageKit: {
    onDemandServer: true, // root defaults to Nuxt's own `public/` dir
  },
})
vue
<VImage src="/photos/cat.jpg" alt="Photo" :widths="[400, 800]" loader="server" />

onDemandServer: true uses every default (root public/, route /_vik/image); pass an object to override any of them — same options as createImageHandler (root, cacheDir, maxAge, allowedWidths, maxWidth), plus route:

ts
vueImageKit: {
  onDemandServer: {
    root: 'assets/uploads',
    route: '/api/img',
    maxWidth: 2000,
  },
},

The route also becomes loader="server"'s default automatically — no need to repeat it as serverRoute unless the handler lives somewhere this module didn't register (e.g. deployed separately). root is only ever put in private runtime config (useRuntimeConfig().vueImageKitServer, server side only) — never exposed to the client, unlike breakpoints.

Vite plugin

Process images at build time — same as the CLI but integrated into the Vite lifecycle. Runs on buildStart and re-runs in dev mode when source images change.

ts
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { vueImageKit } from 'vue-image-kit/vite'

export default defineConfig({
  plugins: [
    vue(),
    vueImageKit({
      input: './src/images',
      output: './public/images',
      widths: [400, 800, 1200],
      manifest: './src/assets/images.ts',
    }),
  ],
})

All CLI options are supported. sharp must be installed as a dev dependency.

buildStart/handleHotUpdate call the same generate() the CLI does, so incremental generation applies here too — and is auto-enabled during vite dev specifically (not vite build) unless you set incremental explicitly. A single source-file change in dev reprocesses just that file, not the whole batch.

Build-time imports

The plugin also resolves query-suffixed imports, so you never wire props by hand — the metadata comes straight into your JS at build time:

ts
import meta from './photo.jpg?vik'
// → { src, srcset, webp, avif, width, height, placeholder, blurhash, thumbhash, name, src400, ... }

import hash from './photo.jpg?thumbhash'
// → 'base64string'

Pass the metadata straight to <VImage>'s image prop — no manual field wiring:

vue
<script setup lang="ts">
import meta from './hero.jpg?vik'
</script>

<template>
  <VImage :image="meta" alt="Hero" />
</template>
  • ?vik resizes/encodes the image into output and returns the full manifest entry (URLs use publicPath, exactly like the generated manifest). The ThumbHash is always included.
  • ?thumbhash computes only the hash string and writes no files.

Both re-run when the source image changes in dev. sharp is required; thumbhash is required for hash output.

TypeScript — enable typed ?vik / ?thumbhash imports by referencing the bundled declarations once (e.g. in env.d.ts):

ts
/// <reference types="vue-image-kit/vite/client" />

On-demand dev serving

Both the CLI and the build-time imports above are batch/ahead-of-time — they process images before they're requested. If you'd rather not run a build step at all during development, set dev.onDemand: true and images resize on request instead, cached to disk after the first hit:

ts
vueImageKit({
  dev: { onDemand: true }, // mounts a handler at /_vik/image during `vite dev`
})
html
<img src="/_vik/image?src=/photos/cat.jpg&w=800&format=webp" />

This is dev-only — configureServer (the Vite hook it uses) never runs during vite build. For production without a CDN, mount the same handler in your own server — see Self-hosted on-demand server below.

Self-hosted on-demand server

No CDN, don't want to pre-run the CLI, want images resized per-request in production too? vue-image-kit/server exports the same handler the Vite dev middleware above uses — a small, framework-agnostic Node request handler you mount yourself.

ts
import { createImageHandler } from 'vue-image-kit/server'

const handler = createImageHandler({ root: './public' })

Plain Node http:

ts
import { createServer } from 'node:http'
import { createImageHandler } from 'vue-image-kit/server'

const imageHandler = createImageHandler({ root: './public' })

createServer((req, res) => {
  if (req.url?.startsWith('/_vik/image')) {
    imageHandler(req, res)
    return
  }
  // ...serve everything else
}).listen(3000)

Express:

ts
app.get('/_vik/image', createImageHandler({ root: './public' }))

Request shape: GET {route}?src=/photos/cat.jpg&w=800&format=webp&q=80src is required (resolved strictly under root; anything that escapes it is rejected with 403, a nonexistent file with 404). w, format (jpg/webp/avif/png) and q are all optional. With neither w nor format, the original bytes are streamed through untouched — no sharp call, works for any file type. Otherwise the result is resized/re-encoded with sharp and cached to disk (cacheDir, default <root>/.vik-cache) keyed by every param that affects the output, so a repeat request is a cache hit, not a re-encode.

ts
buildImageUrl('/photos/cat.jpg', { width: 800, format: 'webp' })
// → '/_vik/image?src=%2Fphotos%2Fcat.jpg&w=800&format=webp'

Options

OptionTypeDefaultDescription
rootstringRequired. Directory src is resolved (and confined) to.
cacheDirstring<root>/.vik-cacheWhere transformed output is cached.
maxAgenumber31536000 (1 year)Cache-Control: public, max-age=..., must-revalidate, plus a source-derived ETag. A cached response is still reused with zero request for the full maxAge — that's what max-age means, regardless of this header — this only affects what happens after it expires (or on an explicit revalidation, e.g. a hard refresh): a cheap ETag-backed 304 instead of a full re-download, and (unlike immutable) the browser is at least allowed to ask. If a source can change and that needs to be picked up sooner than a year, lower maxAge, use no-cache, or put a version in the URL — this option alone won't make that happen.
allowedWidthsnumber[]Restrict w to exactly these values (400 on anything else). Unset: any positive integer, clamped to maxWidth.
maxWidthnumber4000Upper bound for w when allowedWidths isn't set.

Scope: this handles one transform per request for standard raster sources (jpg/png/webp/avif → jpg/webp/avif/png) — the realistic "give me this photo at width X" case. It does not replicate the CLI's GIF/SVG special-casing or multi-variant batch generation (src/cli/processor.ts is the place for that) — a .gif/.svg source always passes through untouched, byte-identical, regardless of w/format (sharp is never asked to resize a GIF here — that would silently drop its animation — or rasterize an SVG). Any other unrecognized source extension falls back to jpg when a transform is requested, or passes through untouched when neither w nor format is set.

Security note: error responses include the underlying error message as plain text (e.g. "sharp is not installed") to make self-hosted setups easier to debug. If you don't want that detail reaching clients, put this behind your own error-handling middleware in production.

Wiring VImage to it: loader="server"

VImage's loader prop builds request URLs against the handler automatically — no manual buildImageUrl() calls:

vue
<VImage src="/photos/cat.jpg" alt="Photo" :widths="[400, 800]" loader="server" />

Same shape as cdn: combined with widths, each candidate gets its own request URL instead of one shared URL. The route defaults to /_vik/image (matching the Vite dev middleware and the handler's own convention) — override it per-component with loaderRoute, or set it once for every VImage via the Vue plugin (app.use(VImageKitPlugin, { serverRoute: '/api/img' })) or the Nuxt module's onDemandServer option (see Nuxt module above, which also registers the actual Nitro route for you). If both cdn and loader="server" are set on the same image, cdn wins — an external CDN already resolves the image, the local on-demand server is the fallback for when there isn't one.