diff --git a/src/runtime/components/NuxtImg.vue b/src/runtime/components/NuxtImg.vue
index 53ecad0eb..e62dac1be 100644
--- a/src/runtime/components/NuxtImg.vue
+++ b/src/runtime/components/NuxtImg.vue
@@ -47,12 +47,40 @@ defineSlots<{ default(props: DefaultSlotProps): any }>()
const $img = useImage()
const { providerOptions, normalizedAttrs, imageModifiers } = useImageProps(props)
-const sizes = computed(() => $img.getSizes(props.src!, {
- ...providerOptions.value,
- sizes: props.sizes,
- densities: props.densities,
- modifiers: imageModifiers.value,
-}))
+/**
+ * Detect SVG sources that should bypass IPX raster processing.
+ * SVGs are vector images that don't benefit from resize, srcset density
+ * variants, or placeholder blur. Passing them through IPX causes crashes
+ * (svgo/css-tree bundler issues) and incorrect sizing.
+ *
+ * When the user explicitly requests format conversion (e.g.
+ * ``), we let the request
+ * through to IPX so the conversion can happen.
+ *
+ * @see https://github.com/nuxt/image/issues/2035
+ * @see https://github.com/nuxt/image/issues/1967
+ * @see https://github.com/nuxt/image/issues/2075
+ */
+const isSvg = computed(() => {
+ if (!(/^[^?#]+\.svg(?:$|[?#])/i.test(props.src || ''))) {
+ return false
+ }
+ // Allow explicit format conversion (e.g. format="png") to pass through to IPX
+ const requestedFormat = props.format?.toLowerCase()
+ return !requestedFormat || requestedFormat === 'svg'
+})
+
+const sizes = computed(() => {
+ if (isSvg.value) {
+ return { src: props.src!, sizes: undefined, srcset: '' }
+ }
+ return $img.getSizes(props.src!, {
+ ...providerOptions.value,
+ sizes: props.sizes,
+ densities: props.densities,
+ modifiers: imageModifiers.value,
+ })
+})
const placeholderLoaded = ref(false)
@@ -68,6 +96,12 @@ const imgAttrs = computed(() => ({
}))
const placeholder = computed(() => {
+ // SVGs are lightweight vectors — no placeholder blur needed, and
+ // sending them through $img() would hit IPX which crashes on SVGs.
+ if (isSvg.value) {
+ return false
+ }
+
if (placeholderLoaded.value) {
return false
}
@@ -95,11 +129,14 @@ const placeholder = computed(() => {
}, providerOptions.value)
})
-const mainSrc = computed(() =>
- props.sizes
+const mainSrc = computed(() => {
+ if (isSvg.value) {
+ return props.src!
+ }
+ return props.sizes
? sizes.value.src
- : $img(props.src!, imageModifiers.value, providerOptions.value),
-)
+ : $img(props.src!, imageModifiers.value, providerOptions.value)
+})
const src = computed(() => placeholder.value || mainSrc.value)
diff --git a/test/e2e/__snapshots__/ipx.json5 b/test/e2e/__snapshots__/ipx.json5
index 9ed913a9d..2db218b1e 100644
--- a/test/e2e/__snapshots__/ipx.json5
+++ b/test/e2e/__snapshots__/ipx.json5
@@ -4,15 +4,15 @@
"/_ipx/s_300x300/images/colors-layer.jpg",
"/_ipx/s_300x300/images/colors.jpg",
"/_ipx/s_300x300/images/everest.jpg",
- "/_ipx/s_300x300/images/tacos.svg",
"/_ipx/s_300x300/unsplash/photo-1606112219348-204d7d8b94ee",
+ "/images/tacos.svg",
],
"sources": [
"/_ipx/s_300x300/images/colors.jpg",
"/_ipx/s_300x300/images/colors-layer.jpg",
"/_ipx/s_300x300/images/colors-layer-config.jpg",
"/_ipx/s_300x300/images/everest.jpg",
- "/_ipx/s_300x300/images/tacos.svg",
+ "/images/tacos.svg",
"/_ipx/s_300x300/unsplash/photo-1606112219348-204d7d8b94ee",
],
}
\ No newline at end of file
diff --git a/test/e2e/generate.test.ts b/test/e2e/generate.test.ts
index 4bb300721..d803b673a 100644
--- a/test/e2e/generate.test.ts
+++ b/test/e2e/generate.test.ts
@@ -39,13 +39,11 @@ describe('ipx provider', () => {
"_ipx/s_300x300/images/colors-layer.jpg",
"_ipx/s_300x300/images/colors.jpg",
"_ipx/s_300x300/images/everest.jpg",
- "_ipx/s_300x300/images/tacos.svg",
"_ipx/s_300x300/unsplash/photo-1606112219348-204d7d8b94ee",
"_ipx/s_600x600/images/colors-layer-config.jpg",
"_ipx/s_600x600/images/colors-layer.jpg",
"_ipx/s_600x600/images/colors.jpg",
"_ipx/s_600x600/images/everest.jpg",
- "_ipx/s_600x600/images/tacos.svg",
"_ipx/s_600x600/unsplash/photo-1606112219348-204d7d8b94ee",
]
`)
diff --git a/test/nuxt/image.test.ts b/test/nuxt/image.test.ts
index 60446a74a..c0b4750b9 100644
--- a/test/nuxt/image.test.ts
+++ b/test/nuxt/image.test.ts
@@ -605,6 +605,61 @@ describe('Renders NuxtImg with the custom prop and default slot', () => {
})
})
+describe('SVG passthrough (#2035)', () => {
+ it('serves SVG as-is without IPX processing', () => {
+ const wrapper = mountImage({ src: '/logo.svg' })
+ const img = wrapper.find('img')
+ expect(img.element.getAttribute('src')).toBe('/logo.svg')
+ expect(img.element.getAttribute('srcset')).toBeFalsy()
+ expect(img.element.getAttribute('sizes')).toBeFalsy()
+ })
+
+ it('handles SVG with query string', () => {
+ const wrapper = mountImage({ src: '/logo.svg?v=123' })
+ expect(wrapper.find('img').element.getAttribute('src')).toBe('/logo.svg?v=123')
+ })
+
+ it('handles SVG with hash', () => {
+ const wrapper = mountImage({ src: '/logo.svg#icon' })
+ expect(wrapper.find('img').element.getAttribute('src')).toBe('/logo.svg#icon')
+ })
+
+ it('is case-insensitive for .SVG extension', () => {
+ const wrapper = mountImage({ src: '/logo.SVG' })
+ expect(wrapper.find('img').element.getAttribute('src')).toBe('/logo.SVG')
+ expect(wrapper.find('img').element.getAttribute('srcset')).toBeFalsy()
+ })
+
+ it('ignores sizes prop for SVGs', () => {
+ const wrapper = mountImage({ src: '/logo.svg', sizes: 'sm:100vw md:50vw' })
+ const img = wrapper.find('img')
+ expect(img.element.getAttribute('src')).toBe('/logo.svg')
+ expect(img.element.getAttribute('srcset')).toBeFalsy()
+ })
+
+ it('ignores width/height modifiers for SVGs', () => {
+ const wrapper = mountImage({ src: '/logo.svg', width: 200, height: 200 })
+ const img = wrapper.find('img')
+ expect(img.element.getAttribute('src')).toBe('/logo.svg')
+ expect(img.element.getAttribute('srcset')).toBeFalsy()
+ })
+
+ it('allows explicit format conversion (svg → png) through IPX', () => {
+ const wrapper = mountImage({ src: '/logo.svg', format: 'png', width: 200 })
+ const img = wrapper.find('img')
+ const src = img.element.getAttribute('src')
+ expect(src).toContain('/_ipx/')
+ expect(src).toContain('f_png')
+ })
+
+ it('still processes non-SVG images normally', () => {
+ const wrapper = mountImage({ src: '/image.png', width: 200, sizes: '200' })
+ const img = wrapper.find('img')
+ expect(img.element.getAttribute('src')).toContain('/_ipx/')
+ expect(img.element.getAttribute('srcset')).toBeTruthy()
+ })
+})
+
const mountImage = (props: ComponentMountingOptions['props']) => mount(NuxtImg, { props })
function setImageContext(options: Partial) {
diff --git a/test/unit/bundle.test.ts b/test/unit/bundle.test.ts
index eea4df32d..955804eee 100644
--- a/test/unit/bundle.test.ts
+++ b/test/unit/bundle.test.ts
@@ -21,7 +21,7 @@ describe.skipIf(process.env.ECOSYSTEM_CI || isWindows)('nuxt image bundle size',
image: { provider: 'ipx' },
})
- expect(roundToKilobytes(withImage.totalBytes - withoutImage.totalBytes)).toMatchInlineSnapshot(`"12.6k"`)
+ expect(roundToKilobytes(withImage.totalBytes - withoutImage.totalBytes)).toMatchInlineSnapshot(`"12.4k"`)
})
})