diff --git a/examples/plugins/hello-world/icon.svg b/examples/plugins/hello-world/icon.svg
new file mode 100644
index 00000000..90a4e78a
--- /dev/null
+++ b/examples/plugins/hello-world/icon.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/components/plugins/PluginIcon.tsx b/src/components/plugins/PluginIcon.tsx
new file mode 100644
index 00000000..38e7127a
--- /dev/null
+++ b/src/components/plugins/PluginIcon.tsx
@@ -0,0 +1,39 @@
+import { useState, useEffect } from 'react';
+import { authenticatedFetch } from '../../utils/api';
+
+type Props = {
+ pluginName: string;
+ iconFile: string;
+ className?: string;
+};
+
+// Module-level cache so repeated renders don't re-fetch
+const svgCache = new Map();
+
+export default function PluginIcon({ pluginName, iconFile, className }: Props) {
+ const url = `/api/plugins/${encodeURIComponent(pluginName)}/assets/${encodeURIComponent(iconFile)}`;
+ const [svg, setSvg] = useState(svgCache.get(url) ?? null);
+
+ useEffect(() => {
+ if (svgCache.has(url)) return;
+ authenticatedFetch(url)
+ .then((r) => r.text())
+ .then((text) => {
+ if (text.trimStart().startsWith('