Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Assets with dynamic names are not resolved #14766

Open
tobiasdiez opened this issue Aug 31, 2022 · 76 comments
Open

Assets with dynamic names are not resolved #14766

tobiasdiez opened this issue Aug 31, 2022 · 76 comments

Comments

@tobiasdiez
Copy link
Contributor

Environment

  • Operating System: Linux
  • Node Version: v16.14.2
  • Nuxt Version: 3.0.0-rc.8
  • Package Manager: npm@7.17.0
  • Builder: vite
  • User Config: -
  • Runtime Modules: -
  • Build Modules: -

Reproduction

https://stackblitz.com/edit/github-pq8nym?file=app.vue

Describe the bug

If you have an image referencing a dynamic asset, e.g.

<template>
  <img :src="`~/assets/${dynamic_image_name}`" alt="Discover Nuxt 3" />
</template>
<script setup lang="ts">
const dynamic_image_name = 'zero-config.svg';
</script>

then this is rendered as

<img src="~/assets/zero-config.svg" alt="Discover Nuxt 3">

without correctly resolving (and copying) the image, thus it doesn't show in the browser.

Additional context

Refs nuxt/framework#6635.

Logs

No response

@danielroe
Copy link
Member

I believe this is vite and @vitejs/plugin-vue behaviour. It's a bit complicated, but you can do this with import.meta.glob:

https://stackblitz.com/edit/github-pq8nym-aoavsh

<template>
  <img :src="images[dynamic_image_name]" alt="Discover Nuxt 3" />
</template>
<script setup lang="ts">
import { filename } from 'pathe/utils';

const glob = import.meta.glob('~/assets/*.svg', { eager: true });
const images = Object.fromEntries(
  Object.entries(glob).map(([key, value]) => [filename(key), value.default])
);

const dynamic_image_name = 'zero-config';
</script>

@tobiasdiez
Copy link
Contributor Author

Thanks @danielroe, this works like a charm! Do you think it makes sense to add the following helper method as a built-in composable?

function useAsset(path: string): string {
  const assets = import.meta.glob('~/assets/**/*', {
    eager: true,
    import: 'default',
  })
  // @ts-expect-error: wrong type info
  return assets['/assets/' + path]
}

//Usage: <img :src="useAsset(dynamic_image_name + '.svg')" alt="Discover Nuxt 3" />

@danielroe
Copy link
Member

  1. I would highly recommend use with file extension, as the glob with eager will end up including content of all imports within your build. Probably safe with images (includes just files) but you wouldn't want to accidentally include anything else. (CSS files, would get included not as filenames but as actual CSS.)

  2. As for a composable, it's an interesting idea. But I'm very cautious. I wonder if it would tree-shake out properly if not used.

@mattyleggy
Copy link

Is this the only way to get dynamic images in Nuxt 3? Was super easy in Nuxt 2 with require.

@jamiecarter7
Copy link

+1 - I use the require() method to build 500 static pages using about 600 images which are inserted dynamically, a solution to this will be very much appreciated

@wangrongding
Copy link

Wait for the official to provide a better solution.😅😅

@3aluw
Copy link

3aluw commented Nov 11, 2022

I believe this is vite and @vitejs/plugin-vue behaviour. It's a bit complicated, but you can do this with import.meta.glob:

https://stackblitz.com/edit/github-pq8nym-aoavsh

<template>
  <img :src="images[dynamic_image_name]" alt="Discover Nuxt 3" />
</template>
<script setup lang="ts">
import { filename } from 'pathe/utils';

const glob = import.meta.glob('~/assets/*.svg', { eager: true });
const images = Object.fromEntries(
  Object.entries(glob).map(([key, value]) => [filename(key), value.default])
);

const dynamic_image_name = 'zero-config';
</script>

Can someone explain this solution a little ?
What is the point of this import ? import { filename } from 'pathe/utils';

@manniL manniL added the dx label Dec 4, 2022
@manniL
Copy link
Member

manniL commented Dec 4, 2022

Is this the only way to get dynamic images in Nuxt 3? Was super easy in Nuxt 2 with require.

I think having some way to improve DX and fetch dynamic images easier would be helpful indeed ☺️

@manniL
Copy link
Member

manniL commented Dec 5, 2022

I believe this is vite and @vitejs/plugin-vue behaviour. It's a bit complicated, but you can do this with import.meta.glob:
stackblitz.com/edit/github-pq8nym-aoavsh

<template>
  <img :src="images[dynamic_image_name]" alt="Discover Nuxt 3" />
</template>
<script setup lang="ts">
import { filename } from 'pathe/utils';

const glob = import.meta.glob('~/assets/*.svg', { eager: true });
const images = Object.fromEntries(
  Object.entries(glob).map(([key, value]) => [filename(key), value.default])
);

const dynamic_image_name = 'zero-config';
</script>

Can someone explain this solution a little ? What is the point of this import ? import { filename } from 'pathe/utils';

The idea is to remove the filenames from the key so you don't have to write /assets/... there

@manniL
Copy link
Member

manniL commented Dec 5, 2022

Also FYI, there is another caveat - when a buildAssetDir is set that is called /assets/ , the approach above will break.

@Seanitzel
Copy link

Another way to do it is just use regular imports:

<template>
  <img :src="img.default" alt="Discover Nuxt 3" />
</template>
<script setup lang="ts">
const img = await import(`~/assets/${fileName}.svg`);
</script>

@mbadr2200
Copy link

mbadr2200 commented Dec 16, 2022

i use this technique and it works with me just fine

new URL(`../assets/images/${props.image}`,import.meta.url).href

@antoinezanardi
Copy link
Contributor

antoinezanardi commented Dec 22, 2022

I created a temporary composable function while waiting for an official solution.

It uses the above solution with good types as much as possible (I didn't succeed to have the correct Module returned by import.meta.glob function).

import { filename } from "pathe/utils";
import { computed } from "#build/imports";

interface ImagesComposable {
  getImageSrc: (fileName: string) => string | undefined;
}

function useImages(): ImagesComposable {
  // TODO : replace the first parameter of the glob function according to your needs, I needed to import only png and jpeg from the images directory.
  const images = computed(() => import.meta.glob("~/assets/images/*.(png|jpeg)", { eager: true }));

  const getImageSrc = (fileName: string): string | undefined => {
    for (const path in images.value) {
      if (Object.hasOwn(images.value, path)) {
        // unknown type is required here to change the final type as typescript thinks that images.value[path] is a function, it is not.
        const image: unknown = images.value[path];
        const imagePath = (image as { default: string }).default;
        if (filename(imagePath) === filename(fileName)) {
          return imagePath;
        }
      }
    }
    return undefined;
  };
  return { getImageSrc };
}

export { useImages };

// USAGE => const src = getImageSrc("test.png")

@luksak
Copy link

luksak commented Dec 23, 2022

Is this the only way to get dynamic images in Nuxt 3? Was super easy in Nuxt 2 with require.

I am also looking for something similar to require. In Nuxt 2 I used this to get the dimensions of an image, which was super useful:

export default {
  computed: {
    imageDimensions() {
      const image = require(`~/static/img/${this.src}`);
      return {
        width: image.width,
        height: image.height,
      }
    },
  },
}

@sami-baadarani
Copy link

I managed to solve this by returning a new URL using a computed function.

Store the path for your image in imagePath ref.

<script setup>
  const imgUrl = computed(() => {
    return new URL(imagePath.value, import.meta.url).href;
  });
</script>

<template>
  <img :src="imgUrl" />
<template/>

For more details check https://vitejs.dev/guide/assets.html#new-url-url-import-meta-url

@tuxsisir
Copy link
Contributor

I managed to solve this by returning a new URL using a computed function.

Store the path for your image in imagePath ref.

<script setup>
  const imgUrl = computed(() => {
    return new URL(imagePath.value, import.meta.url).href;
  });
</script>

<template>
  <img :src="imgUrl" />
<template/>

For more details check https://vitejs.dev/guide/assets.html#new-url-url-import-meta-url

@sami-baadarani Thanks for providing the vite link for more details. Something to keep in mind with this approach: from the docs it states that:

Does not work with SSR

This pattern does not work if you are using Vite for Server-Side Rendering, because import.meta.url have different semantics in browsers vs. Node.js. The server bundle also cannot determine the client host URL ahead of time.

@JBildstein
Copy link

The solution from @antoinezanardi worked well for me (thank you!) but only in dev.
With generate the path gets a hash appended to the filename so the files aren't found anymore.

So to make it work in both dev and prod I had to change the condition from

if (filename(imagePath) === filename(fileName))

to

const regex = new RegExp('^' + filename(fileName) + '(?:\\.[a-zA-Z0-9]+)?$');
if (regex.test(filename(imagePath)))

You have to be a bit careful if your filenames contain a period though, there is a chance it'll get mistakenly matched.
And if for some reason the hash gets appended differently this'll break as well, so it's really just a workaround.

@shamscorner
Copy link

shamscorner commented Jan 17, 2023

This works for me.

// ImageView.vue

<script setup lang="ts">
const props = defineProps<{
  src: string;
  alt: string;
}>();

function getImageUrl(path: string) {
  const pathArr = path.split('.');
  if (pathArr.length < 2) return undefined;

  const url = `../assets/images${pathArr[0]}.${pathArr[1]}`;  // change the path

  return new URL(url, import.meta.url).href;
}
</script>

<template>
  <img :src="getImageUrl(props.src)" :alt="props.alt" />
</template>

Use like below:

<ImageView
   :src="`/carriers/${carrier}.svg`"  // provide the path excluding initial
   :alt="carrier"
   class="h-6 w-6 flex-shrink-0 rounded-full"
/>

@danielroe danielroe added the 3.x label Jan 19, 2023
@danielroe danielroe transferred this issue from nuxt/framework Jan 19, 2023
@miumerfarooq
Copy link

For dynamic image use this

<img :src="`/_nuxt/assets/images/${img.src}`" alt="image"/>

Note: define your directory properly, my image are saved in images folder.

It work for me :)

@manniL
Copy link
Member

manniL commented Jan 23, 2023

@Umer-Farooq10 this will break in prod

@saslavik
Copy link

saslavik commented Jan 26, 2023

I can't explain why, but this fn not working

const getImg = (img) => {
return new URL('./../assets/images/useful/' + img + '.png', import.meta.url).href
}

and this one is working for me O_o :

const getImg = (img) => {
const link = './../assets/images/useful/' + img + '.png'
return new URL(link, import.meta.url).href
}

@IJsLauw
Copy link

IJsLauw commented Jul 25, 2023

I'd like to try out your code, but got stuck somehow.

import.meta.glob('#nuxt-layer-base/assets/images/**/*.(png|jpeg|svg)', { eager: true }

This part has me stumped. When I try this all I get is an error like this:

[plugin:vite:import-glob] Invalid glob: "#nuxt-layer-base/assets/images/**/*.(png|jpeg|svg)" (resolved: "#nuxt-layer-base/assets/images/**/*.(png|jpeg|svg)"). It must start with '/' or './' .... /composables/assets.ts

Any thoughts on that @szulcus ?

@szulcus
Copy link

szulcus commented Jul 25, 2023

@IJsLauw #nuxt-layer-base is an alias for my nuxt layer. If you don't need to use layer, you can simply use ~ (alias for project root).

@lionel-addvanto
Copy link

is there any update on this? Anything official from the Nuxt team maybe? @danielroe?

@matias-gmg
Copy link

Also interested in how to address this.

@3aluw
Copy link

3aluw commented Aug 9, 2023

Is it fair to say that using Public folder is the best solution right now ?

@matias-gmg
Copy link

matias-gmg commented Aug 10, 2023

I solved it by using the following code:

<template>
  <span class="svg-icon-ctr" :class="[name, cls]" v-html="icon"></span>
</template>

<script setup>
const props = defineProps({
  name: { type: String },
  cls: { type: String, default: "" },
});

// Auto-load icons
const icons = Object.fromEntries(
  Object.entries(import.meta.glob("~/assets/svg/*.svg", { as: "raw" })).map(
    ([key, value]) => {
      const filename = key.split("/").pop().split(".").shift();
      return [filename, value];
    }
  )
);

// Lazily load the icon
const icon = props.name && (await icons?.[props.name]?.());
</script>

For reference, in Nuxt 2 this component worked like this:

<template>
  <span class="svg-icon-ctr" :class="[name, cls]" v-html="require(`~/assets/svg/${name}.svg?raw`)"></span>
</template>

<script>
  export default {
    props: {
      name: { type: String },
      cls: { type: String, default: "" },
    }
  };
</script>

@3aluw
Copy link

3aluw commented Aug 10, 2023

I solved it by using the following code:

<script setup>
// Auto-load icons
const icons = Object.fromEntries(
  Object.entries(import.meta.glob("~/assets/svg/*.svg", { as: "raw" })).map(
..........
  )
);

// Lazily load the icon
const icon = props.name && (await icons?.[props.name]?.());
</script>

Sounds interesting.
From a performance perspective,
is it more advantageous to import all the files and load them lazily, or simply use the public folder?

@matias-gmg
Copy link

Sounds interesting. From a performance perspective, is it more advantageous to import all the files and load them lazily, or simply use the public folder?

I am not sure to be honest... Maybe someone from the team can clarify this?

@danielroe
Copy link
Member

If your files don't change in content, it is more performant (and a better decision, I think) to add them to the public folder.

@bitbytebit1
Copy link

Finding it really frustrating to work with images, this needs to be properly addressed in the documentation: making it very clear that it is not possible to using images with dynamic names unless import.meta.glob is used.

@netopolit
Copy link

netopolit commented Sep 12, 2023

Finding it really frustrating to work with images, this needs to be properly addressed in the documentation: making it very clear that it is not possible to using images with dynamic names unless import.meta.glob is used.

This is partially documented in the Vite guide, but I agree it should also be clarified in Nuxt docs.

I also believe this is a Vite limitation, and the issue should be raised in vitejs/vite repository. Please correct me if I'm wrong.

@guidocaru

This comment was marked as off-topic.

@TimGuendel

This comment was marked as off-topic.

@manniL
Copy link
Member

manniL commented Oct 11, 2023

I've written a blog post about that topic recently, but the easiest way would be using the public folder unless you have a good reason not to. In this case, using the import.meta.glob solution from Daniel would be the best way. But loading assets that way is less performant than via public folder.

@shrpne
Copy link

shrpne commented Oct 12, 2023

It is very sad, that the best solution for using dynamic assets with Vite is not to use dynamic assets. After years with Webpack, optimizing images by the bundler seems like a basic task, but not with Vite

@shrpne
Copy link

shrpne commented Oct 12, 2023

@netopolit

I also believe this is a Vite limitation, and the issue should be raised in vitejs/vite repository. Please correct me if I'm wrong.

Yes, but the Vite team closes such issues since this limitation is "by design". You should have significant weight in the community to be heard by Vite team

@maltseva-k
Copy link

maltseva-k commented Oct 16, 2023

Hi!
You can use src without '~/assets' if your remove image from assets to public directory
<img :src="/${dynamic_image_name}" alt="Discover Nuxt 3" />
It works for me :)
try it too
image

@BorisKamp
Copy link

BorisKamp commented Dec 7, 2023

I have a set of country flag icons in my /public/country-flags folder. When I set the SRC like this:

<img v-if="agent.attributes?.country_iso" :src="`/country-flags/${agent.attributes.country_iso}.svg`" class="h-6 w-6 me-2">

It does work when running local, however, when running on prod, the images return a 404, here's the rendered element on prod:
<img src="/country-flags/NL.svg" class="h-6 w-6 me-2">
It shows this error in the console: GET https://my.todayslogistics.nl/country-flags/NL.svg 404 (Not Found)

For images in the same /public folder that I set the SRC non-dynamic, the image loads, heres the rendered element on prod:
<img class="h-8 w-auto" src="/logo-white.svg" alt="Todays logo">

I have no clue how to fix this, I can't be the only one with this problem.

@joelee1992
Copy link

I assume there is no solution to this issue, even after attempting to use a new new URL(link, import.meta.url).href which only works locally but not in the production environment

@manniL
Copy link
Member

manniL commented Dec 14, 2023

@joelee1992 There is, as Daniel pointed out in #14766 (comment) 😊

@BorisKamp
Copy link

@joelee1992 There is, as Daniel pointed out in #14766 (comment) 😊

So the workaround is to move the files to the assets folder and use Daniels solution?

@manniL
Copy link
Member

manniL commented Dec 14, 2023

So the workaround is to move the files to the assets folder and use Daniels solution?

@BorisKamp Well, you have two options (also explained here):

  • Use the public folder (recommended and easier)
  • Or use the assets folder + use the solution daniel provided if necessary

@BorisKamp
Copy link

So the workaround is to move the files to the assets folder and use Daniels solution?

@BorisKamp Well, you have two options (also explained here):

  • Use the public folder (recommended and easier)
  • Or use the assets folder + use the solution daniel provided if necessary

Thank you for your reply but that's the whole point: I am using the public folder, see my comment up here

@manniL
Copy link
Member

manniL commented Dec 14, 2023

@BorisKamp Sorry, didn't see it was linked to the comment above. Do you have a minimal reproduction? Public folder should work in any case, no matter if referenced in the app or not 🤔

@BorisKamp
Copy link

@BorisKamp Sorry, didn't see it was linked to the comment above. Do you have a minimal reproduction? Public folder should work in any case, no matter if referenced in the app or not 🤔

@manniL no problem man! Hmm do you know if I can reproduce a production environment locally? As my issue describes, this is only happening once deployed, with dynamic filename generation. I can verify that with static filename specification this does not happen.

@megaarmos
Copy link

megaarmos commented Dec 29, 2023

I think i just found an easy solution for this problem. I used <NuxtImg> for dynamic images. It worked both during development and in my production build

Note

<NuxtImg> by default looks up files in public directory, in my case directory looked like this public > images

<NuxtImg :src="'images/' + yourImage" />

@CreativeWarlock
Copy link

CreativeWarlock commented Feb 23, 2024

This one works like a charm (from https://www.lichter.io/articles/nuxt3-vue3-dynamic-images/ ):

<script setup lang="ts">
import { filename } from 'pathe/utils'
const dogNames = ['Riley', 'Annie', 'Marvin'];
const selectedDog = ref('');

const glob = import.meta.glob('@/assets/doggos/*.jpg', { eager: true })
const images = Object.fromEntries(
  Object.entries(glob).map(([key, value]) => [filename(key), value.default])
)
</script>

<template>
  <div>
    <label v-for="doggo in dogNames" :key="doggo" style="margin-right: 2rem">
      <input type="radio" :value="doggo" v-model="selectedDog" />
      {{ doggo }}
    </label>
    <img
      :src="images[`${selectedDog.toLowerCase()}`]"
      width="500"
      :alt="selectedDog"
    />
  </div>
</template>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests