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.
npx vue-image-kit placeholders --dry-run # preview
npx vue-image-kit placeholdersRequires sharp as a dev dependency — hazehash for --mode hazehash and thumbhash for --mode thumbhash:
npm install -D sharpWhich 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 thehazehashpackage 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 bysrc— no props are added to the templates. - Template — written straight into the
<VImage>tag as props. This is used for local imports, whosesrcat runtime is a hashed build URL that a lookup bysrccan't match. It's also used for all images when no manifest registration is found in the project.
<!-- 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:
<!-- 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
--replacedoesn'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
sourcesobject 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.
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/heightare kept; missing ones are added as usual. - A placeholder bound to an expression (
:thumbhash="item.hash") and:imageare left alone and listed in the summary — that data comes from your code or the build. - The usage's
srcstill has to be resolvable; a placeholder on an image with a dynamicsrccan't be recomputed. --no-writeskips 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:
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
--modeand 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.dominanttakes the most frequent color;averagetakes 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):
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 placeholdersRegister it once:
import { VImageKitPlugin } from '@macrulez/vue-image-kit'
import placeholders from './image-placeholders'
app.use(VImageKitPlugin, { placeholders })// 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 existThe 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 asscan.--manifest <path>· default:src/image-placeholders.ts. Manifest file,.tsor.json.--mode <mode>· default:hazehashwhen the package is installed, otherwiseblurhash.hazehash,blurhash,thumbhashorcolor.--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--remoteneeded.--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:
npx vue-image-kit placeholders --remote --hosts res.cloudinary.com --limit 200Config file
// 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.