authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-27 15:11:33-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-27 15:21:31-08:00
loge6c5a4a4bb962c8276887c26e889fe97a8668f44
tree39e1bc114e9432cefe2d39591aaf2d0611fdb435
parent732b3abf9970e80b12bb912cf2643a40da7be11e

Handle favicon links when href precedes rel

- Update favicon regex parsing to support rel/href in either order - Add route tests for HTML link tags and object-style metadata with href-first ordering

2 files changed, 42 insertions(+), 3 deletions(-)

apps/server/src/projectFaviconRoute.test.ts+37
......@@ -115,4 +115,41 @@ describe("tryHandleProjectFaviconRequest", () => {
115115 expect(response.body).toBe("<svg>brand</svg>");
116116 });
117117 });
118
119 it("resolves icon link when href appears before rel in HTML", async () => {
120 const projectDir = makeTempDir("t3code-favicon-route-html-order-");
121 const iconPath = path.join(projectDir, "public", "brand", "logo.svg");
122 fs.mkdirSync(path.dirname(iconPath), { recursive: true });
123 fs.writeFileSync(path.join(projectDir, "index.html"), '<link href="/brand/logo.svg" rel="icon">');
124 fs.writeFileSync(iconPath, "<svg>brand-html-order</svg>", "utf8");
125
126 await withRouteServer(async (baseUrl) => {
127 const pathname = `/api/project-favicon?cwd=${encodeURIComponent(projectDir)}`;
128 const response = await request(baseUrl, pathname);
129 expect(response.statusCode).toBe(200);
130 expect(response.contentType).toContain("image/svg+xml");
131 expect(response.body).toBe("<svg>brand-html-order</svg>");
132 });
133 });
134
135 it("resolves object-style icon metadata when href appears before rel", async () => {
136 const projectDir = makeTempDir("t3code-favicon-route-obj-order-");
137 const iconPath = path.join(projectDir, "public", "brand", "obj.svg");
138 fs.mkdirSync(path.dirname(iconPath), { recursive: true });
139 fs.mkdirSync(path.join(projectDir, "src"), { recursive: true });
140 fs.writeFileSync(
141 path.join(projectDir, "src", "root.tsx"),
142 'const links = [{ href: "/brand/obj.svg", rel: "icon" }];',
143 "utf8",
144 );
145 fs.writeFileSync(iconPath, "<svg>brand-obj-order</svg>", "utf8");
146
147 await withRouteServer(async (baseUrl) => {
148 const pathname = `/api/project-favicon?cwd=${encodeURIComponent(projectDir)}`;
149 const response = await request(baseUrl, pathname);
150 expect(response.statusCode).toBe(200);
151 expect(response.contentType).toContain("image/svg+xml");
152 expect(response.body).toBe("<svg>brand-obj-order</svg>");
153 });
154 });
118155});
apps/server/src/projectFaviconRoute.ts+5-3
......@@ -44,9 +44,11 @@ const ICON_SOURCE_FILES = [
4444 "src/index.html",
4545];
4646
47// Matches <link rel="icon" href="..."> or { rel: "icon", href: "..." } patterns.
48const LINK_ICON_HTML_RE = /<link[^>]*rel=["'](?:icon|shortcut icon)["'][^>]*href=["']([^"'?]+)/i;
49const LINK_ICON_OBJ_RE = /rel:\s*["'](?:icon|shortcut icon)["'][^}]*href:\s*["']([^"'?]+)/i;
47// Matches <link ...> tags or object-like icon metadata where rel/href can appear in any order.
48const LINK_ICON_HTML_RE =
49 /<link\b(?=[^>]*\brel=["'](?:icon|shortcut icon)["'])(?=[^>]*\bhref=["']([^"'?]+))[^>]*>/i;
50const LINK_ICON_OBJ_RE =
51 /(?=[^}]*\brel\s*:\s*["'](?:icon|shortcut icon)["'])(?=[^}]*\bhref\s*:\s*["']([^"'?]+))[^}]*/i;
5052
5153function extractIconHref(source: string): string | null {
5254 const htmlMatch = source.match(LINK_ICON_HTML_RE);