How to support external assets properly ?
Hello,
I do have my application running properly using the ~/assets url for my embedded content and the rest in the public folder. Which is fine for my "static assets".
However for my product images and such, they are uploaded in the admin by various clients. Those images are stored on S3.
Previously in angular/aurelia/react and such frameworks i was not doing SSR and used a reverse-proxy nginx config to redirect anything starting with "/cdn" to "http://myapp.assets.s3-eu-west-1.amazonaws.com/"
Which worked fine.
However i'm struggling to replicate this with NUXT. I did skip the nginx config as this time i use node_server it's not just static website anymore. I run everything using a docker-compose file and traefik.
I guess I could instead map my URL to a nginx instance first which would reverse-proxy each URL to either the CDN or NUXT instance but it requires to add another layer and I would rather avoid unless required. Is there any embedded support for reverse-proxy ?
What i would like is that when any URL has the "/cdn/" prefix it should be redirected to "http://myapp.assets.s3-eu-west-1.amazonaws.com/" instead. ipx should not process it either etc...
I guess I could also using something like a pipe/value-converter equivalent in vue (not yet familiar with that) but it would require tu put it everywhere external assets are required. I would like it to be as transparent as possible.
Thank a lot for the help
14 Replies
Hi @Slade. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:
----------------------
To support external assets (like product images uploaded to S3) in Nuxt, and transparently proxy requests such as
The most Nuxt-native way is to use Nitro server middleware to proxy
/cdn/*
to your S3 bucket (e.g., <http://myapp.assets.s3-eu-west-1.amazonaws.com/*
>), you have a few options:
1. Use Nitro Server Middleware
Nuxt 3 (with Nitro) allows you to add custom server middleware. You can create a middleware that intercepts requests to /cdn/*
and proxies them to your S3 bucket. Here’s a basic example of what such middleware could look like:
Place this file in your server/middleware/
directory. This will make Nuxt handle /cdn/*
requests by fetching the asset from S3 and returning it directly, without involving IPX or other image processing.
Note: The knowledge sources do not provide a full code example for this exact use case, but Nuxt's Nitro server engine supports custom middleware for handling such scenarios, as seen in the Nuxt documentation for server middleware and API routes.2. Route Rules (for Headers, Not Proxy) Nuxt's route rules allow you to set headers, redirects, and other behaviors for specific paths, but they do not provide built-in reverse proxy functionality. They are more suited for SSR toggling, CORS, cache headers, etc. 3. External Asset Handling in Nuxt Image If you use the Nuxt Image module, by default it expects local images in the
public/
directory and can optimize remote images if you whitelist their domains. However, it does not provide a reverse proxy for arbitrary paths like /cdn/*
—it will either fetch directly from the remote domain or serve from local/public. You can use the domains
option to allow direct linking to S3, but this does not give you a /cdn/*
local path mapping, nor does it bypass IPX processing unless you avoid using <NuxtImg>
for those URLs Nuxt Image Providers.
4. Nginx/External Proxy
As you mentioned, using an external reverse proxy (like Nginx) is a classic solution, but you prefer to avoid adding another layer.
---
Summary:The most Nuxt-native way is to use Nitro server middleware to proxy
/cdn/*
requests to your S3 bucket. This keeps the logic within your Nuxt app, avoids extra infrastructure, and is transparent to your application code. There is no built-in Nuxt config option to declare a reverse proxy for arbitrary paths, but server middleware is designed for exactly this kind of use case.
If you need a code example or further details on implementing the middleware, let me know!
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------<@341273466806337539> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@kapa.ai I did try with the suggested middleware and an image tag like this:
<img
src="/cdn/images/book_2k.jpg"
alt="Book"
class="w-full h-100 object-cover mb-8"
/>
but i have this error: Internal server error: Failed to resolve import "/cdn/images/book_2k.jpg" from "app/pages/index.vue". Does the file exist?
It does look like the middleware is only triggered on page load but not for every single request.
Let me check the knowledge base for you again. One moment! :books:
----------------------
Based on the knowledge sources, Nuxt's server middleware (in the
Make sure you are not using
Ensure your middleware is in the
Your middleware should check for the
server/middleware/
directory) is designed to run for every incoming HTTP request, not just on page load. This means it should be able to intercept requests like /cdn/images/book_2k.jpg
and proxy them as intended, provided the middleware is set up correctly and the request is actually reaching the server.
However, the error message you are seeing:
Internal server error: Failed to resolve import "/cdn/images/book_2k.jpg" from "app/pages/index.vue". Does the file exist?suggests that Nuxt is trying to resolve
/cdn/images/book_2k.jpg
as a module import during the build process, rather than treating it as a runtime HTTP request. This typically happens if you use a dynamic import or require statement, or if the image path is referenced in a way that triggers Vite/Webpack to try to bundle it.
Key points from the documentation:
- Server middleware in server/middleware/
is executed for every request after Nitro server initialization, and can be used for tasks like authentication, logging, or request transformation. Returning a value from middleware will terminate the request and send the returned value as the response, which is the desired behavior for a proxy[^1].
- Static assets in the public/
directory are served directly, but assets in assets/
are for build-time processing (e.g., CSS, images for bundling) and are not directly accessible at runtime[^2].
- If you want to serve external images via a proxy, you should ensure that the image tag's src
attribute is a plain string (not imported or required), so that the browser makes a runtime HTTP request to /cdn/images/book_2k.jpg
, which the server middleware can intercept[^1].
What to check:
1. How you reference the image:Make sure you are not using
import
or require
for the image path. Use a plain string in the src
attribute:
This will make the browser request /cdn/images/book_2k.jpg
at runtime.
2. Middleware placement:Ensure your middleware is in the
server/middleware/
directory, not in the middleware/
directory (which is for route middleware and only runs within the Vue app, not for static asset requests)[^1].
3. Middleware logic:Your middleware should check for the
/cdn/
prefix and proxy the request to S3, as described previously.
If you are still seeing the error, double-check that the image path is not being processed by Vite/Webpack as a module import. If you use a static import like import img from '/cdn/images/book_2k.jpg'
, it will not work for external or proxied assets.
[^1]: Nuxt Lifecycle: Server Middleware
[^2]: Nuxt Directory Structure: Middleware
If you follow these steps and use a plain string for the image src
, the request should reach your server middleware and be proxied as intended. If the problem persists, please provide the relevant code snippets for further troubleshooting.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------<@341273466806337539> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@kapa.ai It looks like vite is trying to bundling the file, what is the proper way to exclude the /cdn/ folder from the bundling process ?
ERROR Internal server error: Failed to resolve import "/cdn/images/book_2k.jpg" from "app/pages/index.vue". Does the file exist? 20:33:47
Plugin: vite:import-analysis File: E:/greyfox/webapps/find-your-place/findyourplace.client/app/pages/index.vue:79:37 76 | }); 77 | import { createCommentVNode as _createCommentVNode, toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, createTextVNode as _createTextVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock, createElementBlock as _createElementBlock } from "vue"; 78 | import _imports_0 from "/cdn/images/book_2k.jpg"; | ^ 79 | const _hoisted_1 = { class: "text-center text-3xl mb-12" }; 80 | const _hoisted_2 = { class: "flex justify-center" }; at TransformPluginContext._formatLog (file:///E:/greyfox/webapps/find-your-place/findyourplace.client/nodemodules/.pnpm/vite@6.3.5@types+node@22.1_18538aeef47463af40227f6e1ff9092b/node_modules/vite/dist/node/chunks/dep-DBxKXgDP.js:42499:41) at TransformPluginContext.error (file:///E:/greyfox/webapps/find-your-place/findyourplace.client/nodemodules/.pnpm/vite@6.3.5@types+node@22.1_18538aeef47463af40227f6e1ff9092b/node_modules/vite/dist/node/chunks/dep-DBxKXgDP.js:42496:16) at normalizeUrl (file:///E:/greyfox/webapps/find-your-place/findyourplace.client/nodemodules/.pnpm/vite@6.3.5@types+node@22.1_18538aeef47463af40227f6e1ff9092b/node_modules/vite/dist/node/chunks/dep-DBxKXgDP.js:40475:23) at process.processTicksAndRejections (node:internal/process/task_queues:95:5) at async file:///E:/greyfox/webapps/find-your-place/findyourplace.client/nodemodules/.pnpm/vite@6.3.5@types+node@22.1_18538aeef47463af40227f6e1ff9092b/node_modules/vite/dist/node/chunks/dep-DBxKXgDP.js:40594:37 at async Promise.all (index 8) at async TransformPluginContext.transform (file:///E:/greyfox/webapps/find-your-place/findyourplace.client/nodemodules/.pnpm/vite@6.3.5@types+node@22.1_18538aeef47463af40227f6e1ff9092b/node_modules/vite/dist/node/chunks/dep-DBxKXgDP.js:40521:7) at async file:///E:/greyfox/webapps/find-your-place/findyourplace.client/node_modules/.pnpm/vite-plugin-inspect@11.0.0__1ce6c499c09036b29809374d476ded81/node_modules/vite-plugin-inspect/dist/shared/vite-plugin-inspect.Bht9Q8w0.mjs:374:17 at async EnvironmentPluginContainer.transform (file:///E:/greyfox/webapps/find-your-place/findyourplace.client/nodemodules/.pnpm/vite@6.3.5@types+node@22.1_18538aeef47463af40227f6e1ff9092b/node_modules/vite/dist/node/chunks/dep-DBxKXgDP.js:42294:18) at async loadAndTransform (file:///E:/greyfox/webapps/find-your-place/findyourplace.client/nodemodules/.pnpm/vite@6.3.5@types+node@22.1_18538aeef47463af40227f6e1ff9092b/node_modules/vite/dist/node/chunks/dep-DBxKXgDP.js:35735:27)
Plugin: vite:import-analysis File: E:/greyfox/webapps/find-your-place/findyourplace.client/app/pages/index.vue:79:37 76 | }); 77 | import { createCommentVNode as _createCommentVNode, toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, createTextVNode as _createTextVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock, createElementBlock as _createElementBlock } from "vue"; 78 | import _imports_0 from "/cdn/images/book_2k.jpg"; | ^ 79 | const _hoisted_1 = { class: "text-center text-3xl mb-12" }; 80 | const _hoisted_2 = { class: "flex justify-center" }; at TransformPluginContext._formatLog (file:///E:/greyfox/webapps/find-your-place/findyourplace.client/nodemodules/.pnpm/vite@6.3.5@types+node@22.1_18538aeef47463af40227f6e1ff9092b/node_modules/vite/dist/node/chunks/dep-DBxKXgDP.js:42499:41) at TransformPluginContext.error (file:///E:/greyfox/webapps/find-your-place/findyourplace.client/nodemodules/.pnpm/vite@6.3.5@types+node@22.1_18538aeef47463af40227f6e1ff9092b/node_modules/vite/dist/node/chunks/dep-DBxKXgDP.js:42496:16) at normalizeUrl (file:///E:/greyfox/webapps/find-your-place/findyourplace.client/nodemodules/.pnpm/vite@6.3.5@types+node@22.1_18538aeef47463af40227f6e1ff9092b/node_modules/vite/dist/node/chunks/dep-DBxKXgDP.js:40475:23) at process.processTicksAndRejections (node:internal/process/task_queues:95:5) at async file:///E:/greyfox/webapps/find-your-place/findyourplace.client/nodemodules/.pnpm/vite@6.3.5@types+node@22.1_18538aeef47463af40227f6e1ff9092b/node_modules/vite/dist/node/chunks/dep-DBxKXgDP.js:40594:37 at async Promise.all (index 8) at async TransformPluginContext.transform (file:///E:/greyfox/webapps/find-your-place/findyourplace.client/nodemodules/.pnpm/vite@6.3.5@types+node@22.1_18538aeef47463af40227f6e1ff9092b/node_modules/vite/dist/node/chunks/dep-DBxKXgDP.js:40521:7) at async file:///E:/greyfox/webapps/find-your-place/findyourplace.client/node_modules/.pnpm/vite-plugin-inspect@11.0.0__1ce6c499c09036b29809374d476ded81/node_modules/vite-plugin-inspect/dist/shared/vite-plugin-inspect.Bht9Q8w0.mjs:374:17 at async EnvironmentPluginContainer.transform (file:///E:/greyfox/webapps/find-your-place/findyourplace.client/nodemodules/.pnpm/vite@6.3.5@types+node@22.1_18538aeef47463af40227f6e1ff9092b/node_modules/vite/dist/node/chunks/dep-DBxKXgDP.js:42294:18) at async loadAndTransform (file:///E:/greyfox/webapps/find-your-place/findyourplace.client/nodemodules/.pnpm/vite@6.3.5@types+node@22.1_18538aeef47463af40227f6e1ff9092b/node_modules/vite/dist/node/chunks/dep-DBxKXgDP.js:35735:27)
Let me check the knowledge base for you again. One moment! :books:
----------------------
The error occurs because Vite (and thus Nuxt) tries to bundle any asset referenced with a non-absolute path or with an import statement. When you use something like
import img from '/cdn/images/book_2k.jpg'
or reference /cdn/images/book_2k.jpg
in a way that Vite interprets as a module import, it expects the file to exist locally and tries to process it as a build asset, which fails for external or proxied resources.
Proper way to exclude /cdn/
from bundling:
- Do not use import or require for external/proxied assets.
- Use a plain string in your template for the src
attribute:
This way, the browser will make a runtime HTTP request to /cdn/images/book_2k.jpg
, which your Nuxt server middleware can intercept and proxy to S3. Vite will not attempt to bundle or resolve this path at build time.
Key points from the knowledge sources:
- Nuxt will only process and bundle assets in the assets/
directory if referenced with ~/assets/...
or imported. Files in public/
are served as static files at runtime, and any other path (like /cdn/
) is not handled by the build tool unless you import it, which you should avoid for external assets Nuxt Assets Documentation.
- There is no explicit Vite or Nuxt config option to "exclude" a path like /cdn/
from bundling, because the correct approach is to avoid importing or referencing it in a way that triggers the build tool's asset pipeline.
If you are still seeing the error, double-check your code to ensure you are not importing the image or referencing it with a non-string expression that Vite might try to resolve.
I'm sorry, but the knowledge sources do not mention a configuration option in Nuxt or Vite to explicitly exclude a custom path like /cdn/
from the bundling process. The recommended approach is to use plain string URLs in your templates for such external or proxied assets Nuxt Assets Documentation.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------<@341273466806337539> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@kapa.ai i do use plain text import like this <img src="/cdn/images/book_2k.jpg" alt="Book" />, i do not use any import statement for this.
Let me check the knowledge base for you again. One moment! :books:
----------------------
Thank you for clarifying that you are using a plain string in your
If you are using a plain string in your template and still encountering this error, the knowledge sources do not provide enough information to explain why Vite is trying to bundle the file. This is unexpected based on Nuxt's documented behavior. You may want to double-check for any other code or configuration that could be affecting asset resolution, or consider opening an issue with a minimal reproduction for further investigation. If you have made any changes to the default directory structure or are using advanced features, please review those settings as well. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
<img src="/cdn/images/book_2k.jpg" alt="Book" />
tag and not an import statement.
Based on the knowledge sources, referencing an image with a plain string path like /cdn/images/book_2k.jpg
in your template should result in the browser making a runtime HTTP request to that path. This request should then be handled by your Nuxt server middleware if you have set it up to proxy /cdn/*
requests to your S3 bucket. Vite and Nuxt should not attempt to bundle or resolve this path at build time, as long as it is not imported or required in your script section Nuxt Assets Documentation.
However, if you are still seeing an error like:
this suggests that, for some reason, Vite is still trying to process this path as a module import. The knowledge sources do not provide a direct explanation for this behavior when using a plain string in the template.
Possible causes based on the documentation:
- If you are using a custom srcDir
or have changed the default directory structure, ensure that your public/
and assets/
directories are correctly placed, as this can affect how static files are served and resolved Nuxt Assets Documentation.
- There is no mention in the knowledge sources of a Nuxt or Vite configuration option to explicitly exclude a custom path like /cdn/
from the bundling process. The expected behavior is that plain string paths in templates are not bundled.
Summary:If you are using a plain string in your template and still encountering this error, the knowledge sources do not provide enough information to explain why Vite is trying to bundle the file. This is unexpected based on Nuxt's documented behavior. You may want to double-check for any other code or configuration that could be affecting asset resolution, or consider opening an issue with a minimal reproduction for further investigation. If you have made any changes to the default directory structure or are using advanced features, please review those settings as well. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
<@341273466806337539> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@kapa.ai this does not work
<img
src="/cdn/images/book_2k.jpg"
alt="Book"
/>
this does work
<img
src="http://findyourplace.assets.s3-eu-west-1.amazonaws.com/images/book_2k.jpg"
alt="Book"
/>
Let me check the knowledge base for you again. One moment! :books:
----------------------
Based on the knowledge sources, using a plain string path like
- Using a plain string path like
<img src="/cdn/images/book_2k.jpg" alt="Book" />
should result in the browser making a runtime HTTP request to /cdn/images/book_2k.jpg
. This request is expected to be handled by your Nuxt server middleware, which can proxy it to your S3 bucket. There is no indication in the official Nuxt documentation that such a path should be bundled or resolved at build time if it is not imported or required in your script section Nuxt Assets Documentation.
However, you are experiencing a situation where this does not work, but using the full S3 URL does work. The knowledge sources do not provide a direct explanation for this discrepancy. They do not mention any Nuxt or Vite configuration that would cause a plain string path in an <img>
tag to be treated as a module import or to fail unless the file exists locally.
Summary:- Using a plain string path like
/cdn/images/book_2k.jpg
in an <img>
tag should work with server middleware proxying, according to Nuxt's documented behavior.
- If it does not work and results in a build-time error, this is unexpected and not explained in the knowledge sources.
- Using the full S3 URL works because it bypasses Nuxt and Vite entirely, letting the browser fetch the image directly from S3.
I'm sorry, but the knowledge sources do not contain enough information to explain why your /cdn/
proxy approach is not working as expected, given your setup. If you continue to encounter this issue, you may want to review your middleware setup or seek further help with a minimal reproduction in the Nuxt community.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------<@341273466806337539> Kapa.ai is still learning and improving, please let me know how I did by reacting below.