Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .changeset/twelve-times-slide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'astro': minor
---

Adds an experimental flag `svgOptimizer` that enables automatic optimization of your SVG components using the provided optimizer. This supersedes the `svgo` experimental flag, which is now removed.

When enabled, your imported SVG files used as components will be optimized for smaller file sizes and better performance while maintaining visual quality. This can significantly reduce the size of your SVG assets by removing unnecessary metadata, comments, and redundant code.

Astro ships with a [SVGO](https://svgo.dev/) based optimizer but any can be used.
Comment thread
florian-lefebvre marked this conversation as resolved.
Outdated

To enable this feature, add the experimental flag in your Astro config and remove `svgo` if it was enabled:


```diff
// astro.config.mjs
-import { defineConfig } from "astro/config";
+import { defineConfig, svgoOptimizer } from "astro/config";

export default defineConfig({
+ experimental: {
+ svgOptimizer: svgoOptimizer()
- svgo: true
+ }
});
```

For more information on enabling and using this feature in your project, see the [experimental SVG optimization docs](https://docs.astro.build/en/reference/experimental-flags/svg-optimization/).
7 changes: 7 additions & 0 deletions packages/astro/src/assets/svg/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as z from 'zod/v4';
import type { SvgOptimizer } from './types.js';

export const SvgOptimizerSchema = z.object({
name: z.string(),
optimize: z.custom<SvgOptimizer['optimize']>((v) => typeof v === 'function'),
});
10 changes: 10 additions & 0 deletions packages/astro/src/assets/svg/svgo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { SvgOptimizer } from './types.js';
import { optimize, type Config } from 'svgo';

/** SVG optimizer using pass a [SVGO](https://svgo.dev/). */
Comment thread
florian-lefebvre marked this conversation as resolved.
Outdated
export function svgoOptimizer(config?: Config): SvgOptimizer {
return {
name: 'svgo',
optimize: (contents) => optimize(contents, config).data,
};
}
4 changes: 4 additions & 0 deletions packages/astro/src/assets/svg/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface SvgOptimizer {
name: string;
optimize: (contents: string) => string | Promise<string>;
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,28 @@
import { optimize } from 'svgo';
import { ELEMENT_NODE, TEXT_NODE, parse, renderSync } from 'ultrahtml';
import { AstroError, AstroErrorData } from '../../core/errors/index.js';
import type { AstroConfig } from '../../types/public/config.js';
import type { SvgComponentProps } from '../runtime.js';
import { dropAttributes } from '../runtime.js';
import type { ImageMetadata } from '../types.js';
import type { SvgOptimizer } from './types.js';

function parseSvg({
async function parseSvg({
path,
contents,
svgoConfig,
svgOptimizer,
}: {
path: string;
contents: string;
svgoConfig: AstroConfig['experimental']['svgo'];
svgOptimizer: SvgOptimizer | undefined;
}) {
let processedContents = contents;
if (svgoConfig) {
if (svgOptimizer) {
try {
const config = typeof svgoConfig === 'boolean' ? undefined : svgoConfig;
const result = optimize(contents, config);
processedContents = result.data;
processedContents = await svgOptimizer.optimize(contents);
} catch (cause) {
throw new AstroError(
{
...AstroErrorData.CannotOptimizeSvg,
message: AstroErrorData.CannotOptimizeSvg.message(path),
message: AstroErrorData.CannotOptimizeSvg.message(path, svgOptimizer.name),
},
{ cause },
);
Expand Down Expand Up @@ -58,20 +55,20 @@ function parseSvg({
return { attributes, body, styles };
}

export function makeSvgComponent(
export async function makeSvgComponent(
meta: ImageMetadata,
contents: Buffer | string,
svgoConfig: AstroConfig['experimental']['svgo'],
): string {
svgOptimizer: SvgOptimizer | undefined,
): Promise<string> {
const file = typeof contents === 'string' ? contents : contents.toString('utf-8');
const {
attributes,
body: children,
styles,
} = parseSvg({
} = await parseSvg({
path: meta.fsPath,
contents: file,
svgoConfig,
svgOptimizer,
});
const props: SvgComponentProps = {
meta,
Expand All @@ -89,20 +86,20 @@ export default createSvgComponent(${JSON.stringify(props)})`;
* (attributes + inner HTML body) without generating any module code.
* @internal Used by the asset pipeline for content-collection SVG images.
*/
export function parseSvgComponentData(
export async function parseSvgComponentData(
meta: ImageMetadata,
contents: Buffer | string,
svgoConfig: AstroConfig['experimental']['svgo'],
): { attributes: Record<string, string>; children: string; styles: string[] } {
svgOptimizer: SvgOptimizer | undefined,
): Promise<{ attributes: Record<string, string>; children: string; styles: string[] }> {
const file = typeof contents === 'string' ? contents : contents.toString('utf-8');
const {
attributes,
body: children,
styles,
} = parseSvg({
} = await parseSvg({
path: meta.fsPath,
contents: file,
svgoConfig,
svgOptimizer,
});
return { attributes: dropAttributes(attributes), children, styles };
}
12 changes: 8 additions & 4 deletions packages/astro/src/assets/vite-plugin-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { hashTransform, propsToFilename } from './utils/hash.js';
import { emitImageMetadata } from './utils/node.js';
import { CONTENT_IMAGE_FLAG } from '../content/consts.js';
import { getProxyCode } from './utils/proxy.js';
import { makeSvgComponent, parseSvgComponentData } from './utils/svg.js';
import { makeSvgComponent, parseSvgComponentData } from './svg/utils.js';
import { createPlaceholderURL, stringifyPlaceholderURL } from './utils/url.js';

const assetRegex = new RegExp(`\\.(${VALID_INPUT_FORMATS.join('|')})`, 'i');
Expand Down Expand Up @@ -371,7 +371,11 @@ export default function assets({ fs, settings, sync, logger }: Options): vite.Pl
});
// We know that the contents are present, as we only emit this property for SVG files
return {
code: makeSvgComponent(imageMetadata, contents, settings.config.experimental.svgo),
code: await makeSvgComponent(
imageMetadata,
contents,
settings.config.experimental.svgOptimizer,
),
};
}
// In SSR builds, any image loaded by the SSR environment could be reachable at
Expand All @@ -389,10 +393,10 @@ export default function assets({ fs, settings, sync, logger }: Options): vite.Pl
const contents = await fs.promises.readFile(imageMetadata.fsPath, {
encoding: 'utf8',
});
const svgData = parseSvgComponentData(
const svgData = await parseSvgComponentData(
imageMetadata,
contents,
settings.config.experimental.svgo,
settings.config.experimental.svgOptimizer,
);
const metadataWithSvg = { ...imageMetadata, __svgData: svgData };
return {
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/config/entrypoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export { validateConfig } from '../core/config/validate.js';
export { envField } from '../env/config.js';
export { defineConfig, getViteConfig } from './index.js';
export { sessionDrivers } from '../core/session/drivers.js';
export { svgoOptimizer } from '../assets/svg/svgo.js';

/**
* Return the configuration needed to use the Sharp-based image service
Expand Down
8 changes: 2 additions & 6 deletions packages/astro/src/core/config/schemas/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import type {
} from '@astrojs/markdown-remark';
import { markdownConfigDefaults, syntaxHighlightDefaults } from '@astrojs/markdown-remark';
import { type BuiltinTheme, bundledThemes } from 'shiki';
import type { Config as SvgoConfig } from 'svgo';
import * as z from 'zod/v4';
import { FontFamilySchema } from '../../../assets/fonts/config.js';
import { EnvSchema } from '../../../env/schema.js';
import type { AstroUserConfig, ViteUserConfig } from '../../../types/public/config.js';
import { allowedDirectivesSchema, cspAlgorithmSchema, cspHashSchema } from '../../csp/config.js';
import { CacheSchema, RouteRulesSchema } from '../../cache/config.js';
import { SessionSchema } from '../../session/config.js';
import { SvgOptimizerSchema } from '../../../assets/svg/config.js';

// The below types are required boilerplate to work around a Zod issue since v3.21.2. Since that version,
// Zod's compiled TypeScript would "simplify" certain values to their base representation, causing references
Expand Down Expand Up @@ -109,7 +109,6 @@ export const ASTRO_CONFIG_DEFAULTS = {
clientPrerender: false,
contentIntellisense: false,
chromeDevtoolsWorkspace: false,
svgo: false,
rustCompiler: false,
queuedRendering: {
enabled: false,
Expand Down Expand Up @@ -533,10 +532,7 @@ export const AstroConfigSchema = z.object({
.boolean()
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.chromeDevtoolsWorkspace),
svgo: z
.union([z.boolean(), z.custom<SvgoConfig>((value) => value && typeof value === 'object')])
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.svgo),
svgOptimizer: SvgOptimizerSchema.optional(),
cache: CacheSchema.optional(),
routeRules: RouteRulesSchema.optional(),
rustCompiler: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.rustCompiler),
Expand Down
6 changes: 3 additions & 3 deletions packages/astro/src/core/errors/errors-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,13 +610,13 @@ export const UnsupportedImageConversion = {

/**
* @docs
* @message An error occurred while optimizing the SVG file with SVGO.
* @message An error occurred while optimizing the SVG file with the optimizer.
*/
export const CannotOptimizeSvg = {
name: 'CannotOptimizeSvg',
title: 'Cannot optimize SVG',
message: (path: string) => `An error occurred while optimizing SVG file "${path}" with SVGO.`,
hint: 'Review the included SVGO error message provided for guidance.',
message: (path: string, name: string) => `An error occurred while optimizing SVG file "${path}" with the "${name}" optimizer.`,
hint: 'Review the included error message provided for guidance.',
} satisfies ErrorData;

/**
Expand Down
38 changes: 9 additions & 29 deletions packages/astro/src/types/public/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import type {
Smartypants,
SyntaxHighlightConfigType,
} from '@astrojs/markdown-remark';
import type { Config as SvgoConfig } from 'svgo';
import type { UserConfig as OriginalViteUserConfig, SSROptions as ViteSSROptions } from 'vite';
import type { FontFamily, FontProvider } from '../../assets/fonts/types.js';
import type { ImageFit, ImageLayout } from '../../assets/types.js';
Expand All @@ -25,6 +24,7 @@ import type {
} from '../../core/session/types.js';
import type { EnvSchema } from '../../env/schema.js';
import type { AstroIntegration } from './integrations.js';
import type { SvgOptimizer } from '../../assets/svg/types.js';

export type Locales = (string | { codes: [string, ...string[]]; path: string })[];

Expand Down Expand Up @@ -2853,14 +2853,12 @@ export interface AstroUserConfig<
chromeDevtoolsWorkspace?: boolean;

/**
* @name experimental.svgo
* @type {boolean | SvgoConfig}
* @default `false`
* @name experimental.svgOptimizer
* @type {SvgOptimizer}
* @default `undefined`
* @version 6.2.0
* @description
* Enable SVG optimization using SVGO during build time.
*
* Set to `true` to enable optimization with default settings, or pass a configuration
* object to customize SVGO behavior.
* Enable SVG optimization at build time.
*
* When enabled, all imported SVG files will be optimized for smaller file sizes
* and better performance while maintaining visual quality.
Expand All @@ -2869,32 +2867,14 @@ export interface AstroUserConfig<
* {
* experimental: {
* // Enable with defaults
* svgo: true
* }
* }
* ```
*
* To customize optimization, pass a [SVGO configuration object](https://svgo.dev/):
*
* ```js
* {
* experimental: {
* svgo: {
* plugins: [
* 'preset-default',
* {
* name: 'removeViewBox',
* active: false
* }
* ]
* }
* svgOptimizer: svgoOptimizer()
* }
* }
* ```
*
* See the [experimental SVGO optimization docs](https://docs.astro.build/en/reference/experimental-flags/svg-optimization/) for more information.
* See the [experimental SVG optimization docs](https://docs.astro.build/en/reference/experimental-flags/svg-optimization/) for more information.
*/
svgo?: boolean | SvgoConfig;
svgOptimizer?: SvgOptimizer;

/**
* @name experimental.cache
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { defineConfig } from 'astro/config';
import { defineConfig, svgoOptimizer } from 'astro/config';

export default defineConfig({
experimental: {
svgo: {
svgOptimizer: svgoOptimizer({
plugins: [
'preset-default',
{
name: 'removeViewBox',
active: false,
},
],
},
}),
},
});
8 changes: 8 additions & 0 deletions packages/astro/test/types/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import type { CacheSchema, RouteRulesSchema } from '../../src/core/cache/config.
import type { CacheProviderConfig, RouteRules } from '../../dist/core/cache/types.js';
import type { SessionDriverConfigSchema } from '../../dist/core/session/config.js';
import type { SessionDriverConfig } from '../../dist/core/session/types.js';
import type { SvgOptimizer } from '../../dist/assets/svg/types.js';
import type { SvgOptimizerSchema } from '../../dist/assets/svg/config.js';

describe('fonts', () => {
it('FontFamily type matches FontFamilySchema', () => {
Expand Down Expand Up @@ -36,3 +38,9 @@ describe('session', () => {
expectTypeOf<z.input<typeof SessionDriverConfigSchema>>().toEqualTypeOf<SessionDriverConfig>();
});
});

describe('svgOptimizer', () => {
it('SvgOptimizer type matches SvgOptimizerSchema', () => {
expectTypeOf<z.input<typeof SvgOptimizerSchema>>().toEqualTypeOf<SvgOptimizer>();
});
});
Loading