diff --git a/CLAUDE.md b/CLAUDE.md index 50e0800..321a246 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,6 +41,7 @@ Single Cloudflare Worker built with Hono. Routes: | `GET /files/:attachmentId/:filename` | R2 attachment serving | | `GET /admin` | Password-protected admin UI | | `/hub` | WebSub hub (subscribe/publish) | +| `GET /favicon.svg`, `/favicon.ico` | Project favicon (envelope logo); fallback for per-feed favicons | | `GET /health` | Health check | | `email` | Cloudflare Email routing handler (alternative to ForwardEmail webhook) | diff --git a/TODO.md b/TODO.md index 00e8da3..c7737aa 100644 --- a/TODO.md +++ b/TODO.md @@ -10,7 +10,7 @@ Feature gaps identified by comparing with [kill-the-newsletter](https://github.c - [x] **JSON API for feed creation** — accept `Content-Type: application/json` on `POST /admin/feeds` and return `{ feedId, email, feedUrl }`. Useful for automation (e.g. Tofu/OpenTofu provisioning). -- [ ] **Project favicon** — serve a single bundled icon at `/favicon.ico` and add a `` in the shared `Layout` so the admin UI, status page, and entry views stop 404-ing. Doubles as the default/fallback icon for the per-feed favicon feature below. +- [x] **Project favicon** — serve a single bundled icon at `/favicon.ico` and add a `` in the shared `Layout` so the admin UI, status page, and entry views stop 404-ing. Doubles as the default/fallback icon for the per-feed favicon feature below. ## Medium effort diff --git a/src/index.ts b/src/index.ts index 99e73fb..265e31f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ import { handle as handleEntry } from "./routes/entries"; import { handle as handleFiles } from "./routes/files"; import { handle as handleStats } from "./routes/stats"; import { handle as handleHome } from "./routes/home"; +import { handle as handleFavicon } from "./routes/favicon"; import { hubRouter } from "./routes/hub"; import { handleCloudflareEmail } from "./lib/cloudflare-email"; import { Env } from "./types"; @@ -168,6 +169,10 @@ app.route("/files", files); app.route("/admin", admin); app.route("/hub", hubRouter); +// Project favicon (also the fallback for the future per-feed favicon) +app.get("/favicon.svg", handleFavicon); +app.get("/favicon.ico", handleFavicon); // readers/browsers that hardcode .ico + // Health check endpoint for monitoring app.get("/health", (c) => c.json({ status: "ok", timestamp: Date.now() })); diff --git a/src/routes/admin/ui.tsx b/src/routes/admin/ui.tsx index 35fdcd8..742cd0e 100644 --- a/src/routes/admin/ui.tsx +++ b/src/routes/admin/ui.tsx @@ -3,8 +3,14 @@ import layoutCss from "../../styles/layout.css"; import componentsCss from "../../styles/components.css"; import utilitiesCss from "../../styles/utilities.css"; import { interactiveScripts } from "../../scripts/index"; +import { FAVICON_PATH } from "../favicon"; -const designSystem = [variablesCss, layoutCss, componentsCss, utilitiesCss].join("\n"); +const designSystem = [ + variablesCss, + layoutCss, + componentsCss, + utilitiesCss, +].join("\n"); type LayoutProps = { title: string; @@ -17,6 +23,7 @@ export const Layout = ({ title, label = "admin", children }: LayoutProps) => {