fix(studio): inject base tag for asset resolution in preview

The preview iframe serves bundled HTML from /api/projects/:id/preview
but relative asset paths (video.mp4, font.woff2) resolve to the wrong
URL without a <base> tag. Now injects <base href="/api/projects/:id/preview/">
so relative paths route through the static asset handler.

Also adds proper MIME types for video, audio, image, and font files
served from the preview asset route.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-03-26 18:03:32 +00:00
committed by Vance Ingalls
co-authored by Claude Opus 4.6
parent 30821cd35c
commit 263f35bd75
+31 -8
View File
@@ -162,9 +162,16 @@ function devProjectApi(): Plugin {
if (req.method === "GET" && rest === "/preview") {
try {
const bundler = await getBundler();
const bundled = bundler
let bundled = bundler
? await bundler(projectDir)
: readFileSync(join(projectDir, "index.html"), "utf-8");
// Inject <base> so relative asset paths resolve through /preview/ route
const baseTag = `<base href="/api/projects/${projectId}/preview/">`;
if (bundled.includes("<head>")) {
bundled = bundled.replace("<head>", `<head>${baseTag}`);
} else {
bundled = baseTag + bundled;
}
res.writeHead(200, {
"Content-Type": "text/html; charset=utf-8",
"Cache-Control": "no-store",
@@ -274,13 +281,29 @@ ${content}
return;
}
const isText = /\.(html|css|js|json|svg|txt)$/i.test(subPath);
const contentType = subPath.endsWith(".html")
? "text/html"
: subPath.endsWith(".js")
? "text/javascript"
: subPath.endsWith(".css")
? "text/css"
: "application/octet-stream";
const mimeTypes: Record<string, string> = {
".html": "text/html",
".js": "text/javascript",
".css": "text/css",
".json": "application/json",
".svg": "image/svg+xml",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".webp": "image/webp",
".gif": "image/gif",
".mp4": "video/mp4",
".webm": "video/webm",
".mp3": "audio/mpeg",
".wav": "audio/wav",
".m4a": "audio/mp4",
".ogg": "audio/ogg",
".woff2": "font/woff2",
".woff": "font/woff",
".ttf": "font/ttf",
};
const ext = "." + subPath.split(".").pop()?.toLowerCase();
const contentType = mimeTypes[ext] ?? "application/octet-stream";
res.writeHead(200, { "Content-Type": contentType });
res.end(readFileSync(file, isText ? "utf-8" : undefined));
return;