Assets

Nuxt offers two options for your assets.

Nuxt uses two directories to handle assets like stylesheets, fonts or images.

  • The public/ directory content is served at the server root as-is.
  • The app/assets/ directory contains by convention every asset that you want the build tool (Vite or webpack) to process.

Public Directory

The public/ directory is used as a public server for static assets publicly available at a defined URL of your application.

You can get a file in the public/ directory from your application's code or from a browser by the root URL /.

Example

For example, referencing an image file in the public/img/ directory, available at the static URL /img/nuxt.png:

app/app.vue
<template>
  <img
    src="/img/nuxt.png"
    alt="Discover Nuxt"
  >
</template>

Assets Directory

Nuxt uses Vite (default) or webpack to build and bundle your application. The main function of these build tools is to process JavaScript files, but they can be extended through plugins (for Vite) or loaders (for webpack) to process other kinds of assets, like stylesheets, fonts or SVGs. This step transforms the original file, mainly for performance or caching purposes (such as stylesheet minification or browser cache invalidation).

By convention, Nuxt uses the app/assets/ directory to store these files but there is no auto-scan functionality for this directory, and you can use any other name for it.

In your application's code, you can reference a file located in the app/assets/ directory by using the ~/assets/ path.

Example

For example, referencing an image file that will be processed if a build tool is configured to handle this file extension:

app/app.vue
<template>
  <img
    src="~/assets/img/nuxt.png"
    alt="Discover Nuxt"
  >
</template>
Nuxt won't serve files in the app/assets/ directory at a static URL like /assets/my-file.png. If you need a static URL, use the public/ directory.

Static vs. Dynamic src

When an src is a static string literal in your template, the build tool rewrites it into a runtime helper that resolves the final URL. A public path such as /img/nuxt.png is wrapped so that your app.baseURL is applied when the page renders, and a bundled path such as ~/assets/img/nuxt.png additionally becomes an import that resolves to the hashed output file.

<template>
  <!-- Static paths are rewritten: app.baseURL is applied at runtime, and the bundled file is hashed. -->
  <img src="/img/nuxt.png">
  <img src="~/assets/img/nuxt.png">
</template>

Because app.baseURL is applied at runtime, a static public path works even when the base URL is only known at deploy time (for example set via NUXT_APP_BASE_URL), and it works whether or not the file is processed by the build. This resolution only happens for literal paths the build tool can see.

A bound :src whose value is assembled at runtime is opaque to the build tool, so none of that rewriting happens. The string is used exactly as written:

<template>
  <!-- This does not work: the path is built at runtime, so Vite never sees it as an import. -->
  <img :src="`~/assets/img/${name}.png`">
</template>

A runtime-built public path like /img/${name}.png is therefore not prefixed with app.baseURL. If your application is deployed below the origin root, prefix it yourself with useRuntimeConfig().app.baseURL (for example via joinURL).

The sections below cover how to handle each case when the path is only known at runtime.

Public Assets

If the files do not need to be processed or hashed, put them in the public/ directory and reference them by URL:

app/app.vue
<script setup lang="ts">
const props = defineProps<{
  name: string
}>()

const imageUrl = computed(() => `/img/${props.name}.png`)
</script>

<template>
  <img
    :src="imageUrl"
    :alt="props.name"
  >
</template>

Files in public/ keep their original filenames.

Bundled Assets with Vite

The approaches below are specific to Vite, Nuxt's default builder.

When the possible files are known, list their imports explicitly:

app/app.vue
<script setup lang="ts">
const props = defineProps<{
  theme: 'light' | 'dark'
}>()

const logos = {
  light: () => import('./assets/img/logo-light.png?url'),
  dark: () => import('./assets/img/logo-dark.png?url'),
}

const logoUrl = (await logos[props.theme]()).default
</script>

<template>
  <img
    :src="logoUrl"
    alt="Nuxt"
  >
</template>

Each import has a literal path, so Vite can find both files at build time while loading only the selected module at runtime.

When many files share a directory and extension, use a variable dynamic import instead of listing every file:

async function getImageUrl (name: string) {
  const image = await import(`./assets/img/${name}.png?url`)
  return image.default
}

Only the filename can be dynamic in this example. Keeping the directory and extension in the import lets Vite find the possible files at build time.

For a broader pattern or an explicit map of available files, use import.meta.glob:

const images = import.meta.glob<string>('./assets/img/*.{png,jpg,svg}', {
  query: '?url',
  import: 'default',
})

async function getImageUrl (name: string) {
  const load = images[`./assets/img/${name}.png`]

  if (!load) {
    throw new Error(`Unknown image: ${name}`)
  }

  return await load()
}

Glob imports are lazy by default. Add eager: true if the URLs must be available synchronously:

const images = import.meta.glob<string>('./assets/img/*.{png,jpg,svg}', {
  query: '?url',
  import: 'default',
  eager: true,
})

Every matching asset is still included in the build output. Lazy imports load each match on demand, while an eager glob loads all matches up front and can increase the initial JavaScript size or inline small assets.

Await a lazy import before using its URL in server-rendered markup. Vite's new URL(..., import.meta.url) pattern does not work with SSR.