Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/admin-site-icon.md
Original file line number Diff line number Diff line change
@@ -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.
53 changes: 53 additions & 0 deletions e2e/tests/admin-fixes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"><rect fill="red" width="1" height="1"/></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");
});
});
28 changes: 26 additions & 2 deletions packages/core/src/astro/routes/admin.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] AGENTS.md asks that bug fixes ship with a reproducing test, and the e2e suite already exercises the admin shell (see e2e/tests/admin-fixes.spec.ts). Consider adding an assertion that visits /_emdash/admin and checks link[rel="icon"] — ideally after setting a Site Icon through the settings API — to guard both the precedence ordering and any type-attribute fix.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 6eb89fe — new e2e case in admin-fixes.spec.ts: uploads an SVG via the media API, sets it as the Site Icon through POST /_emdash/api/settings, then loads /_emdash/admin and asserts link[rel="icon"] points at the media-file URL with type="image/svg+xml". Verified green locally.

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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we're defaulting to adminConfig?.favicon we could skip getting siteFavicon if it's present

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — the settings read is now skipped entirely when an explicit admin.favicon is configured (013c8ad), so the common self-hosted case with a build-time favicon no longer pays the per-request settings lookup.

// 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;
---

<!doctype html>
Expand All @@ -42,8 +66,8 @@ Astro.response.headers.set("Cache-Control", "private, no-store");
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href={adminStylesUrl} />
<Font cssVariable="--font-emdash" />
{adminConfig?.favicon ? (
<link rel="icon" href={adminConfig.favicon} />
{favicon ? (
<link rel="icon" href={favicon} type={faviconType} />
) : (
<link
rel="icon"
Expand Down
Loading