ScreenshotNeo

BlogHow-to

How to Bundle Image Assets in Vue With Vite

Learn when to use src/assets or public, how to reference images in Vue components, and how to make asset URLs work in production builds and subpath deployments.

By the ScreenshotNeo team4 October 20269 min read

For images used by Vue components, put files in your source tree, usually under src/assets, then reference them in a Vue template or import them in JavaScript. The Vue plugin converts template asset references into imports, and Vite includes those images in the build graph and emits production URLs, commonly with hashed filenames. Put a file in the project-root public directory when it needs a fixed URL or should be copied unchanged.

This guide covers the usual Vue single-file component setup, JavaScript-created URLs, public files, CSS, dynamic image choices, deployment base paths, and troubleshooting. It assumes a client-side Vue app built with Vite; SSR needs extra care for dynamically resolved URLs.

1. Choose between src/assets and public

Need Use What happens
An image used by a Vue component src/assets and a template reference or import Vite can analyze the reference, include it in the build graph, and emit a production URL that may contain a hash.
A URL with a stable filename, or a file copied unchanged Project-root public/ The file is served from the site root in development and copied to the output root without a content hash.
A finite set of images selected by a known key A static import map or a supported static new URL() pattern Every possible asset is discoverable at build time.
A path assembled from arbitrary runtime data A runtime URL strategy appropriate to your app and deployment Vite cannot bundle a path it cannot determine during the build.

For assets in source code, Vite’s guidance is to “prefer importing assets unless you specifically need the guarantees provided by the public directory.” See the [Vite static asset guide](https://vite.dev/guide/assets.html) for the exact handling rules.

2. Reference a source asset in a Vue template

For a known image, the shortest approach is a relative path in the component template. The path is relative to the Vue file, not the project root.

<template>
  <main>
    <img src="../assets/hero.png" alt="A mountain landscape">
  </main>
</template>

For example, if the component is src/components/Hero.vue and the image is src/assets/hero.png, then ../assets/hero.png points to the file. Adjust the relative path to match your folders. With the Vue plugin enabled, template asset references are converted into imports for Vite to process.

Use a JavaScript import when the URL is needed in code

An import is useful when you need to reuse the URL, conditionally render an image, or pass it to another component:

<script setup>
import heroUrl from '../assets/hero.png'
</script>

<template>
  <img :src="heroUrl" alt="A mountain landscape">
</template>

A static asset import returns a resolved URL string. Vite can rename the emitted file while keeping the reference correct in the built application. Common image, media, and font types are recognized automatically. For another file type that should be treated as a URL, use the ?url suffix, for example import dataUrl from './file.custom?url'.

3. Use public for fixed-name files

Put a file at public/logo.png when its name must stay fixed, when it is referenced by a URL rather than source code, or when you need Vite to copy it unchanged. Reference it from the site root:

<template>
  <img src="/logo.png" alt="Company logo">
</template>

The browser URL is /logo.png, not /public/logo.png. The public directory name is stripped from the served path. This root-absolute example is suitable when the site is deployed at the domain root. For an app deployed under a nested base path, see the next section.

4. Make image URLs work under a deployment base path

If the app is deployed below a path such as /my-app/, configure Vite’s base and use URL patterns that Vite can rebase. Vite adjusts imported asset URLs, CSS url() references, and HTML asset references during a production build. For a public asset whose URL is assembled in code, use the exact expression import.meta.env.BASE_URL:

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  base: '/my-app/',
})
<script setup>
const logoUrl = `${import.meta.env.BASE_URL}logo.png`
</script>

<template>
  <img :src="logoUrl" alt="Company logo">
</template>

With that configuration, place the file at public/logo.png. Vite replaces the exact import.meta.env.BASE_URL expression with the configured base. For a root deployment, the base is typically /. If the final base path is unknown, Vite also supports a relative base such as ./; consult the [Vite production build guide](https://vite.dev/guide/build.html) for its behavior and browser caveats.

Test the built app at the actual subpath. A development server opened at / will not expose mistakes that only occur when the deployed app lives under /my-app/.

5. Build a URL in JavaScript

For a statically named file relative to the current module, use new URL():

const imageUrl = new URL('./hero.png', import.meta.url).href

Vite can transform this pattern when it can statically analyze the path. A finite dynamic set can also use a template literal that Vite can enumerate:

function imageUrl(name) {
  return new URL(`./images/${name}.png`, import.meta.url).href
}

This is not an arbitrary runtime filesystem lookup. If the path expression cannot be analyzed at build time, Vite leaves it as-is, which can produce a URL that does not exist in the deployed app. For a small, explicit collection, an import map makes the available files clear:

import forestUrl from './images/forest.png'
import lakeUrl from './images/lake.png'

const images = {
  forest: forestUrl,
  lake: lakeUrl,
}

const selectedImage = images[chosenScene]

Validate the selection or provide a fallback if chosenScene may not match a key. The new URL(..., import.meta.url) approach also has documented limitations for SSR; choose a URL strategy compatible with your server-rendering setup rather than assuming browser module behavior applies on the server.

6. Reference images from CSS and HTML

CSS url() references are processed similarly to imported assets. A stylesheet inside the source tree can refer to a neighboring file with a relative path:

/* src/components/hero.css */
.hero {
  background-image: url('../assets/hero-background.webp');
}

Vite resolves the URL as part of the build. References in index.html are also processed because Vite treats that file as source code in the module graph. If a URL must stay fixed, use a public asset path and account for the configured base path.

7. Check the production build

  1. Confirm the Vue Vite plugin is configured in vite.config.js or vite.config.ts.
  2. Confirm every source asset reference points to a real file, relative to the component, script, or stylesheet that uses it.
  3. Run the project’s production build command, commonly npm run build when the project script is configured that way.
  4. Serve the generated output using the project’s chosen static host or preview workflow and inspect the image requests in the browser’s network panel.
  5. Repeat at the configured nested base path if the production site is not served from the domain root.

Vite’s assetsInlineLimit configuration controls when small assets may be inlined as data URLs. The threshold depends on the installed Vite version and project configuration, so check your configuration and version instead of relying on a hard-coded number. Inlining can avoid a separate request for a small asset but adds its bytes to the containing output; it is not a substitute for choosing appropriate image dimensions and formats.

8. Common errors and fixes

Symptom Likely cause Fix
Image works in development but returns 404 after deployment A root-relative URL ignores the deployed base path, or a dynamic path was not bundled. Set Vite’s base, use imports for source assets, or build public asset URLs with import.meta.env.BASE_URL. Test the built output at the real subpath.
/public/logo.png returns 404 The directory name is not part of the public URL. Use /logo.png at the domain root, or prepend import.meta.env.BASE_URL for a configured base.
Imported image path cannot be resolved The relative path is incorrect or the filename’s case differs. Some development filesystems are case-insensitive while deployment hosts are not. Check the path relative to the importing file and match filename capitalization exactly.
A computed image URL is broken in production Vite could not statically analyze the runtime-generated path. Use explicit imports, an import map, or a supported statically analyzable new URL() pattern. For arbitrary remote or user-provided paths, use a runtime URL that is actually available to the browser.
Image is missing only in SSR The client-oriented new URL(..., import.meta.url) pattern does not generally provide an SSR-safe asset lookup. Use the SSR framework’s asset handling or a URL strategy available to both server and client, and verify its deployment behavior.
Unknown extension is treated as a module The file extension is not among the types Vite recognizes as an asset. Use an explicit ?url import when the file should be emitted and referenced as a URL.
Image appears blurry or unexpectedly large The source dimensions, selected image, or device pixel ratio do not match the display size. Inspect the actual emitted image and rendered dimensions. Provide appropriately sized source variants where needed; Vite asset handling does not automatically choose responsive variants for you.

9. Performance, reliability, and cost

Asset imports make references explicit and let the build produce URLs that remain connected to their emitted files. Hashed names are useful for cache invalidation when file content changes. Public assets keep stable names and are copied unchanged, which is useful for externally referenced files but means you must manage cache behavior and filename changes yourself.

Keep image payloads appropriate to their display size and use a format that fits the content and browser requirements. Avoid importing a large collection if only a small subset is needed on an initial view. For finite image choices, an import map gives Vite a known asset set; arbitrary runtime paths can fail after deployment because the build cannot emit files it was not told about.

Vite’s asset handling does not itself create hosting or image-processing costs beyond the files your build emits. The eventual transfer and storage costs depend on your hosting and delivery setup. Verify emitted files and request behavior in the production environment rather than assuming a development-server result guarantees deployment behavior.

10. Inspect a page screenshot with ScreenshotNeo

After deploying, a screenshot can help you inspect whether an image appears on the rendered page at the expected route and viewport. [ScreenshotNeo](https://screenshotneo.com) is a website screenshot API and MCP server. Its API accepts a URL and returns a screenshot or PDF; it does not replace Vite’s asset bundling or repair an incorrect asset path.

Or skip the browser setup

Use this one-call capture against your deployed page; replace the target URL with the page you want to inspect. See the [ScreenshotNeo API documentation](https://screenshotneo.com/docs/) for request options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers identify the page verdict and billing status. Its MCP server provides screenshot tools for AI agents. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Create a [free ScreenshotNeo account](https://screenshotneo.com/account/sign-up/) to capture a deployed page.

11. FAQ

Will an imported image keep its original filename?

Not necessarily. Vite commonly emits imported assets with hashed filenames so their URLs can change when their content changes. Use public when the name must remain fixed.

Can I load any image by joining a folder and a string?

Not if that path must point to a bundled source asset. Vite needs to discover source files at build time. Use explicit imports or a statically analyzable pattern for a finite set, or use a real runtime URL for externally available images.

Should I use a template reference or an import?

Either works for a known image in a Vue component. Use a template reference for simple markup; import it when code needs to select, reuse, or pass around the resolved URL.

Does a public asset get hashed or transformed?

Public files are copied to the output root unchanged and retain their names. Use a source import when you want the asset handled as part of the build graph.

Sources