Skip to content

CLI — Placeholders for Existing Images ​

npx vue-image-kit placeholders fills in a placeholder for every <VImage> that doesn't have one yet: it reads the real image, computes a HazeHash, a BlurHash, a ThumbHash or the image's dominant color plus its original size, and delivers the result either through a placeholders manifest or straight into the template.

Integration Recipes show ready-made configurations for every case — with a note on what each is for and when to use it.

bash
npx vue-image-kit placeholders --dry-run   # preview
npx vue-image-kit placeholders

Requires sharp as a dev dependency — hazehash for --mode hazehash and thumbhash for --mode thumbhash:

bash
npm install -D sharp

Which images are processed ​

The command scans the project and takes every <VImage> that has none of hazehash, blurhash, thumbhash, placeholder, placeholderColor, image or a placeholderMode of color/shimmer, and whose image can be resolved:

  • Local imports and public/ paths — read from disk.
  • CDN and remote URLs — downloaded only with --remote; a project can reference thousands of them. A CDN image is requested as a small rendition (128px wide) through its adapter, and its original size is read from just the first 64 KB of the original file.

Skipped, and listed in the summary: a dynamic src, data: URIs, files that don't exist, and usages that spread their props with v-bind. Usages that already have a placeholder are skipped too, unless --replace is given.

What's computed ​

  • --mode hazehash (default when the hazehash package is installed) — a HazeHash, a 7–48 byte string.
  • --mode blurhash (default otherwise) — a BlurHash.
  • --mode thumbhash — a ThumbHash.
  • --mode color — only the dominant color: the most frequent color of the downscaled image, ignoring transparent pixels.

Manifest entries always include the dominant color and the original width/height as well; a template gets only the value of the chosen mode (plus the size, when it has none). An SVG always gets only its color and size.

How the result is delivered ​

Chosen per usage:

  • Manifest — public/, CDN and remote images go into a placeholders manifest when the project registers one. <VImage> finds its entry at runtime by src — no props are added to the templates.
  • Template — written straight into the <VImage> tag as props. This is used for local imports, whose src at runtime is a hashed build URL that a lookup by src can't match. It's also used for all images when no manifest registration is found in the project.
vue
<!-- before -->
<VImage src="/images/hero.jpg" alt="Hero" />

<!-- after, written into the template -->
<VImage
  src="/images/hero.jpg"
  alt="Hero"
  :width="1200"
  :height="800"
  blurhash="LJ9+E%}}$^I_^Z=:xUNMIwI^R.s+"
/>

The props are inserted after the last existing attribute, on new lines with the same indentation when the tag already spans several lines. :width/:height are added only when the tag has neither. With --mode color the template gets placeholder-color="#…".

Source files are never edited when they have uncommitted git changes; they're listed instead, so you can commit or stash first. --force-write overrides this, --no-write leaves the sources alone entirely, and --dry-run prints every edit without writing anything.

Art-direction sources ​

Entries of a literal :sources="{ … }" prop are processed too — independently of the image's own src, so a <VImage> with a dynamic src can still get placeholders for its sources. An entry is a path or URL (mobile: '/m.jpg') or an object with a src (tablet: { src: tabletImage, width: 800, height: 400 }). Each is resolved and delivered by the same rules as an image: public/, CDN and remote sources go into the manifest when one is registered, and local imports — or every source, when none is registered — are written into the literal:

vue
<!-- before -->
<VImage
  src="/desktop.jpg"
  :sources="{ tablet: '/tablet.jpg', mobile: { src: mobileImage, width: 400, height: 600 } }"
/>

<!-- after, written into the template -->
<VImage
  src="/desktop.jpg"
  :sources="{
    tablet: {
      src: '/tablet.jpg',
      width: 800,
      height: 400,
      blurhash: 'LKO2?U%2Tw=w]~RBVZRi};RPxuwH',
    },
    mobile: { src: mobileImage, width: 400, height: 600, blurhash: 'LEHV6nWB2yk8pyo0adR*.7kCMdnj' },
  }"
/>

A bare string becomes an { src, … } object. width/height are added only when the entry has neither. <VImage> shows an entry's placeholder while its breakpoint is active — see Per-breakpoint placeholders.

  • An entry that already has a placeholder is left alone, and --replace doesn't change that.
  • An entry that is a plain { avif, webp, fallback } object is skipped — it can't carry a placeholder; use { src: { avif, webp, fallback } }.
  • A sources object that reaches the template through a variable can fill the manifest, but is never edited in place; without a registered manifest those entries are reported as not editable.

Replacing existing placeholders ​

With --replace, usages that already have a placeholder are redone as well: their static hazehash, blurhash, thumbhash, placeholder, placeholder-color and placeholder-mode attributes are removed, and the placeholder of the chosen --mode takes their place.

bash
npx vue-image-kit placeholders --replace --dry-run
    src/components/Blog.vue:31  - thumbhash  + blurhash="L2M^#R=1fQ=1]UjtfQjtfQfQfQfQ"
    src/components/Card.vue:14  - placeholder-color  - placeholder-mode  + :width="400" :height="300" blurhash="LJ9+E%}}$^I_^Z=:xUNMIwI^R.s+"
    src/components/Hero.vue:9   - placeholder-color  (value → manifest)
  • The new value is delivered by the same rules as above: into the template, or — for a public/, CDN or remote image when a manifest is registered — into the manifest, with the old attribute only removed (otherwise it would override the manifest entry).
  • Existing width/height are kept; missing ones are added as usual.
  • A placeholder bound to an expression (:thumbhash="item.hash") and :image are left alone and listed in the summary — that data comes from your code or the build.
  • The usage's src still has to be resolvable; a placeholder on an image with a dynamic src can't be recomputed.
  • --no-write skips replacements entirely, and files with uncommitted changes aren't edited without --force-write.

Folders and URLs ​

A usage whose src is built at runtime can't be found by scanning. --dir adds every image of a folder to the manifest instead, whether or not any template mentions it:

bash
npx vue-image-kit placeholders --dir public/images --dir src/img=/assets/img
  • A folder is scanned recursively. Each file is keyed by the URL it is served at: its path from the public directory for a folder inside it, <urlPrefix>/<path from the folder> for --dir <folder>=<urlPrefix>.
  • A folder outside the public directory without a prefix is keyed by its project path (/src/img/a.jpg), which only matches the dev-server URL; the command warns about it.
  • --url <url> (repeatable) adds a single remote or CDN image and downloads it without --remote — naming it is the consent.
  • Folder and URL entries are merged with the ones found through usages, use the same cache, and honour --mode and the tuning.

The same manifest can be built by the Vite plugin on every build, without the command — see Placeholders manifest from folders.

v-lazy-img and useBackgroundImage() ​

v-lazy-img and useBackgroundImage() read the manifest too, so the command fills it for their usages with a static src — through the manifest only, never into the template. That needs a registered manifest and a public/, CDN or remote src; a usage with its own placeholder, a local import or a dynamic src is skipped and counted in the summary.

Tuning the hashes ​

  • --components <XxY> · default: 4x3. BlurHash components per axis, 1–9 each.
  • --sample <px> · default: 100. The size the image is downscaled to before hashing, up to 256.
  • --color <strategy> · default: dominant. dominant takes the most frequent color; average takes the mean of the opaque pixels.

The cache is dropped when any of them changes. In the config file they live under tuning: { components: [x, y], sample, color }.

Registering the manifest ​

The manifest is written to src/image-placeholders.ts by default (image-placeholders.ts in the Nuxt source directory for a Nuxt project):

ts
import type { PlaceholderManifest } from '@macrulez/vue-image-kit'

// Generated by `vue-image-kit placeholders` — re-run the command instead of editing by hand.
const placeholders: PlaceholderManifest = {
  "/images/hero.jpg": {"blurhash":"LJ9+E%}}$^I_^Z=:xUNMIwI^R.s+","color":"#1e6ec7","width":1200,"height":800},
  "/logo.svg": {"color":"#7c3aed","width":120,"height":60},
}

export default placeholders

Register it once:

ts
import { VImageKitPlugin } from '@macrulez/vue-image-kit'
import placeholders from './image-placeholders'

app.use(VImageKitPlugin, { placeholders })
ts
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@macrulez/vue-image-kit/nuxt'],
  vueImageKit: { placeholders: './image-placeholders.ts' },
})

The command detects either form — as well as app.provide(PLACEHOLDERS_KEY, …) — when it scans the project. How <VImage> uses an entry is described in Placeholders.

Output ​

[vue-image-kit] placeholders — mode: blurhash
  Images: 1 computed (0 local, 1 remote) · 4 from cache · 0 failed
  Manifest: 4 usage(s) → src/image-placeholders.ts (written)
  Source edits: 1 usage(s) in 1 file(s)
    src/App.vue
  Skipped:
       1 usage(s) have a dynamic src
       1 usage(s) already have a placeholder
       1 usage(s) point at a file that does not exist

The command exits with code 1 when an image fails to process (for example, a remote URL returns 404); the rest are still processed.

Cache ​

Results are cached in node_modules/.cache/vue-image-kit/placeholders.json. A local file is recomputed only when its modification time or size changes; a downloaded image is reused until --refresh-remote. --no-cache recomputes everything. Manifest entries for remote images stay in the manifest on later runs without --remote. The manifest file can be reformatted by your formatter or edited by hand — the command parses it as code when it reads it back.

Options ​

  • --root, --include, --exclude, --public-dir, --alias, --package, --no-vite-config — the same project discovery options as scan.
  • --manifest <path> · default: src/image-placeholders.ts. Manifest file, .ts or .json.
  • --mode <mode> · default: hazehash when the package is installed, otherwise blurhash. hazehash, blurhash, thumbhash or color.
  • --budget <bytes> · default: 28. Size of a HazeHash string in bytes, 7–48.
  • --dir <path> — also compute every image in a folder, repeatable; --dir <path>=<urlPrefix> for a folder served from another URL. See Folders and URLs.
  • --url <url> — also compute a remote or CDN image, repeatable; no --remote needed.
  • --components, --sample, --color — see Tuning the hashes.
  • --remote — also download CDN and remote images.
  • --hosts <list> — with --remote, only these hosts (comma-separated; subdomains included).
  • --limit <n> — with --remote, download at most this many images in one run.
  • --concurrency <n> · default: 4. Images processed in parallel.
  • --timeout <ms> · default: 15000. Per-request timeout for remote images.
  • --max-bytes <n> · default: 15728640 (15 MB). Remote images larger than this are skipped.
  • --dry-run — show what would change without writing anything.
  • --no-write — never edit source files; only write the manifest.
  • --force-write — edit source files even when they have uncommitted changes.
  • --no-cache — ignore the cache and recompute everything.
  • --refresh-remote — download remote images again even if they're cached.
  • --replace — also redo usages that already have a static placeholder; see Replacing existing placeholders.

Example — remote images from one CDN, in batches:

bash
npx vue-image-kit placeholders --remote --hosts res.cloudinary.com --limit 200

Config file ​

js
// vue-image-kit.config.js
export default {
  placeholders: {
    manifest: './src/image-placeholders.json',
    mode: 'color',
    remote: true,
    hosts: ['res.cloudinary.com'],
    limit: 500,
    replace: false,
    dirs: ['public/images', { dir: 'src/img', urlPrefix: '/assets/img' }],
    urls: ['https://cdn.example.com/hero.jpg'],
    tuning: { components: [4, 3], sample: 100, color: 'dominant' },
  },
}

The placeholders section also accepts every key of the scan section.