diff --git a/.changeset/admin-site-icon.md b/.changeset/admin-site-icon.md new file mode 100644 index 000000000..bf93f6e93 --- /dev/null +++ b/.changeset/admin-site-icon.md @@ -0,0 +1,5 @@ +--- +"emdash": patch +--- + +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/e2e/tests/admin-fixes.spec.ts b/e2e/tests/admin-fixes.spec.ts index d8a58c3c4..df988b5fc 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 c917b2865..031c2570a 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,29 @@ 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"); + +// Favicon precedence: an explicit build-time `admin.favicon` is admin-specific +// 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; +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; --- @@ -42,8 +66,8 @@ Astro.response.headers.set("Cache-Control", "private, no-store"); - {adminConfig?.favicon ? ( - + {favicon ? ( + ) : (