diff --git a/.changeset/tricky-dolls-cover.md b/.changeset/tricky-dolls-cover.md
new file mode 100644
index 00000000000..5574c309b9f
--- /dev/null
+++ b/.changeset/tricky-dolls-cover.md
@@ -0,0 +1,5 @@
+---
+'@astrojs/starlight': patch
+---
+
+Fixes an issue where aside icons were rendered incorrectly in projects where Astro’s MDX integration had optimization disabled
diff --git a/packages/starlight/__e2e__/fixtures/no-mdx-optimization/astro.config.mjs b/packages/starlight/__e2e__/fixtures/no-mdx-optimization/astro.config.mjs
new file mode 100644
index 00000000000..5d063eb8fca
--- /dev/null
+++ b/packages/starlight/__e2e__/fixtures/no-mdx-optimization/astro.config.mjs
@@ -0,0 +1,15 @@
+// @ts-check
+import mdx from '@astrojs/mdx';
+import starlight from '@astrojs/starlight';
+import { defineConfig } from 'astro/config';
+
+export default defineConfig({
+ integrations: [
+ starlight({
+ title: 'Basics',
+ pagefind: false,
+ }),
+ // Starlight sets `optimize: true`, so we provide our own copy of the MDX integration to test without optimization.
+ mdx({ optimize: false }),
+ ],
+});
diff --git a/packages/starlight/__e2e__/fixtures/no-mdx-optimization/package.json b/packages/starlight/__e2e__/fixtures/no-mdx-optimization/package.json
new file mode 100644
index 00000000000..390b6be783c
--- /dev/null
+++ b/packages/starlight/__e2e__/fixtures/no-mdx-optimization/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@e2e/no-mdx-optimization",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@astrojs/starlight": "workspace:*",
+ "astro": "^7.0.2"
+ }
+}
diff --git a/packages/starlight/__e2e__/fixtures/no-mdx-optimization/src/content.config.ts b/packages/starlight/__e2e__/fixtures/no-mdx-optimization/src/content.config.ts
new file mode 100644
index 00000000000..080e5813f72
--- /dev/null
+++ b/packages/starlight/__e2e__/fixtures/no-mdx-optimization/src/content.config.ts
@@ -0,0 +1,7 @@
+import { docsLoader } from '@astrojs/starlight/loaders';
+import { docsSchema } from '@astrojs/starlight/schema';
+import { defineCollection } from 'astro:content';
+
+export const collections = {
+ docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
+};
diff --git a/packages/starlight/__e2e__/fixtures/no-mdx-optimization/src/content/docs/asides.mdx b/packages/starlight/__e2e__/fixtures/no-mdx-optimization/src/content/docs/asides.mdx
new file mode 100644
index 00000000000..a9f631bd73e
--- /dev/null
+++ b/packages/starlight/__e2e__/fixtures/no-mdx-optimization/src/content/docs/asides.mdx
@@ -0,0 +1,19 @@
+---
+title: Asides
+---
+
+:::note
+Directive aside
+:::
+
+:::tip{icon="heart"}
+Directive aside with custom icon
+:::
+
+import { Aside } from '@astrojs/starlight/components';
+
+
+
+
diff --git a/packages/starlight/__e2e__/no-mdx-optimization.test.ts b/packages/starlight/__e2e__/no-mdx-optimization.test.ts
new file mode 100644
index 00000000000..d66b86ce5d5
--- /dev/null
+++ b/packages/starlight/__e2e__/no-mdx-optimization.test.ts
@@ -0,0 +1,19 @@
+import { expect, testFactory } from './test-utils';
+
+const test = testFactory('./fixtures/no-mdx-optimization/');
+
+test.describe('no MDX optimization', () => {
+ test.describe('asides', () => {
+ test('asides render an SVG icon', async ({ page, getProdServer }) => {
+ const starlight = await getProdServer();
+ await starlight.goto('/asides/');
+
+ const asideIconsLocator = page.locator('.starlight-aside__title svg');
+
+ // All four aside instances should have an icon, so we expect four icons to be rendered.
+ // This checks an issue where SVGs were escaped and rendered as text with MDX optimization
+ // disabled. See https://github.com/withastro/starlight/pull/3967#issuecomment-4833368829
+ await expect(asideIconsLocator).toHaveCount(4);
+ });
+ });
+});
diff --git a/packages/starlight/integrations/satteri.ts b/packages/starlight/integrations/satteri.ts
index 32a0907d892..7fc25a006ae 100644
--- a/packages/starlight/integrations/satteri.ts
+++ b/packages/starlight/integrations/satteri.ts
@@ -103,6 +103,15 @@ function satteriAsidesPlugin(
const icon = getAsideIcon(variant, node.attributes?.['icon']);
const iconSvg = ``;
+ // Markdown and MDX require different AST shapes for raw HTML content.
+ // TODO: replace endsWith check with an official Sätteri API once available.
+ const iconNode = ctx.fileURL?.pathname.endsWith('.mdx')
+ ? {
+ type: 'mdxJsxTextElement',
+ name: 'Fragment',
+ attributes: [{ type: 'mdxJsxAttribute', name: 'set:html', value: iconSvg }],
+ }
+ : { type: 'html', value: iconSvg };
return paragraphElement(
'aside',
@@ -112,7 +121,7 @@ function satteriAsidesPlugin(
},
[
paragraphElement('p', { class: 'starlight-aside__title', 'aria-hidden': 'true' }, [
- { type: 'html', value: iconSvg },
+ iconNode,
...titleNode,
]),
paragraphElement('div', { class: 'starlight-aside__content' }, children),
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 281db62031b..f1bee5817ec 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -322,6 +322,15 @@ importers:
specifier: ^7.0.2
version: 7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(jiti@2.6.1)(yaml@2.8.3)
+ packages/starlight/__e2e__/fixtures/no-mdx-optimization:
+ dependencies:
+ '@astrojs/starlight':
+ specifier: workspace:*
+ version: link:../../..
+ astro:
+ specifier: ^7.0.2
+ version: 7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(jiti@2.6.1)(yaml@2.8.3)
+
packages/starlight/__e2e__/fixtures/ssr:
dependencies:
'@astrojs/node':