From 7c540abdf4f34a07df002590f9a1fe10935bf9cd Mon Sep 17 00:00:00 2001
From: swissky <30409887+swissky@users.noreply.github.com>
Date: Tue, 16 Jun 2026 15:38:41 +0200
Subject: [PATCH 1/4] fix(admin): use site favicon (Site Icon) for the admin
shell
---
.changeset/admin-site-icon.md | 5 +++++
packages/core/src/astro/routes/admin.astro | 21 +++++++++++++++++++--
2 files changed, 24 insertions(+), 2 deletions(-)
create mode 100644 .changeset/admin-site-icon.md
diff --git a/.changeset/admin-site-icon.md b/.changeset/admin-site-icon.md
new file mode 100644
index 000000000..c0d59fc56
--- /dev/null
+++ b/.changeset/admin-site-icon.md
@@ -0,0 +1,5 @@
+---
+"emdash": patch
+---
+
+The admin shell now uses the Site Icon configured in Settings → General as its favicon, so the EmDash backend is branded like the public site. Falls back to the build-time `admin.favicon` config and then the default EmDash mark when no Site Icon is set.
diff --git a/packages/core/src/astro/routes/admin.astro b/packages/core/src/astro/routes/admin.astro
index c917b2865..e41ed2b5a 100644
--- a/packages/core/src/astro/routes/admin.astro
+++ b/packages/core/src/astro/routes/admin.astro
@@ -17,6 +17,7 @@ import { Font } from "astro:assets";
export const prerender = false;
import { resolveLocale, loadMessages, getLocaleDir } from "@emdash-cms/admin/locales";
+import { getSiteSettingsWithDb } from "#settings/index.js";
const resolvedLocale = resolveLocale(Astro.request);
const resolvedDir = getLocaleDir(resolvedLocale);
@@ -33,6 +34,22 @@ const pageTitle = adminConfig?.siteName ? `${adminConfig.siteName} Admin` : "EmD
// via cookie/Accept-Language). API routes already send `private, no-store`
// (API_CACHE_HEADERS); this closes the same gap for the admin HTML.
Astro.response.headers.set("Cache-Control", "private, no-store");
+
+// Prefer the Site Icon configured in Settings → General so the admin shell is
+// branded like the public site (WordPress-style Site Icon, which also brands
+// wp-admin). Falls back to the build-time `admin.favicon` config, then the
+// bundled EmDash mark below.
+const emdash = Astro.locals.emdash;
+let siteFavicon: string | undefined;
+if (emdash?.db) {
+ try {
+ const settings = await getSiteSettingsWithDb(emdash.db, emdash.storage ?? null);
+ siteFavicon = settings.favicon?.url ?? undefined;
+ } catch {
+ // Settings unavailable (e.g. pre-setup) — fall back to the config/default.
+ }
+}
+const favicon = siteFavicon ?? adminConfig?.favicon;
---
@@ -42,8 +59,8 @@ Astro.response.headers.set("Cache-Control", "private, no-store");
- {adminConfig?.favicon ? (
-
+ {favicon ? (
+
) : (
Date: Wed, 1 Jul 2026 15:35:23 +0200
Subject: [PATCH 2/4] fix(admin): prefer explicit admin.favicon over Site Icon
Address review: an explicitly configured build-time admin.favicon is
admin-specific and should not be overridden by the more general Site
Icon. Flip precedence to admin.favicon -> Site Icon -> bundled mark.
The Site Icon still brands the admin for the common case where no
admin.favicon is set.
---
.changeset/admin-site-icon.md | 2 +-
packages/core/src/astro/routes/admin.astro | 9 +++++----
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/.changeset/admin-site-icon.md b/.changeset/admin-site-icon.md
index c0d59fc56..bf93f6e93 100644
--- a/.changeset/admin-site-icon.md
+++ b/.changeset/admin-site-icon.md
@@ -2,4 +2,4 @@
"emdash": patch
---
-The admin shell now uses the Site Icon configured in Settings → General as its favicon, so the EmDash backend is branded like the public site. Falls back to the build-time `admin.favicon` config and then the default EmDash mark when no Site Icon is set.
+The admin shell now falls back to the Site Icon configured in Settings → General for its favicon, so the EmDash backend is branded like the public site. An explicit build-time `admin.favicon` still takes precedence, and the default EmDash mark is used when neither is set.
diff --git a/packages/core/src/astro/routes/admin.astro b/packages/core/src/astro/routes/admin.astro
index e41ed2b5a..c274a6c70 100644
--- a/packages/core/src/astro/routes/admin.astro
+++ b/packages/core/src/astro/routes/admin.astro
@@ -35,9 +35,10 @@ const pageTitle = adminConfig?.siteName ? `${adminConfig.siteName} Admin` : "EmD
// (API_CACHE_HEADERS); this closes the same gap for the admin HTML.
Astro.response.headers.set("Cache-Control", "private, no-store");
-// Prefer the Site Icon configured in Settings → General so the admin shell is
-// branded like the public site (WordPress-style Site Icon, which also brands
-// wp-admin). Falls back to the build-time `admin.favicon` config, then the
+// Favicon precedence: an explicit build-time `admin.favicon` is admin-specific
+// and always wins. Otherwise fall back to the Site Icon configured in
+// Settings → General, so the admin shell is branded like the public site
+// (WordPress-style Site Icon, which also brands wp-admin), and finally the
// bundled EmDash mark below.
const emdash = Astro.locals.emdash;
let siteFavicon: string | undefined;
@@ -49,7 +50,7 @@ if (emdash?.db) {
// Settings unavailable (e.g. pre-setup) — fall back to the config/default.
}
}
-const favicon = siteFavicon ?? adminConfig?.favicon;
+const favicon = adminConfig?.favicon ?? siteFavicon;
---
From 4a0c7e6f3c77ec6588562495d2f87439c3164022 Mon Sep 17 00:00:00 2001
From: swissky <30409887+swissky@users.noreply.github.com>
Date: Wed, 1 Jul 2026 22:08:07 +0200
Subject: [PATCH 3/4] fix(admin): skip the settings read when admin.favicon is
configured
---
packages/core/src/astro/routes/admin.astro | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/packages/core/src/astro/routes/admin.astro b/packages/core/src/astro/routes/admin.astro
index c274a6c70..e5a12d6df 100644
--- a/packages/core/src/astro/routes/admin.astro
+++ b/packages/core/src/astro/routes/admin.astro
@@ -36,13 +36,13 @@ const pageTitle = adminConfig?.siteName ? `${adminConfig.siteName} Admin` : "EmD
Astro.response.headers.set("Cache-Control", "private, no-store");
// Favicon precedence: an explicit build-time `admin.favicon` is admin-specific
-// and always wins. Otherwise fall back to the Site Icon configured in
-// Settings → General, so the admin shell is branded like the public site
-// (WordPress-style Site Icon, which also brands wp-admin), and finally the
-// bundled EmDash mark below.
+// and always wins — in that case the settings read is skipped entirely.
+// Otherwise fall back to the Site Icon configured in Settings → General, so
+// the admin shell is branded like the public site (WordPress-style Site Icon,
+// which also brands wp-admin), and finally the bundled EmDash mark below.
const emdash = Astro.locals.emdash;
let siteFavicon: string | undefined;
-if (emdash?.db) {
+if (!adminConfig?.favicon && emdash?.db) {
try {
const settings = await getSiteSettingsWithDb(emdash.db, emdash.storage ?? null);
siteFavicon = settings.favicon?.url ?? undefined;
From 6eb89fe1e8c953b2dc0a1d384aae2763e5a850ef Mon Sep 17 00:00:00 2001
From: swissky <30409887+swissky@users.noreply.github.com>
Date: Fri, 10 Jul 2026 11:42:19 +0200
Subject: [PATCH 4/4] fix: emit the Site Icon MIME type on the admin favicon
link
The media-file URL has no extension, so Chromium ignores SVG favicons
without type="image/svg+xml". Adds an e2e test that sets a Site Icon
via the API and asserts the admin shell links it with its MIME type.
---
e2e/tests/admin-fixes.spec.ts | 53 ++++++++++++++++++++++
packages/core/src/astro/routes/admin.astro | 8 +++-
2 files changed, 60 insertions(+), 1 deletion(-)
diff --git a/e2e/tests/admin-fixes.spec.ts b/e2e/tests/admin-fixes.spec.ts
index 93d1f51c0..0a9ada01f 100644
--- a/e2e/tests/admin-fixes.spec.ts
+++ b/e2e/tests/admin-fixes.spec.ts
@@ -380,3 +380,56 @@ test.describe("Autosave after perf optimizations", () => {
expect(postData).toContain("ABCDEF");
});
});
+
+// ==========================================================================
+// Admin favicon uses the configured Site Icon (#1477)
+// ==========================================================================
+
+test.describe("Admin favicon from Site Icon", () => {
+ test("admin shell links the Site Icon with its MIME type", async ({
+ admin,
+ page,
+ serverInfo,
+ }) => {
+ await admin.devBypassAuth();
+ const baseUrl = serverInfo.baseUrl;
+ const headers = apiHeaders(serverInfo.token, baseUrl);
+
+ // Upload an SVG — the case that needs type="image/svg+xml" (Chromium
+ // ignores SVG favicons served from extension-less URLs without it).
+ const svg = ``;
+ const form = new FormData();
+ form.append("file", new File([svg], "e2e-site-icon.svg", { type: "image/svg+xml" }));
+ const uploadRes = await fetch(`${baseUrl}/_emdash/api/media`, {
+ method: "POST",
+ // No Content-Type header — fetch sets the multipart boundary itself
+ headers: {
+ Authorization: headers.Authorization,
+ "X-EmDash-Request": "1",
+ Origin: baseUrl,
+ },
+ body: form,
+ });
+ if (!uploadRes.ok) {
+ test.skip();
+ return;
+ }
+ const uploadData: any = await uploadRes.json();
+ const mediaId = uploadData.data?.item?.id;
+ expect(mediaId).toBeTruthy();
+
+ // Set it as the Site Icon
+ const settingsRes = await fetch(`${baseUrl}/_emdash/api/settings`, {
+ method: "POST",
+ headers,
+ body: JSON.stringify({ favicon: { mediaId } }),
+ });
+ expect(settingsRes.ok).toBe(true);
+
+ // The admin shell should link the Site Icon and carry its MIME type
+ await page.goto(`${baseUrl}/_emdash/admin`);
+ const icon = page.locator('link[rel="icon"]');
+ await expect(icon).toHaveAttribute("href", /\/_emdash\/api\/media\/file\//);
+ await expect(icon).toHaveAttribute("type", "image/svg+xml");
+ });
+});
diff --git a/packages/core/src/astro/routes/admin.astro b/packages/core/src/astro/routes/admin.astro
index e5a12d6df..031c2570a 100644
--- a/packages/core/src/astro/routes/admin.astro
+++ b/packages/core/src/astro/routes/admin.astro
@@ -42,15 +42,21 @@ Astro.response.headers.set("Cache-Control", "private, no-store");
// which also brands wp-admin), and finally the bundled EmDash mark below.
const emdash = Astro.locals.emdash;
let siteFavicon: string | undefined;
+let siteFaviconType: string | undefined;
if (!adminConfig?.favicon && emdash?.db) {
try {
const settings = await getSiteSettingsWithDb(emdash.db, emdash.storage ?? null);
siteFavicon = settings.favicon?.url ?? undefined;
+ siteFaviconType = settings.favicon?.contentType ?? undefined;
} catch {
// Settings unavailable (e.g. pre-setup) — fall back to the config/default.
}
}
const favicon = adminConfig?.favicon ?? siteFavicon;
+// The media-file URL has no extension, so browsers need the MIME type —
+// Chromium ignores SVG favicons without type="image/svg+xml". Only set for
+// Site Icons; build-time `admin.favicon` strings keep their current behavior.
+const faviconType = adminConfig?.favicon ? undefined : siteFaviconType;
---
@@ -61,7 +67,7 @@ const favicon = adminConfig?.favicon ?? siteFavicon;
{favicon ? (
-
+
) : (