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", "");
+
+ 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, "", "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("");
+ });
+ });
+
it("serves a fallback favicon when no icon exists", async () => {
const projectDir = makeTempDir("t3code-favicon-route-fallback-");