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 ? ( - + ) : (