| ... | ... | @@ -9,6 +9,8 @@ const FAVICON_MIME_TYPES: Record<string, string> = { |
| 9 | 9 | ".ico": "image/x-icon", |
| 10 | 10 | }; |
| 11 | 11 | |
| 12 | const 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 | 14 | // Well-known favicon paths checked in order. |
| 13 | 15 | const FAVICON_CANDIDATES = [ |
| 14 | 16 | "favicon.svg", |
| ... | ... | @@ -85,6 +87,14 @@ function serveFaviconFile(filePath: string, res: http.ServerResponse): void { |
| 85 | 87 | }); |
| 86 | 88 | } |
| 87 | 89 | |
| 90 | function 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 | |
| 88 | 98 | export function tryHandleProjectFaviconRequest(url: URL, res: http.ServerResponse): boolean { |
| 89 | 99 | if (url.pathname !== "/api/project-favicon") { |
| 90 | 100 | return false; |
| ... | ... | @@ -118,8 +128,7 @@ export function tryHandleProjectFaviconRequest(url: URL, res: http.ServerRespons |
| 118 | 128 | |
| 119 | 129 | const trySourceFiles = (index: number): void => { |
| 120 | 130 | if (index >= ICON_SOURCE_FILES.length) { |
| 121 | | res.writeHead(404, { "Content-Type": "text/plain" }); |
| 122 | | res.end("No favicon found"); |
| 131 | serveFallbackFavicon(res); |
| 123 | 132 | return; |
| 124 | 133 | } |
| 125 | 134 | const sourceFile = path.join(projectCwd, ICON_SOURCE_FILES[index]!); |