diff --git a/.changeset/little-carrots-add.md b/.changeset/little-carrots-add.md
new file mode 100644
index 000000000000..4336351582d1
--- /dev/null
+++ b/.changeset/little-carrots-add.md
@@ -0,0 +1,5 @@
+---
+'@astrojs/image': minor
+---
+
+Add support for SVG images
diff --git a/packages/integrations/image/README.md b/packages/integrations/image/README.md
index 1cb0d5d4c7e4..d1cc22f2c6fc 100644
--- a/packages/integrations/image/README.md
+++ b/packages/integrations/image/README.md
@@ -169,13 +169,16 @@ Set to an empty string (`alt=""`) if the image is not a key part of the content
-**Type:** `'avif' | 'jpeg' | 'png' | 'webp'`
+**Type:** `'avif' | 'jpeg' | 'jpg' | 'png' | 'svg' | 'webp'`
**Default:** `undefined`
The output format to be used in the optimized image. The original image format will be used if `format` is not provided.
This property is required for remote images when using the default image transformer Squoosh, this is because the original format cannot be inferred.
+
+> When using the `svg` format, the original image must be in SVG format already (raster images cannot be converted to vector images). The SVG image itself won't be transformed but the final `` element will get the optimization attributes.
+
#### quality
diff --git a/packages/integrations/image/client.d.ts b/packages/integrations/image/client.d.ts
index cafec4184c65..71842742ab4d 100644
--- a/packages/integrations/image/client.d.ts
+++ b/packages/integrations/image/client.d.ts
@@ -1,6 +1,16 @@
///
-type InputFormat = 'avif' | 'gif' | 'heic' | 'heif' | 'jpeg' | 'jpg' | 'png' | 'tiff' | 'webp';
+type InputFormat =
+ | 'avif'
+ | 'gif'
+ | 'heic'
+ | 'heif'
+ | 'jpeg'
+ | 'jpg'
+ | 'png'
+ | 'tiff'
+ | 'webp'
+ | 'svg';
interface ImageMetadata {
src: string;
@@ -46,3 +56,7 @@ declare module '*.webp' {
const metadata: ImageMetadata;
export default metadata;
}
+declare module '*.svg' {
+ const metadata: ImageMetadata;
+ export default metadata;
+}
diff --git a/packages/integrations/image/src/loaders/index.ts b/packages/integrations/image/src/loaders/index.ts
index 280fb37c28db..225e98cee9cd 100644
--- a/packages/integrations/image/src/loaders/index.ts
+++ b/packages/integrations/image/src/loaders/index.ts
@@ -10,10 +10,11 @@ export type InputFormat =
| 'png'
| 'tiff'
| 'webp'
- | 'gif';
+ | 'gif'
+ | 'svg';
export type OutputFormatSupportsAlpha = 'avif' | 'png' | 'webp';
-export type OutputFormat = OutputFormatSupportsAlpha | 'jpeg' | 'jpg';
+export type OutputFormat = OutputFormatSupportsAlpha | 'jpeg' | 'jpg' | 'svg';
export type ColorDefinition =
| NamedColor
@@ -49,7 +50,7 @@ export type CropPosition =
| 'attention';
export function isOutputFormat(value: string): value is OutputFormat {
- return ['avif', 'jpeg', 'jpg', 'png', 'webp'].includes(value);
+ return ['avif', 'jpeg', 'jpg', 'png', 'webp', 'svg'].includes(value);
}
export function isOutputFormatSupportsAlpha(value: string): value is OutputFormatSupportsAlpha {
diff --git a/packages/integrations/image/src/loaders/sharp.ts b/packages/integrations/image/src/loaders/sharp.ts
index 55ea28645b92..517224289602 100644
--- a/packages/integrations/image/src/loaders/sharp.ts
+++ b/packages/integrations/image/src/loaders/sharp.ts
@@ -5,6 +5,14 @@ import type { OutputFormat, TransformOptions } from './index.js';
class SharpService extends BaseSSRService {
async transform(inputBuffer: Buffer, transform: TransformOptions) {
+ if (transform.format === 'svg') {
+ // sharp can't output SVG so we return the input image
+ return {
+ data: inputBuffer,
+ format: transform.format,
+ };
+ }
+
const sharpImage = sharp(inputBuffer, { failOnError: false, pages: -1 });
// always call rotate to adjust for EXIF data orientation
diff --git a/packages/integrations/image/src/loaders/squoosh.ts b/packages/integrations/image/src/loaders/squoosh.ts
index 5d71cdb7fbce..a5be16adb019 100644
--- a/packages/integrations/image/src/loaders/squoosh.ts
+++ b/packages/integrations/image/src/loaders/squoosh.ts
@@ -82,6 +82,14 @@ class SquooshService extends BaseSSRService {
}
async transform(inputBuffer: Buffer, transform: TransformOptions) {
+ if (transform.format === 'svg') {
+ // squoosh can't output SVG so we return the input image
+ return {
+ data: inputBuffer,
+ format: transform.format,
+ };
+ }
+
const operations: Operation[] = [];
if (!isRemoteImage(transform.src)) {
diff --git a/packages/integrations/image/src/vite-plugin-astro-image.ts b/packages/integrations/image/src/vite-plugin-astro-image.ts
index 3eee310e8905..b721578a5992 100644
--- a/packages/integrations/image/src/vite-plugin-astro-image.ts
+++ b/packages/integrations/image/src/vite-plugin-astro-image.ts
@@ -1,5 +1,6 @@
import type { AstroConfig } from 'astro';
import MagicString from 'magic-string';
+import mime from 'mime';
import fs from 'node:fs/promises';
import { basename, extname } from 'node:path';
import { Readable } from 'node:stream';
@@ -18,7 +19,7 @@ export interface ImageMetadata {
export function createPlugin(config: AstroConfig, options: Required): Plugin {
const filter = (id: string) =>
- /^(?!\/_image?).*.(heic|heif|avif|jpeg|jpg|png|tiff|webp|gif)$/.test(id);
+ /^(?!\/_image?).*.(heic|heif|avif|jpeg|jpg|png|tiff|webp|gif|svg)$/.test(id);
const virtualModuleId = 'virtual:image-loader';
@@ -97,7 +98,7 @@ export function createPlugin(config: AstroConfig, options: Required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/integrations/image/test/fixtures/basic-image/src/pages/index.astro b/packages/integrations/image/test/fixtures/basic-image/src/pages/index.astro
index ba492576cf96..ed1d79db662d 100644
--- a/packages/integrations/image/test/fixtures/basic-image/src/pages/index.astro
+++ b/packages/integrations/image/test/fixtures/basic-image/src/pages/index.astro
@@ -1,5 +1,6 @@
---
import socialJpg from '../assets/social.jpg';
+import logoSvg from '../assets/logo.svg';
import introJpg from '../assets/blog/introducing astro.jpg';
import outsideSrc from '../../social.png';
import { Image } from '@astrojs/image/components';
@@ -21,6 +22,8 @@ const publicImage = new URL('./hero.jpg', Astro.url);
+
+
diff --git a/packages/integrations/image/test/image-ssg.test.js b/packages/integrations/image/test/image-ssg.test.js
index 5bc1c1e0d22c..12b3ffea9c92 100644
--- a/packages/integrations/image/test/image-ssg.test.js
+++ b/packages/integrations/image/test/image-ssg.test.js
@@ -52,6 +52,12 @@ describe('SSG images - dev', function () {
query: { f: 'png', w: '2024', h: '1012' },
contentType: 'image/png',
},
+ {
+ title: 'SVG image',
+ id: '#logo-svg',
+ url: toAstroImage('src/assets/logo.svg'),
+ query: { f: 'svg', w: '192', h: '256' },
+ },
{
title: 'Inline imports',
id: '#inline',
@@ -157,6 +163,12 @@ describe('SSG images with subpath - dev', function () {
query: { f: 'png', w: '2024', h: '1012' },
contentType: 'image/png',
},
+ {
+ title: 'SVG image',
+ id: '#logo-svg',
+ url: toAstroImage('src/assets/logo.svg'),
+ query: { f: 'svg', w: '192', h: '256' },
+ },
{
title: 'Inline imports',
id: '#inline',
@@ -263,6 +275,12 @@ describe('SSG images - build', function () {
regex: /^\/_astro\/social.\w{8}_\w{4,10}.png/,
size: { type: 'png', width: 2024, height: 1012 },
},
+ {
+ title: 'SVG image',
+ id: '#logo-svg',
+ regex: /^\/_astro\/logo.\w{8}_\w{4,10}.svg/,
+ size: { width: 192, height: 256, type: 'svg' },
+ },
{
title: 'Inline imports',
id: '#inline',
@@ -351,6 +369,12 @@ describe('SSG images with subpath - build', function () {
regex: /^\/docs\/_astro\/social.\w{8}_\w{4,10}.png/,
size: { type: 'png', width: 2024, height: 1012 },
},
+ {
+ title: 'SVG image',
+ id: '#logo-svg',
+ regex: /^\/docs\/_astro\/logo.\w{8}_\w{4,10}.svg/,
+ size: { width: 192, height: 256, type: 'svg' },
+ },
{
title: 'Inline imports',
id: '#inline',
diff --git a/packages/integrations/image/test/image-ssr-build.test.js b/packages/integrations/image/test/image-ssr-build.test.js
index 4b985c0ad9e9..f85373c27cac 100644
--- a/packages/integrations/image/test/image-ssr-build.test.js
+++ b/packages/integrations/image/test/image-ssr-build.test.js
@@ -28,6 +28,12 @@ describe('SSR images - build', async function () {
url: '/_image',
query: { f: 'webp', w: '768', h: '414', href: /^\/_astro\/introducing astro.\w{8}.jpg/ },
},
+ {
+ title: 'SVG image',
+ id: '#logo-svg',
+ url: '/_image',
+ query: { f: 'svg', w: '192', h: '256', href: /^\/_astro\/logo.\w{8}.svg/ },
+ },
{
title: 'Inline imports',
id: '#inline',
@@ -144,6 +150,12 @@ describe('SSR images with subpath - build', function () {
href: /^\/docs\/_astro\/introducing astro.\w{8}.jpg/,
},
},
+ {
+ title: 'SVG image',
+ id: '#logo-svg',
+ url: '/_image',
+ query: { f: 'svg', w: '192', h: '256', href: /^\/docs\/_astro\/logo.\w{8}.svg/ },
+ },
{
title: 'Inline imports',
id: '#inline',
diff --git a/packages/integrations/image/test/image-ssr-dev.test.js b/packages/integrations/image/test/image-ssr-dev.test.js
index fbaa6f965e17..186100b12da2 100644
--- a/packages/integrations/image/test/image-ssr-dev.test.js
+++ b/packages/integrations/image/test/image-ssr-dev.test.js
@@ -59,6 +59,13 @@ describe('SSR images - dev', function () {
query: { f: 'png', w: '2024', h: '1012' },
contentType: 'image/png',
},
+ {
+ title: 'SVG image',
+ id: '#logo-svg',
+ url: toAstroImage('src/assets/logo.svg'),
+ query: { f: 'svg', w: '192', h: '256' },
+ contentType: 'image/svg+xml',
+ },
{
title: 'Inline imports',
id: '#inline',
@@ -181,6 +188,13 @@ describe('SSR images with subpath - dev', function () {
query: { f: 'png', w: '2024', h: '1012' },
contentType: 'image/png',
},
+ {
+ title: 'SVG image',
+ id: '#logo-svg',
+ url: toAstroImage('src/assets/logo.svg'),
+ query: { f: 'svg', w: '192', h: '256' },
+ contentType: 'image/svg+xml',
+ },
{
title: 'Inline imports',
id: '#inline',