diff --git a/apps/server/src/project/Layers/ProjectFaviconResolver.test.ts b/apps/server/src/project/Layers/ProjectFaviconResolver.test.ts index 29b30739b9cd..21b93e321a9d 100644 --- a/apps/server/src/project/Layers/ProjectFaviconResolver.test.ts +++ b/apps/server/src/project/Layers/ProjectFaviconResolver.test.ts @@ -60,6 +60,24 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => { }), ); + it.effect("resolves SvelteKit app icons from src/app.html into static assets", () => + Effect.gen(function* () { + const resolver = yield* ProjectFaviconResolver; + const cwd = yield* makeTempDir; + yield* writeTextFile( + cwd, + "src/app.html", + '', + ); + yield* writeTextFile(cwd, "static/brand/logo.svg", "brand"); + + const resolved = yield* resolver.resolvePath(cwd); + + expect(resolved).not.toBeNull(); + expect(resolved).toContain("static/brand/logo.svg"); + }), + ); + it.effect("returns null when no icon is present", () => Effect.gen(function* () { const resolver = yield* ProjectFaviconResolver; diff --git a/apps/server/src/project/Layers/ProjectFaviconResolver.ts b/apps/server/src/project/Layers/ProjectFaviconResolver.ts index 3004a7a45cfd..299566673775 100644 --- a/apps/server/src/project/Layers/ProjectFaviconResolver.ts +++ b/apps/server/src/project/Layers/ProjectFaviconResolver.ts @@ -27,6 +27,13 @@ const FAVICON_CANDIDATES = [ "assets/icon.png", "assets/logo.svg", "assets/logo.png", + "static/favicon.svg", + "static/favicon.ico", + "static/favicon.png", + "static/icon.svg", + "static/icon.png", + "static/logo.svg", + "static/logo.png", ] as const; // Files that may contain a or icon metadata declaration. @@ -38,6 +45,8 @@ const ICON_SOURCE_FILES = [ "app/root.tsx", "src/root.tsx", "src/index.html", + "src/app.html", + "app.html", ] as const; // Matches tags or object-like icon metadata where rel/href can appear in any order. @@ -58,9 +67,24 @@ export const makeProjectFaviconResolver = Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const resolveIconHref = (projectCwd: string, href: string): string[] => { - const clean = href.replace(/^\//, ""); - return [path.join(projectCwd, "public", clean), path.join(projectCwd, clean)]; + const resolveIconHref = (projectCwd: string, sourcePath: string, href: string): string[] => { + if (/^(?:[a-z]+:)?\/\//i.test(href) || href.startsWith("data:")) { + return []; + } + + const normalizedHref = href.replace(/^%sveltekit\.assets%/, ""); + const clean = normalizedHref.replace(/^\/+/, "").replace(/^\.\//, ""); + const projectCandidates = [ + path.join(projectCwd, "public", clean), + path.join(projectCwd, "static", clean), + path.join(projectCwd, clean), + ]; + + if (href.startsWith("/") || href.startsWith("%sveltekit.assets%")) { + return projectCandidates; + } + + return [path.join(path.dirname(sourcePath), clean), ...projectCandidates]; }; const isPathWithinProject = (projectCwd: string, candidatePath: string): boolean => { @@ -109,7 +133,7 @@ export const makeProjectFaviconResolver = Effect.gen(function* () { if (!href) { continue; } - const existing = yield* findExistingFile(cwd, resolveIconHref(cwd, href)); + const existing = yield* findExistingFile(cwd, resolveIconHref(cwd, sourcePath, href)); if (existing) { return existing; } diff --git a/apps/server/src/projectFaviconRoute.test.ts b/apps/server/src/projectFaviconRoute.test.ts index 99b61aedc923..27d8240449d4 100644 --- a/apps/server/src/projectFaviconRoute.test.ts +++ b/apps/server/src/projectFaviconRoute.test.ts @@ -175,6 +175,27 @@ describe("tryHandleProjectFaviconRequest", () => { }); }); + it("resolves SvelteKit src/app.html icon hrefs from static assets", async () => { + const projectDir = makeTempDir("t3code-favicon-route-sveltekit-static-"); + const iconPath = path.join(projectDir, "static", "brand", "app.svg"); + fs.mkdirSync(path.dirname(iconPath), { recursive: true }); + fs.mkdirSync(path.join(projectDir, "src"), { recursive: true }); + fs.writeFileSync( + path.join(projectDir, "src", "app.html"), + '', + "utf8", + ); + fs.writeFileSync(iconPath, "sveltekit-static", "utf8"); + + await withRouteServer(async (baseUrl) => { + const pathname = `/api/project-favicon?cwd=${encodeURIComponent(projectDir)}`; + const response = await request(baseUrl, pathname); + expect(response.statusCode).toBe(200); + expect(response.contentType).toContain("image/svg+xml"); + expect(response.body).toBe("sveltekit-static"); + }); + }); + it("serves a fallback favicon when no icon exists", async () => { const projectDir = makeTempDir("t3code-favicon-route-fallback-");