authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-01 23:15:19-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-01 23:18:45-08:00
log96bf54e293b40c62a49162cae8586f9867a868e3
treeb48e6c9e8f14af7fa3f483bb856f94b93a69c588
parent5ca9b54d6627e23ab9d3d4d14888770e9264a933

Serve fallback project favicon when no icon file is found

- return an inline SVG favicon instead of 404 when project/icon files are missing - add route test coverage for fallback response status and MIME type

2 files changed, 23 insertions(+), 2 deletions(-)

apps/server/src/projectFaviconRoute.test.ts+12
...@@ -152,4 +152,16 @@ describe("tryHandleProjectFaviconRequest", () => {...@@ -152,4 +152,16 @@ describe("tryHandleProjectFaviconRequest", () => {
152 expect(response.body).toBe("<svg>brand-obj-order</svg>");152 expect(response.body).toBe("<svg>brand-obj-order</svg>");
153 });153 });
154 });154 });
155
156 it("serves a fallback favicon when no icon exists", async () => {
157 const projectDir = makeTempDir("t3code-favicon-route-fallback-");
158
159 await withRouteServer(async (baseUrl) => {
160 const pathname = `/api/project-favicon?cwd=${encodeURIComponent(projectDir)}`;
161 const response = await request(baseUrl, pathname);
162 expect(response.statusCode).toBe(200);
163 expect(response.contentType).toContain("image/svg+xml");
164 expect(response.body).toContain('data-fallback="project-favicon"');
165 });
166 });
155});167});
apps/server/src/projectFaviconRoute.ts+11-2
...@@ -9,6 +9,8 @@ const FAVICON_MIME_TYPES: Record<string, string> = {...@@ -9,6 +9,8 @@ const FAVICON_MIME_TYPES: Record<string, string> = {
9 ".ico": "image/x-icon",9 ".ico": "image/x-icon",
10};10};
1111
12const FALLBACK_FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#6b728080" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-fallback="project-favicon"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-8l-2-2H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2Z"/></svg>`;
13
12// Well-known favicon paths checked in order.14// Well-known favicon paths checked in order.
13const FAVICON_CANDIDATES = [15const FAVICON_CANDIDATES = [
14 "favicon.svg",16 "favicon.svg",
...@@ -85,6 +87,14 @@ function serveFaviconFile(filePath: string, res: http.ServerResponse): void {...@@ -85,6 +87,14 @@ function serveFaviconFile(filePath: string, res: http.ServerResponse): void {
85 });87 });
86}88}
8789
90function serveFallbackFavicon(res: http.ServerResponse): void {
91 res.writeHead(200, {
92 "Content-Type": "image/svg+xml",
93 "Cache-Control": "public, max-age=3600",
94 });
95 res.end(FALLBACK_FAVICON_SVG);
96}
97
88export function tryHandleProjectFaviconRequest(url: URL, res: http.ServerResponse): boolean {98export function tryHandleProjectFaviconRequest(url: URL, res: http.ServerResponse): boolean {
89 if (url.pathname !== "/api/project-favicon") {99 if (url.pathname !== "/api/project-favicon") {
90 return false;100 return false;
...@@ -118,8 +128,7 @@ export function tryHandleProjectFaviconRequest(url: URL, res: http.ServerRespons...@@ -118,8 +128,7 @@ export function tryHandleProjectFaviconRequest(url: URL, res: http.ServerRespons
118128
119 const trySourceFiles = (index: number): void => {129 const trySourceFiles = (index: number): void => {
120 if (index >= ICON_SOURCE_FILES.length) {130 if (index >= ICON_SOURCE_FILES.length) {
121 res.writeHead(404, { "Content-Type": "text/plain" });131 serveFallbackFavicon(res);
122 res.end("No favicon found");
123 return;132 return;
124 }133 }
125 const sourceFile = path.join(projectCwd, ICON_SOURCE_FILES[index]!);134 const sourceFile = path.join(projectCwd, ICON_SOURCE_FILES[index]!);