enzostvs HF Staff commited on
Commit
65fb0ee
·
1 Parent(s): d18fddd

fix preview stuffs

Browse files
app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/route.ts ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { RepoDesignation, listFiles, spaceInfo, downloadFile } from "@huggingface/hub";
3
+
4
+ import { isAuthenticated } from "@/lib/auth";
5
+ import { Page } from "@/types";
6
+
7
+ export async function GET(
8
+ req: NextRequest,
9
+ { params }: {
10
+ params: Promise<{
11
+ namespace: string;
12
+ repoId: string;
13
+ commitId: string;
14
+ }>
15
+ }
16
+ ) {
17
+ const user = await isAuthenticated();
18
+
19
+ if (user instanceof NextResponse || !user) {
20
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
21
+ }
22
+
23
+ const param = await params;
24
+ const { namespace, repoId, commitId } = param;
25
+
26
+ try {
27
+ const repo: RepoDesignation = {
28
+ type: "space",
29
+ name: `${namespace}/${repoId}`,
30
+ };
31
+
32
+ const space = await spaceInfo({
33
+ name: `${namespace}/${repoId}`,
34
+ accessToken: user.token as string,
35
+ additionalFields: ["author"],
36
+ });
37
+
38
+ if (!space || space.sdk !== "static") {
39
+ return NextResponse.json(
40
+ { ok: false, error: "Space is not a static space." },
41
+ { status: 404 }
42
+ );
43
+ }
44
+
45
+ if (space.author !== user.name) {
46
+ return NextResponse.json(
47
+ { ok: false, error: "Space does not belong to the authenticated user." },
48
+ { status: 403 }
49
+ );
50
+ }
51
+
52
+ const pages: Page[] = [];
53
+
54
+ for await (const fileInfo of listFiles({
55
+ repo,
56
+ accessToken: user.token as string,
57
+ revision: commitId,
58
+ })) {
59
+ const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
60
+
61
+ if (fileInfo.path.endsWith(".html") || fileInfo.path.endsWith(".css") || fileInfo.path.endsWith(".js") || fileInfo.path.endsWith(".json")) {
62
+ const blob = await downloadFile({
63
+ repo,
64
+ accessToken: user.token as string,
65
+ path: fileInfo.path,
66
+ revision: commitId,
67
+ raw: true
68
+ });
69
+ const content = await blob?.text();
70
+
71
+ if (content) {
72
+ if (fileInfo.path === "index.html") {
73
+ pages.unshift({
74
+ path: fileInfo.path,
75
+ html: content,
76
+ });
77
+ } else {
78
+ pages.push({
79
+ path: fileInfo.path,
80
+ html: content,
81
+ });
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ return NextResponse.json({
88
+ ok: true,
89
+ pages,
90
+ });
91
+ } catch (error: any) {
92
+ console.error("Error fetching commit pages:", error);
93
+ return NextResponse.json(
94
+ {
95
+ ok: false,
96
+ error: error.message || "Failed to fetch commit pages",
97
+ },
98
+ { status: 500 }
99
+ );
100
+ }
101
+ }
102
+
app/api/proxy/[...path]/route.ts ADDED
@@ -0,0 +1,547 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+
3
+ export async function GET(
4
+ req: NextRequest,
5
+ { params }: { params: Promise<{ path: string[] }> }
6
+ ) {
7
+ const pathParams = await params;
8
+ const pathSegments = pathParams.path || [];
9
+
10
+ // Get the target URL from query parameter
11
+ const targetUrl = req.nextUrl.searchParams.get("url");
12
+
13
+ if (!targetUrl) {
14
+ return NextResponse.json(
15
+ { error: "Missing 'url' query parameter" },
16
+ { status: 400 }
17
+ );
18
+ }
19
+
20
+ try {
21
+ // Parse the target URL
22
+ const url = new URL(targetUrl);
23
+
24
+ // Only allow static.hf.space domains for security
25
+ if (!url.hostname.endsWith(".static.hf.space")) {
26
+ return NextResponse.json(
27
+ { error: "Only static.hf.space domains are allowed" },
28
+ { status: 403 }
29
+ );
30
+ }
31
+
32
+ // Construct the actual target URL
33
+ // If path segments exist, use them; otherwise use "/" for root
34
+ const targetPath = pathSegments.length > 0
35
+ ? "/" + pathSegments.join("/")
36
+ : "/";
37
+
38
+ // Merge query parameters from the request URL with the target URL's search params
39
+ const requestSearchParams = req.nextUrl.searchParams;
40
+ const targetSearchParams = new URLSearchParams(url.search);
41
+
42
+ // Copy all query params except 'url' to the target URL
43
+ requestSearchParams.forEach((value, key) => {
44
+ if (key !== "url") {
45
+ targetSearchParams.set(key, value);
46
+ }
47
+ });
48
+
49
+ const searchString = targetSearchParams.toString();
50
+ const fullTargetUrl = `${url.protocol}//${url.hostname}${targetPath}${searchString ? `?${searchString}` : ""}`;
51
+
52
+ // Fetch the content from the target URL
53
+ const response = await fetch(fullTargetUrl, {
54
+ headers: {
55
+ "User-Agent": "Mozilla/5.0 (compatible; DeepSite/1.0)",
56
+ },
57
+ redirect: "follow", // Follow redirects automatically
58
+ });
59
+
60
+ if (!response.ok) {
61
+ return NextResponse.json(
62
+ { error: `Failed to fetch: ${response.statusText}` },
63
+ { status: response.status }
64
+ );
65
+ }
66
+
67
+ let contentType = response.headers.get("content-type") || "";
68
+ const content = await response.text();
69
+
70
+ // Detect content type from file extension if not properly set
71
+ if (!contentType || contentType === "text/plain" || contentType === "application/octet-stream") {
72
+ const fileExtension = pathSegments.length > 0
73
+ ? pathSegments[pathSegments.length - 1].split(".").pop()?.toLowerCase()
74
+ : "";
75
+
76
+ if (fileExtension === "js") {
77
+ contentType = "application/javascript";
78
+ } else if (fileExtension === "css") {
79
+ contentType = "text/css";
80
+ } else if (fileExtension === "html" || fileExtension === "htm") {
81
+ contentType = "text/html";
82
+ } else if (fileExtension === "json") {
83
+ contentType = "application/json";
84
+ }
85
+ }
86
+
87
+ // Get the base proxy URL
88
+ // Extract the base path from the request URL
89
+ const requestUrl = new URL(req.url);
90
+ // Find where "/api/proxy" starts in the pathname
91
+ const proxyIndex = requestUrl.pathname.indexOf("/api/proxy");
92
+ const basePath = proxyIndex > 0 ? requestUrl.pathname.substring(0, proxyIndex) : "";
93
+ const proxyBaseUrl = `${basePath}/api/proxy`;
94
+ const targetUrlParam = `?url=${encodeURIComponent(`https://${url.hostname}`)}`;
95
+
96
+ // Rewrite URLs in HTML content
97
+ let processedContent = content;
98
+
99
+ if (contentType.includes("text/html")) {
100
+ // Rewrite relative URLs and URLs pointing to the same domain
101
+ processedContent = rewriteUrls(
102
+ content,
103
+ url.hostname,
104
+ proxyBaseUrl,
105
+ targetUrlParam,
106
+ pathSegments
107
+ );
108
+
109
+ // Inject script to intercept JavaScript-based navigation
110
+ processedContent = injectNavigationInterceptor(
111
+ processedContent,
112
+ url.hostname,
113
+ proxyBaseUrl,
114
+ targetUrlParam
115
+ );
116
+ } else if (contentType.includes("text/css")) {
117
+ // Rewrite URLs in CSS
118
+ processedContent = rewriteCssUrls(
119
+ content,
120
+ url.hostname,
121
+ proxyBaseUrl,
122
+ targetUrlParam
123
+ );
124
+ } else if (contentType.includes("application/javascript") || contentType.includes("text/javascript")) {
125
+ // For component files and most JavaScript, don't rewrite URLs
126
+ // Only rewrite if needed (for fetch calls, etc.)
127
+ // Most component JavaScript files don't need URL rewriting
128
+ // Only rewrite if the file contains fetch calls with relative URLs
129
+ if (content.includes("fetch(") && content.match(/fetch\(["'][^"']*[^\/]\./)) {
130
+ processedContent = rewriteJsUrls(
131
+ content,
132
+ url.hostname,
133
+ proxyBaseUrl,
134
+ targetUrlParam
135
+ );
136
+ } else {
137
+ // Don't modify component JavaScript files - they should work as-is
138
+ processedContent = content;
139
+ }
140
+ }
141
+
142
+ // Ensure JavaScript files have charset specified
143
+ let finalContentType = contentType;
144
+ if (contentType.includes("javascript") && !contentType.includes("charset")) {
145
+ finalContentType = contentType.includes(";")
146
+ ? contentType + "; charset=utf-8"
147
+ : contentType + "; charset=utf-8";
148
+ }
149
+
150
+ // Return the processed content with appropriate headers
151
+ return new NextResponse(processedContent, {
152
+ status: 200,
153
+ headers: {
154
+ "Content-Type": finalContentType,
155
+ "X-Content-Type-Options": "nosniff",
156
+ // Don't set X-Frame-Options for JavaScript files - they need to execute
157
+ ...(contentType.includes("text/html") && { "X-Frame-Options": "SAMEORIGIN" }),
158
+ // Remove CORS restrictions since we're proxying
159
+ "Access-Control-Allow-Origin": "*",
160
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
161
+ "Access-Control-Allow-Headers": "Content-Type",
162
+ },
163
+ });
164
+ } catch (error: any) {
165
+ console.error("Proxy error:", error);
166
+ return NextResponse.json(
167
+ { error: error.message || "Proxy error occurred" },
168
+ { status: 500 }
169
+ );
170
+ }
171
+ }
172
+
173
+ function rewriteUrls(
174
+ html: string,
175
+ targetHost: string,
176
+ proxyBaseUrl: string,
177
+ targetUrlParam: string,
178
+ currentPathSegments: string[] = []
179
+ ): string {
180
+ let processed = html;
181
+
182
+ // Get the current directory path for resolving relative URLs
183
+ const currentDir = currentPathSegments.length > 0
184
+ ? "/" + currentPathSegments.slice(0, -1).join("/") + "/"
185
+ : "/";
186
+
187
+ // Rewrite relative URLs in href attributes
188
+ processed = processed.replace(
189
+ /href=["']([^"']+)["']/gi,
190
+ (match, urlStr) => {
191
+ if (urlStr.startsWith("http://") || urlStr.startsWith("https://")) {
192
+ // Absolute URL - rewrite if it's the same domain
193
+ try {
194
+ const urlObj = new URL(urlStr);
195
+ if (urlObj.hostname === targetHost) {
196
+ const pathPart = urlObj.pathname === "/" ? "" : urlObj.pathname;
197
+ const searchPart = urlObj.search ? `&${urlObj.search.substring(1)}` : "";
198
+ return `href="${proxyBaseUrl}${pathPart}${targetUrlParam}${searchPart}"`;
199
+ }
200
+ } catch {
201
+ // Invalid URL, keep as is
202
+ }
203
+ return match;
204
+ } else if (urlStr.startsWith("//")) {
205
+ // Protocol-relative URL
206
+ try {
207
+ const urlObj = new URL(`https:${urlStr}`);
208
+ if (urlObj.hostname === targetHost) {
209
+ const pathPart = urlObj.pathname === "/" ? "" : urlObj.pathname;
210
+ const searchPart = urlObj.search ? `&${urlObj.search.substring(1)}` : "";
211
+ return `href="${proxyBaseUrl}${pathPart}${targetUrlParam}${searchPart}"`;
212
+ }
213
+ } catch {
214
+ // Invalid URL, keep as is
215
+ }
216
+ return match;
217
+ } else if (urlStr.startsWith("#") || urlStr.startsWith("javascript:") || urlStr.startsWith("mailto:") || urlStr.startsWith("tel:")) {
218
+ // Hash links, javascript:, mailto:, tel: - keep as is
219
+ return match;
220
+ } else {
221
+ // Relative URL - resolve it properly
222
+ let resolvedPath: string;
223
+ if (urlStr.startsWith("/")) {
224
+ // Absolute path relative to root
225
+ resolvedPath = urlStr;
226
+ } else if (urlStr.startsWith("components/") || urlStr.startsWith("images/") || urlStr.startsWith("videos/") || urlStr.startsWith("audio/")) {
227
+ // Paths starting with known directories should be treated as absolute from root
228
+ resolvedPath = "/" + urlStr;
229
+ } else {
230
+ // Relative path - resolve relative to current directory
231
+ resolvedPath = currentDir + urlStr;
232
+ // Normalize the path (remove ./ and ../)
233
+ const parts = resolvedPath.split("/");
234
+ const normalized: string[] = [];
235
+ for (const part of parts) {
236
+ if (part === "..") {
237
+ normalized.pop();
238
+ } else if (part !== "." && part !== "") {
239
+ normalized.push(part);
240
+ }
241
+ }
242
+ resolvedPath = "/" + normalized.join("/");
243
+ }
244
+ const [path, search] = resolvedPath.split("?");
245
+ const pathPart = path === "/" ? "" : path;
246
+ const searchPart = search ? `&${search}` : "";
247
+ return `href="${proxyBaseUrl}${pathPart}${targetUrlParam}${searchPart}"`;
248
+ }
249
+ }
250
+ );
251
+
252
+ // Rewrite relative URLs in src attributes
253
+ processed = processed.replace(
254
+ /src=["']([^"']+)["']/gi,
255
+ (match, urlStr) => {
256
+ if (urlStr.startsWith("http://") || urlStr.startsWith("https://")) {
257
+ try {
258
+ const urlObj = new URL(urlStr);
259
+ if (urlObj.hostname === targetHost) {
260
+ const pathPart = urlObj.pathname === "/" ? "" : urlObj.pathname;
261
+ const searchPart = urlObj.search ? `&${urlObj.search.substring(1)}` : "";
262
+ return `src="${proxyBaseUrl}${pathPart}${targetUrlParam}${searchPart}"`;
263
+ }
264
+ } catch {
265
+ // Invalid URL, keep as is
266
+ }
267
+ return match;
268
+ } else if (urlStr.startsWith("//")) {
269
+ try {
270
+ const urlObj = new URL(`https:${urlStr}`);
271
+ if (urlObj.hostname === targetHost) {
272
+ const pathPart = urlObj.pathname === "/" ? "" : urlObj.pathname;
273
+ const searchPart = urlObj.search ? `&${urlObj.search.substring(1)}` : "";
274
+ return `src="${proxyBaseUrl}${pathPart}${targetUrlParam}${searchPart}"`;
275
+ }
276
+ } catch {
277
+ // Invalid URL, keep as is
278
+ }
279
+ return match;
280
+ } else if (urlStr.startsWith("data:") || urlStr.startsWith("blob:")) {
281
+ // Data URLs and blob URLs - keep as is
282
+ return match;
283
+ } else {
284
+ // Relative URL - resolve it properly
285
+ let resolvedPath: string;
286
+ if (urlStr.startsWith("/")) {
287
+ // Absolute path relative to root
288
+ resolvedPath = urlStr;
289
+ } else if (urlStr.startsWith("components/") || urlStr.startsWith("images/") || urlStr.startsWith("videos/") || urlStr.startsWith("audio/")) {
290
+ // Paths starting with known directories should be treated as absolute from root
291
+ resolvedPath = "/" + urlStr;
292
+ } else {
293
+ // Relative path - resolve relative to current directory
294
+ resolvedPath = currentDir + urlStr;
295
+ // Normalize the path (remove ./ and ../)
296
+ const parts = resolvedPath.split("/");
297
+ const normalized: string[] = [];
298
+ for (const part of parts) {
299
+ if (part === "..") {
300
+ normalized.pop();
301
+ } else if (part !== "." && part !== "") {
302
+ normalized.push(part);
303
+ }
304
+ }
305
+ resolvedPath = "/" + normalized.join("/");
306
+ }
307
+ const [path, search] = resolvedPath.split("?");
308
+ const pathPart = path === "/" ? "" : path;
309
+ const searchPart = search ? `&${search}` : "";
310
+ return `src="${proxyBaseUrl}${pathPart}${targetUrlParam}${searchPart}"`;
311
+ }
312
+ }
313
+ );
314
+
315
+ // Rewrite URLs in action attributes (forms)
316
+ processed = processed.replace(
317
+ /action=["']([^"']+)["']/gi,
318
+ (match, urlStr) => {
319
+ if (urlStr.startsWith("http://") || urlStr.startsWith("https://")) {
320
+ try {
321
+ const urlObj = new URL(urlStr);
322
+ if (urlObj.hostname === targetHost) {
323
+ const pathPart = urlObj.pathname === "/" ? "" : urlObj.pathname;
324
+ const searchPart = urlObj.search ? `&${urlObj.search.substring(1)}` : "";
325
+ return `action="${proxyBaseUrl}${pathPart}${targetUrlParam}${searchPart}"`;
326
+ }
327
+ } catch {
328
+ // Invalid URL, keep as is
329
+ }
330
+ return match;
331
+ } else if (urlStr.startsWith("//")) {
332
+ try {
333
+ const urlObj = new URL(`https:${urlStr}`);
334
+ if (urlObj.hostname === targetHost) {
335
+ const pathPart = urlObj.pathname === "/" ? "" : urlObj.pathname;
336
+ const searchPart = urlObj.search ? `&${urlObj.search.substring(1)}` : "";
337
+ return `action="${proxyBaseUrl}${pathPart}${targetUrlParam}${searchPart}"`;
338
+ }
339
+ } catch {
340
+ // Invalid URL, keep as is
341
+ }
342
+ return match;
343
+ } else {
344
+ // Relative URL - rewrite to proxy
345
+ const cleanUrl = urlStr.startsWith("/") ? urlStr : `/${urlStr}`;
346
+ const [path, search] = cleanUrl.split("?");
347
+ const pathPart = path === "/" ? "" : path;
348
+ const searchPart = search ? `&${search}` : "";
349
+ return `action="${proxyBaseUrl}${pathPart}${targetUrlParam}${searchPart}"`;
350
+ }
351
+ }
352
+ );
353
+
354
+ // Rewrite URLs in CSS url() functions within style tags
355
+ processed = processed.replace(
356
+ /<style[^>]*>([\s\S]*?)<\/style>/gi,
357
+ (match, cssContent) => {
358
+ const rewrittenCss = rewriteCssUrls(cssContent, targetHost, proxyBaseUrl, targetUrlParam);
359
+ return match.replace(cssContent, rewrittenCss);
360
+ }
361
+ );
362
+
363
+ return processed;
364
+ }
365
+
366
+ function injectNavigationInterceptor(
367
+ html: string,
368
+ targetHost: string,
369
+ proxyBaseUrl: string,
370
+ targetUrlParam: string
371
+ ): string {
372
+ // Escape strings for safe injection
373
+ const escapeJs = (str: string) => {
374
+ return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
375
+ };
376
+
377
+ const escapedProxyBaseUrl = escapeJs(proxyBaseUrl);
378
+ const escapedTargetUrlParam = escapeJs(targetUrlParam);
379
+ const escapedTargetHost = escapeJs(targetHost);
380
+
381
+ const interceptorScript = `
382
+ <script>
383
+ (function() {
384
+ const proxyBaseUrl = "${escapedProxyBaseUrl}";
385
+ const targetUrlParam = "${escapedTargetUrlParam}";
386
+ const targetHost = "${escapedTargetHost}";
387
+
388
+ // Helper function to convert URL to proxy URL
389
+ function convertToProxyUrl(url) {
390
+ try {
391
+ const urlObj = typeof url === 'string' ? new URL(url, window.location.href) : url;
392
+ if (urlObj.hostname === targetHost || urlObj.hostname === window.location.hostname) {
393
+ const pathPart = urlObj.pathname === "/" ? "" : urlObj.pathname;
394
+ const searchPart = urlObj.search ? urlObj.search.substring(1) : "";
395
+ return proxyBaseUrl + pathPart + targetUrlParam + (searchPart ? "&" + searchPart : "");
396
+ }
397
+ return typeof url === 'string' ? url : urlObj.href;
398
+ } catch (e) {
399
+ // Relative URL or invalid URL
400
+ const urlStr = typeof url === 'string' ? url : url.href || '';
401
+ const pathPart = urlStr.startsWith("/") ? urlStr.split("?")[0] : "/" + urlStr.split("?")[0];
402
+ const searchPart = urlStr.includes("?") ? urlStr.split("?")[1] : "";
403
+ return proxyBaseUrl + pathPart + targetUrlParam + (searchPart ? "&" + searchPart : "");
404
+ }
405
+ }
406
+
407
+ // Intercept window.location.replace()
408
+ const originalReplace = window.location.replace.bind(window.location);
409
+ window.location.replace = function(url) {
410
+ const proxyUrl = convertToProxyUrl(url);
411
+ originalReplace(proxyUrl);
412
+ };
413
+
414
+ // Intercept window.location.assign()
415
+ const originalAssign = window.location.assign.bind(window.location);
416
+ window.location.assign = function(url) {
417
+ const proxyUrl = convertToProxyUrl(url);
418
+ originalAssign(proxyUrl);
419
+ };
420
+
421
+ // Intercept direct assignment to location.href using a proxy
422
+ // Since we can't override window.location, we intercept href assignments
423
+ // Try to intercept href setter, but handle gracefully if it's not configurable
424
+ try {
425
+ let locationHrefDescriptor = Object.getOwnPropertyDescriptor(window.location, 'href');
426
+ if (locationHrefDescriptor && locationHrefDescriptor.set && locationHrefDescriptor.configurable) {
427
+ const originalHrefSetter = locationHrefDescriptor.set;
428
+ Object.defineProperty(window.location, 'href', {
429
+ get: locationHrefDescriptor.get,
430
+ set: function(url) {
431
+ const proxyUrl = convertToProxyUrl(url);
432
+ originalHrefSetter.call(window.location, proxyUrl);
433
+ },
434
+ configurable: true,
435
+ enumerable: true
436
+ });
437
+ }
438
+ } catch (e) {
439
+ // If we can't intercept href setter, that's okay - replace() and assign() are intercepted
440
+ console.warn('Could not intercept location.href setter:', e);
441
+ }
442
+ })();
443
+ </script>
444
+ `;
445
+
446
+ // Inject script before closing </head> or before </body>
447
+ if (html.includes("</head>")) {
448
+ return html.replace("</head>", interceptorScript + "</head>");
449
+ } else if (html.includes("</body>")) {
450
+ return html.replace("</body>", interceptorScript + "</body>");
451
+ } else {
452
+ return interceptorScript + html;
453
+ }
454
+ }
455
+
456
+ function rewriteCssUrls(
457
+ css: string,
458
+ targetHost: string,
459
+ proxyBaseUrl: string,
460
+ targetUrlParam: string
461
+ ): string {
462
+ return css.replace(
463
+ /url\(["']?([^"')]+)["']?\)/gi,
464
+ (match, urlStr) => {
465
+ // Remove quotes if present
466
+ const cleanUrl = urlStr.replace(/^["']|["']$/g, "");
467
+
468
+ if (cleanUrl.startsWith("http://") || cleanUrl.startsWith("https://")) {
469
+ try {
470
+ const urlObj = new URL(cleanUrl);
471
+ if (urlObj.hostname === targetHost) {
472
+ const pathPart = urlObj.pathname === "/" ? "" : urlObj.pathname;
473
+ const searchPart = urlObj.search ? `&${urlObj.search.substring(1)}` : "";
474
+ return `url("${proxyBaseUrl}${pathPart}${targetUrlParam}${searchPart}")`;
475
+ }
476
+ } catch {
477
+ // Invalid URL, keep as is
478
+ }
479
+ return match;
480
+ } else if (cleanUrl.startsWith("//")) {
481
+ try {
482
+ const urlObj = new URL(`https:${cleanUrl}`);
483
+ if (urlObj.hostname === targetHost) {
484
+ const pathPart = urlObj.pathname === "/" ? "" : urlObj.pathname;
485
+ const searchPart = urlObj.search ? `&${urlObj.search.substring(1)}` : "";
486
+ return `url("${proxyBaseUrl}${pathPart}${targetUrlParam}${searchPart}")`;
487
+ }
488
+ } catch {
489
+ // Invalid URL, keep as is
490
+ }
491
+ return match;
492
+ } else if (cleanUrl.startsWith("data:") || cleanUrl.startsWith("blob:")) {
493
+ // Data URLs and blob URLs - keep as is
494
+ return match;
495
+ } else {
496
+ // Relative URL - rewrite to proxy
497
+ const cleanUrlPath = cleanUrl.startsWith("/") ? cleanUrl : `/${cleanUrl}`;
498
+ const [path, search] = cleanUrlPath.split("?");
499
+ const pathPart = path === "/" ? "" : path;
500
+ const searchPart = search ? `&${search}` : "";
501
+ return `url("${proxyBaseUrl}${pathPart}${targetUrlParam}${searchPart}")`;
502
+ }
503
+ }
504
+ );
505
+ }
506
+
507
+ function rewriteJsUrls(
508
+ js: string,
509
+ targetHost: string,
510
+ proxyBaseUrl: string,
511
+ targetUrlParam: string
512
+ ): string {
513
+ // This is a basic implementation - JavaScript URL rewriting is complex
514
+ // For now, we'll handle common patterns like fetch() and XMLHttpRequest
515
+ let processed = js;
516
+
517
+ // Rewrite fetch() calls with relative URLs
518
+ processed = processed.replace(
519
+ /fetch\(["']([^"']+)["']\)/gi,
520
+ (match, urlStr) => {
521
+ if (urlStr.startsWith("http://") || urlStr.startsWith("https://")) {
522
+ try {
523
+ const urlObj = new URL(urlStr);
524
+ if (urlObj.hostname === targetHost) {
525
+ const pathPart = urlObj.pathname === "/" ? "" : urlObj.pathname;
526
+ const searchPart = urlObj.search ? `&${urlObj.search.substring(1)}` : "";
527
+ return `fetch("${proxyBaseUrl}${pathPart}${targetUrlParam}${searchPart}")`;
528
+ }
529
+ } catch {
530
+ // Invalid URL, keep as is
531
+ }
532
+ return match;
533
+ } else if (!urlStr.startsWith("//") && !urlStr.startsWith("data:") && !urlStr.startsWith("blob:")) {
534
+ // Relative URL - rewrite to proxy
535
+ const cleanUrl = urlStr.startsWith("/") ? urlStr : `/${urlStr}`;
536
+ const [path, search] = cleanUrl.split("?");
537
+ const pathPart = path === "/" ? "" : path;
538
+ const searchPart = search ? `&${search}` : "";
539
+ return `fetch("${proxyBaseUrl}${pathPart}${targetUrlParam}${searchPart}")`;
540
+ }
541
+ return match;
542
+ }
543
+ );
544
+
545
+ return processed;
546
+ }
547
+
app/api/proxy/route.ts ADDED
@@ -0,0 +1,557 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+
3
+ export async function GET(req: NextRequest) {
4
+ // Get the target URL from query parameter
5
+ const targetUrl = req.nextUrl.searchParams.get("url");
6
+
7
+ if (!targetUrl) {
8
+ return NextResponse.json(
9
+ { error: "Missing 'url' query parameter" },
10
+ { status: 400 }
11
+ );
12
+ }
13
+
14
+ try {
15
+ // Parse the target URL
16
+ const url = new URL(targetUrl);
17
+
18
+ // Only allow static.hf.space domains for security
19
+ if (!url.hostname.endsWith(".static.hf.space")) {
20
+ return NextResponse.json(
21
+ { error: "Only static.hf.space domains are allowed" },
22
+ { status: 403 }
23
+ );
24
+ }
25
+
26
+ // Use the pathname from the URL query param, or "/" for root
27
+ const targetPath = url.pathname || "/";
28
+
29
+ // Merge query parameters from the request URL with the target URL's search params
30
+ const requestSearchParams = req.nextUrl.searchParams;
31
+ const targetSearchParams = new URLSearchParams(url.search);
32
+
33
+ // Copy all query params except 'url' to the target URL
34
+ requestSearchParams.forEach((value, key) => {
35
+ if (key !== "url") {
36
+ targetSearchParams.set(key, value);
37
+ }
38
+ });
39
+
40
+ const searchString = targetSearchParams.toString();
41
+ const fullTargetUrl = `${url.protocol}//${url.hostname}${targetPath}${searchString ? `?${searchString}` : ""}`;
42
+
43
+ // Fetch the content from the target URL
44
+ const response = await fetch(fullTargetUrl, {
45
+ headers: {
46
+ "User-Agent": "Mozilla/5.0 (compatible; DeepSite/1.0)",
47
+ },
48
+ redirect: "follow", // Follow redirects automatically
49
+ });
50
+
51
+ if (!response.ok) {
52
+ return NextResponse.json(
53
+ { error: `Failed to fetch: ${response.statusText}` },
54
+ { status: response.status }
55
+ );
56
+ }
57
+
58
+ let contentType = response.headers.get("content-type") || "";
59
+ const content = await response.text();
60
+
61
+ // Detect content type from URL path if not properly set
62
+ if (!contentType || contentType === "text/plain" || contentType === "application/octet-stream") {
63
+ const urlPath = url.pathname || "";
64
+ const fileExtension = urlPath.split(".").pop()?.toLowerCase();
65
+
66
+ if (fileExtension === "js") {
67
+ contentType = "application/javascript";
68
+ } else if (fileExtension === "css") {
69
+ contentType = "text/css";
70
+ } else if (fileExtension === "html" || fileExtension === "htm") {
71
+ contentType = "text/html";
72
+ } else if (fileExtension === "json") {
73
+ contentType = "application/json";
74
+ }
75
+ }
76
+
77
+ // Get the base proxy URL
78
+ const requestUrl = new URL(req.url);
79
+ const proxyIndex = requestUrl.pathname.indexOf("/api/proxy");
80
+ const basePath = proxyIndex > 0 ? requestUrl.pathname.substring(0, proxyIndex) : "";
81
+ const proxyBaseUrl = `${basePath}/api/proxy`;
82
+ // Build the base target URL with the path included
83
+ const baseTargetUrl = `https://${url.hostname}${targetPath}`;
84
+ const targetUrlParam = `?url=${encodeURIComponent(baseTargetUrl)}`;
85
+
86
+ // Rewrite URLs in HTML content
87
+ let processedContent = content;
88
+
89
+ if (contentType.includes("text/html")) {
90
+ // Rewrite relative URLs and URLs pointing to the same domain
91
+ processedContent = rewriteUrls(
92
+ content,
93
+ url.hostname,
94
+ proxyBaseUrl,
95
+ baseTargetUrl,
96
+ [] // Empty path segments for root route
97
+ );
98
+
99
+ // Inject script to intercept JavaScript-based navigation
100
+ processedContent = injectNavigationInterceptor(
101
+ processedContent,
102
+ url.hostname,
103
+ proxyBaseUrl,
104
+ targetUrlParam
105
+ );
106
+ } else if (contentType.includes("text/css")) {
107
+ // Rewrite URLs in CSS
108
+ processedContent = rewriteCssUrls(
109
+ content,
110
+ url.hostname,
111
+ proxyBaseUrl,
112
+ targetUrlParam
113
+ );
114
+ } else if (contentType.includes("application/javascript") || contentType.includes("text/javascript")) {
115
+ // For component files and most JavaScript, don't rewrite URLs
116
+ // Only rewrite if needed (for fetch calls, etc.)
117
+ // Most component JavaScript files don't need URL rewriting
118
+ // Only rewrite if the file contains fetch calls with relative URLs
119
+ if (content.includes("fetch(") && content.match(/fetch\(["'][^"']*[^\/]\./)) {
120
+ processedContent = rewriteJsUrls(
121
+ content,
122
+ url.hostname,
123
+ proxyBaseUrl,
124
+ targetUrlParam
125
+ );
126
+ } else {
127
+ // Don't modify component JavaScript files - they should work as-is
128
+ processedContent = content;
129
+ }
130
+ }
131
+
132
+ // Ensure JavaScript files have charset specified
133
+ let finalContentType = contentType;
134
+ if (contentType.includes("javascript") && !contentType.includes("charset")) {
135
+ finalContentType = contentType.includes(";")
136
+ ? contentType + "; charset=utf-8"
137
+ : contentType + "; charset=utf-8";
138
+ }
139
+
140
+ // Return the processed content with appropriate headers
141
+ return new NextResponse(processedContent, {
142
+ status: 200,
143
+ headers: {
144
+ "Content-Type": finalContentType,
145
+ "X-Content-Type-Options": "nosniff",
146
+ // Don't set X-Frame-Options for JavaScript files - they need to execute
147
+ ...(contentType.includes("text/html") && { "X-Frame-Options": "SAMEORIGIN" }),
148
+ // Remove CORS restrictions since we're proxying
149
+ "Access-Control-Allow-Origin": "*",
150
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
151
+ "Access-Control-Allow-Headers": "Content-Type",
152
+ },
153
+ });
154
+ } catch (error: any) {
155
+ console.error("Proxy error:", error);
156
+ return NextResponse.json(
157
+ { error: error.message || "Proxy error occurred" },
158
+ { status: 500 }
159
+ );
160
+ }
161
+ }
162
+
163
+ // Import the helper functions from the catch-all route
164
+ // We'll need to move these to a shared file or duplicate them here
165
+ // For now, let's duplicate them to avoid refactoring
166
+
167
+ function rewriteUrls(
168
+ html: string,
169
+ targetHost: string,
170
+ proxyBaseUrl: string,
171
+ baseTargetUrl: string,
172
+ currentPathSegments: string[] = []
173
+ ): string {
174
+ let processed = html;
175
+
176
+ // Get the current directory path for resolving relative URLs
177
+ const currentDir = currentPathSegments.length > 0
178
+ ? "/" + currentPathSegments.slice(0, -1).join("/") + "/"
179
+ : "/";
180
+
181
+ // Helper function to build proxy URL with path in query parameter
182
+ const buildProxyUrl = (path: string, search?: string) => {
183
+ const fullTargetUrl = `https://${targetHost}${path}`;
184
+ const encodedUrl = encodeURIComponent(fullTargetUrl);
185
+ const searchPart = search ? `&${search}` : "";
186
+ return `${proxyBaseUrl}?url=${encodedUrl}${searchPart}`;
187
+ };
188
+
189
+ // Rewrite relative URLs in href attributes
190
+ processed = processed.replace(
191
+ /href=["']([^"']+)["']/gi,
192
+ (match, urlStr) => {
193
+ if (urlStr.startsWith("http://") || urlStr.startsWith("https://")) {
194
+ // Absolute URL - rewrite if it's the same domain
195
+ try {
196
+ const urlObj = new URL(urlStr);
197
+ if (urlObj.hostname === targetHost) {
198
+ const searchPart = urlObj.search ? urlObj.search.substring(1) : "";
199
+ return `href="${buildProxyUrl(urlObj.pathname, searchPart)}"`;
200
+ }
201
+ } catch {
202
+ // Invalid URL, keep as is
203
+ }
204
+ return match;
205
+ } else if (urlStr.startsWith("//")) {
206
+ // Protocol-relative URL
207
+ try {
208
+ const urlObj = new URL(`https:${urlStr}`);
209
+ if (urlObj.hostname === targetHost) {
210
+ const searchPart = urlObj.search ? urlObj.search.substring(1) : "";
211
+ return `href="${buildProxyUrl(urlObj.pathname, searchPart)}"`;
212
+ }
213
+ } catch {
214
+ // Invalid URL, keep as is
215
+ }
216
+ return match;
217
+ } else if (urlStr.startsWith("#") || urlStr.startsWith("javascript:") || urlStr.startsWith("mailto:") || urlStr.startsWith("tel:")) {
218
+ // Hash links, javascript:, mailto:, tel: - keep as is
219
+ return match;
220
+ } else {
221
+ // Relative URL - resolve it properly
222
+ let resolvedPath: string;
223
+ if (urlStr.startsWith("/")) {
224
+ // Absolute path relative to root
225
+ resolvedPath = urlStr;
226
+ } else if (urlStr.startsWith("components/") || urlStr.startsWith("images/") || urlStr.startsWith("videos/") || urlStr.startsWith("audio/")) {
227
+ // Paths starting with known directories should be treated as absolute from root
228
+ resolvedPath = "/" + urlStr;
229
+ } else {
230
+ // Relative path - resolve relative to current directory
231
+ resolvedPath = currentDir + urlStr;
232
+ // Normalize the path (remove ./ and ../)
233
+ const parts = resolvedPath.split("/");
234
+ const normalized: string[] = [];
235
+ for (const part of parts) {
236
+ if (part === "..") {
237
+ normalized.pop();
238
+ } else if (part !== "." && part !== "") {
239
+ normalized.push(part);
240
+ }
241
+ }
242
+ resolvedPath = "/" + normalized.join("/");
243
+ }
244
+ const [path, search] = resolvedPath.split("?");
245
+ const normalizedPath = path === "/" ? "/" : path;
246
+ return `href="${buildProxyUrl(normalizedPath, search)}"`;
247
+ }
248
+ }
249
+ );
250
+
251
+ // Rewrite relative URLs in src attributes
252
+ processed = processed.replace(
253
+ /src=["']([^"']+)["']/gi,
254
+ (match, urlStr) => {
255
+ if (urlStr.startsWith("http://") || urlStr.startsWith("https://")) {
256
+ try {
257
+ const urlObj = new URL(urlStr);
258
+ if (urlObj.hostname === targetHost) {
259
+ const searchPart = urlObj.search ? urlObj.search.substring(1) : "";
260
+ return `src="${buildProxyUrl(urlObj.pathname, searchPart)}"`;
261
+ }
262
+ } catch {
263
+ // Invalid URL, keep as is
264
+ }
265
+ return match;
266
+ } else if (urlStr.startsWith("//")) {
267
+ try {
268
+ const urlObj = new URL(`https:${urlStr}`);
269
+ if (urlObj.hostname === targetHost) {
270
+ const searchPart = urlObj.search ? urlObj.search.substring(1) : "";
271
+ return `src="${buildProxyUrl(urlObj.pathname, searchPart)}"`;
272
+ }
273
+ } catch {
274
+ // Invalid URL, keep as is
275
+ }
276
+ return match;
277
+ } else if (urlStr.startsWith("data:") || urlStr.startsWith("blob:")) {
278
+ // Data URLs and blob URLs - keep as is
279
+ return match;
280
+ } else {
281
+ // Relative URL - resolve it properly
282
+ let resolvedPath: string;
283
+ if (urlStr.startsWith("/")) {
284
+ // Absolute path relative to root
285
+ resolvedPath = urlStr;
286
+ } else if (urlStr.startsWith("components/") || urlStr.startsWith("images/") || urlStr.startsWith("videos/") || urlStr.startsWith("audio/")) {
287
+ // Paths starting with known directories should be treated as absolute from root
288
+ resolvedPath = "/" + urlStr;
289
+ } else {
290
+ // Relative path - resolve relative to current directory
291
+ resolvedPath = currentDir + urlStr;
292
+ // Normalize the path (remove ./ and ../)
293
+ const parts = resolvedPath.split("/");
294
+ const normalized: string[] = [];
295
+ for (const part of parts) {
296
+ if (part === "..") {
297
+ normalized.pop();
298
+ } else if (part !== "." && part !== "") {
299
+ normalized.push(part);
300
+ }
301
+ }
302
+ resolvedPath = "/" + normalized.join("/");
303
+ }
304
+ const [path, search] = resolvedPath.split("?");
305
+ const normalizedPath = path === "/" ? "/" : path;
306
+ return `src="${buildProxyUrl(normalizedPath, search)}"`;
307
+ }
308
+ }
309
+ );
310
+
311
+ // Rewrite URLs in action attributes (forms)
312
+ processed = processed.replace(
313
+ /action=["']([^"']+)["']/gi,
314
+ (match, urlStr) => {
315
+ if (urlStr.startsWith("http://") || urlStr.startsWith("https://")) {
316
+ try {
317
+ const urlObj = new URL(urlStr);
318
+ if (urlObj.hostname === targetHost) {
319
+ const searchPart = urlObj.search ? urlObj.search.substring(1) : "";
320
+ return `action="${buildProxyUrl(urlObj.pathname, searchPart)}"`;
321
+ }
322
+ } catch {
323
+ // Invalid URL, keep as is
324
+ }
325
+ return match;
326
+ } else if (urlStr.startsWith("//")) {
327
+ try {
328
+ const urlObj = new URL(`https:${urlStr}`);
329
+ if (urlObj.hostname === targetHost) {
330
+ const searchPart = urlObj.search ? urlObj.search.substring(1) : "";
331
+ return `action="${buildProxyUrl(urlObj.pathname, searchPart)}"`;
332
+ }
333
+ } catch {
334
+ // Invalid URL, keep as is
335
+ }
336
+ return match;
337
+ } else {
338
+ // Relative URL - resolve it properly
339
+ let resolvedPath: string;
340
+ if (urlStr.startsWith("/")) {
341
+ resolvedPath = urlStr;
342
+ } else if (urlStr.startsWith("components/") || urlStr.startsWith("images/") || urlStr.startsWith("videos/") || urlStr.startsWith("audio/")) {
343
+ resolvedPath = "/" + urlStr;
344
+ } else {
345
+ resolvedPath = currentDir + urlStr;
346
+ const parts = resolvedPath.split("/");
347
+ const normalized: string[] = [];
348
+ for (const part of parts) {
349
+ if (part === "..") {
350
+ normalized.pop();
351
+ } else if (part !== "." && part !== "") {
352
+ normalized.push(part);
353
+ }
354
+ }
355
+ resolvedPath = "/" + normalized.join("/");
356
+ }
357
+ const [path, search] = resolvedPath.split("?");
358
+ const normalizedPath = path === "/" ? "/" : path;
359
+ return `action="${buildProxyUrl(normalizedPath, search)}"`;
360
+ }
361
+ }
362
+ );
363
+
364
+ // Rewrite URLs in CSS url() functions within style tags
365
+ processed = processed.replace(
366
+ /<style[^>]*>([\s\S]*?)<\/style>/gi,
367
+ (match, cssContent) => {
368
+ const rewrittenCss = rewriteCssUrls(cssContent, targetHost, proxyBaseUrl, baseTargetUrl);
369
+ return match.replace(cssContent, rewrittenCss);
370
+ }
371
+ );
372
+
373
+ return processed;
374
+ }
375
+
376
+ function injectNavigationInterceptor(
377
+ html: string,
378
+ targetHost: string,
379
+ proxyBaseUrl: string,
380
+ targetUrlParam: string
381
+ ): string {
382
+ // Escape strings for safe injection
383
+ const escapeJs = (str: string) => {
384
+ return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
385
+ };
386
+
387
+ const escapedProxyBaseUrl = escapeJs(proxyBaseUrl);
388
+ const escapedTargetUrlParam = escapeJs(targetUrlParam);
389
+ const escapedTargetHost = escapeJs(targetHost);
390
+
391
+ const interceptorScript = `
392
+ <script>
393
+ (function() {
394
+ const proxyBaseUrl = "${escapedProxyBaseUrl}";
395
+ const targetUrlParam = "${escapedTargetUrlParam}";
396
+ const targetHost = "${escapedTargetHost}";
397
+
398
+ // Helper function to convert URL to proxy URL
399
+ function convertToProxyUrl(url) {
400
+ try {
401
+ const urlObj = typeof url === 'string' ? new URL(url, window.location.href) : url;
402
+ if (urlObj.hostname === targetHost || urlObj.hostname === window.location.hostname) {
403
+ const pathPart = urlObj.pathname === "/" ? "" : urlObj.pathname;
404
+ const searchPart = urlObj.search ? urlObj.search.substring(1) : "";
405
+ return proxyBaseUrl + pathPart + targetUrlParam + (searchPart ? "&" + searchPart : "");
406
+ }
407
+ return typeof url === 'string' ? url : urlObj.href;
408
+ } catch (e) {
409
+ // Relative URL or invalid URL
410
+ const urlStr = typeof url === 'string' ? url : url.href || '';
411
+ const pathPart = urlStr.startsWith("/") ? urlStr.split("?")[0] : "/" + urlStr.split("?")[0];
412
+ const searchPart = urlStr.includes("?") ? urlStr.split("?")[1] : "";
413
+ return proxyBaseUrl + pathPart + targetUrlParam + (searchPart ? "&" + searchPart : "");
414
+ }
415
+ }
416
+
417
+ // Intercept window.location.replace()
418
+ const originalReplace = window.location.replace.bind(window.location);
419
+ window.location.replace = function(url) {
420
+ const proxyUrl = convertToProxyUrl(url);
421
+ originalReplace(proxyUrl);
422
+ };
423
+
424
+ // Intercept window.location.assign()
425
+ const originalAssign = window.location.assign.bind(window.location);
426
+ window.location.assign = function(url) {
427
+ const proxyUrl = convertToProxyUrl(url);
428
+ originalAssign(proxyUrl);
429
+ };
430
+
431
+ // Intercept direct assignment to location.href using a proxy
432
+ // Since we can't override window.location, we intercept href assignments
433
+ // Try to intercept href setter, but handle gracefully if it's not configurable
434
+ try {
435
+ let locationHrefDescriptor = Object.getOwnPropertyDescriptor(window.location, 'href');
436
+ if (locationHrefDescriptor && locationHrefDescriptor.set && locationHrefDescriptor.configurable) {
437
+ const originalHrefSetter = locationHrefDescriptor.set;
438
+ Object.defineProperty(window.location, 'href', {
439
+ get: locationHrefDescriptor.get,
440
+ set: function(url) {
441
+ const proxyUrl = convertToProxyUrl(url);
442
+ originalHrefSetter.call(window.location, proxyUrl);
443
+ },
444
+ configurable: true,
445
+ enumerable: true
446
+ });
447
+ }
448
+ } catch (e) {
449
+ // If we can't intercept href setter, that's okay - replace() and assign() are intercepted
450
+ console.warn('Could not intercept location.href setter:', e);
451
+ }
452
+ })();
453
+ </script>
454
+ `;
455
+
456
+ // Inject script before closing </head> or before </body>
457
+ if (html.includes("</head>")) {
458
+ return html.replace("</head>", interceptorScript + "</head>");
459
+ } else if (html.includes("</body>")) {
460
+ return html.replace("</body>", interceptorScript + "</body>");
461
+ } else {
462
+ return interceptorScript + html;
463
+ }
464
+ }
465
+
466
+ function rewriteCssUrls(
467
+ css: string,
468
+ targetHost: string,
469
+ proxyBaseUrl: string,
470
+ targetUrlParam: string
471
+ ): string {
472
+ return css.replace(
473
+ /url\(["']?([^"')]+)["']?\)/gi,
474
+ (match, urlStr) => {
475
+ // Remove quotes if present
476
+ const cleanUrl = urlStr.replace(/^["']|["']$/g, "");
477
+
478
+ if (cleanUrl.startsWith("http://") || cleanUrl.startsWith("https://")) {
479
+ try {
480
+ const urlObj = new URL(cleanUrl);
481
+ if (urlObj.hostname === targetHost) {
482
+ const pathPart = urlObj.pathname === "/" ? "" : urlObj.pathname;
483
+ const searchPart = urlObj.search ? `&${urlObj.search.substring(1)}` : "";
484
+ return `url("${proxyBaseUrl}${pathPart}${targetUrlParam}${searchPart}")`;
485
+ }
486
+ } catch {
487
+ // Invalid URL, keep as is
488
+ }
489
+ return match;
490
+ } else if (cleanUrl.startsWith("//")) {
491
+ try {
492
+ const urlObj = new URL(`https:${cleanUrl}`);
493
+ if (urlObj.hostname === targetHost) {
494
+ const pathPart = urlObj.pathname === "/" ? "" : urlObj.pathname;
495
+ const searchPart = urlObj.search ? `&${urlObj.search.substring(1)}` : "";
496
+ return `url("${proxyBaseUrl}${pathPart}${targetUrlParam}${searchPart}")`;
497
+ }
498
+ } catch {
499
+ // Invalid URL, keep as is
500
+ }
501
+ return match;
502
+ } else if (cleanUrl.startsWith("data:") || cleanUrl.startsWith("blob:")) {
503
+ // Data URLs and blob URLs - keep as is
504
+ return match;
505
+ } else {
506
+ // Relative URL - rewrite to proxy
507
+ const cleanUrlPath = cleanUrl.startsWith("/") ? cleanUrl : `/${cleanUrl}`;
508
+ const [path, search] = cleanUrlPath.split("?");
509
+ const pathPart = path === "/" ? "" : path;
510
+ const searchPart = search ? `&${search}` : "";
511
+ return `url("${proxyBaseUrl}${pathPart}${targetUrlParam}${searchPart}")`;
512
+ }
513
+ }
514
+ );
515
+ }
516
+
517
+ function rewriteJsUrls(
518
+ js: string,
519
+ targetHost: string,
520
+ proxyBaseUrl: string,
521
+ targetUrlParam: string
522
+ ): string {
523
+ // This is a basic implementation - JavaScript URL rewriting is complex
524
+ // For now, we'll handle common patterns like fetch() and XMLHttpRequest
525
+ let processed = js;
526
+
527
+ // Rewrite fetch() calls with relative URLs
528
+ processed = processed.replace(
529
+ /fetch\(["']([^"']+)["']\)/gi,
530
+ (match, urlStr) => {
531
+ if (urlStr.startsWith("http://") || urlStr.startsWith("https://")) {
532
+ try {
533
+ const urlObj = new URL(urlStr);
534
+ if (urlObj.hostname === targetHost) {
535
+ const pathPart = urlObj.pathname === "/" ? "" : urlObj.pathname;
536
+ const searchPart = urlObj.search ? `&${urlObj.search.substring(1)}` : "";
537
+ return `fetch("${proxyBaseUrl}${pathPart}${targetUrlParam}${searchPart}")`;
538
+ }
539
+ } catch {
540
+ // Invalid URL, keep as is
541
+ }
542
+ return match;
543
+ } else if (!urlStr.startsWith("//") && !urlStr.startsWith("data:") && !urlStr.startsWith("blob:")) {
544
+ // Relative URL - rewrite to proxy
545
+ const cleanUrl = urlStr.startsWith("/") ? urlStr : `/${urlStr}`;
546
+ const [path, search] = cleanUrl.split("?");
547
+ const pathPart = path === "/" ? "" : path;
548
+ const searchPart = search ? `&${search}` : "";
549
+ return `fetch("${proxyBaseUrl}${pathPart}${targetUrlParam}${searchPart}")`;
550
+ }
551
+ return match;
552
+ }
553
+ );
554
+
555
+ return processed;
556
+ }
557
+
components/editor/index.tsx CHANGED
@@ -155,7 +155,7 @@ export const AppEditor = ({
155
  }}
156
  />
157
  </div>
158
- <Preview isNew={isNew} />
159
  </main>
160
 
161
  {/* Save Changes Popup */}
 
155
  }}
156
  />
157
  </div>
158
+ <Preview isNew={isNew} namespace={namespace} repoId={repoId} />
159
  </main>
160
 
161
  {/* Save Changes Popup */}
components/editor/preview/index.tsx CHANGED
@@ -16,8 +16,17 @@ import { HistoryNotification } from "../history-notification";
16
  import { api } from "@/lib/api";
17
  import { toast } from "sonner";
18
  import { RefreshCcw, TriangleAlert } from "lucide-react";
19
-
20
- export const Preview = ({ isNew }: { isNew: boolean }) => {
 
 
 
 
 
 
 
 
 
21
  const {
22
  project,
23
  device,
@@ -52,6 +61,9 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
52
  const [throttledHtml, setThrottledHtml] = useState<string>("");
53
  const lastUpdateTimeRef = useRef<number>(0);
54
  const [iframeKey, setIframeKey] = useState(0);
 
 
 
55
 
56
  useEffect(() => {
57
  if (!previewPage && pages.length > 0) {
@@ -63,31 +75,189 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
63
  }
64
  }, [pages, previewPage]);
65
 
 
 
66
  const previewPageData = useMemo(() => {
67
- const found = pages.find((p) => {
68
  const normalizedPagePath = p.path.replace(/^\.?\//, "");
69
  const normalizedPreviewPage = previewPage.replace(/^\.?\//, "");
70
  return normalizedPagePath === normalizedPreviewPage;
71
  });
72
- return found || currentPageData;
73
- }, [pages, previewPage, currentPageData]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
  const injectAssetsIntoHtml = useCallback(
76
- (html: string): string => {
77
  if (!html) return html;
78
 
79
- const cssFiles = pages.filter(
80
  (p) => p.path.endsWith(".css") && p.path !== previewPageData?.path
81
  );
82
- const jsFiles = pages.filter(
83
  (p) => p.path.endsWith(".js") && p.path !== previewPageData?.path
84
  );
85
- const jsonFiles = pages.filter(
86
  (p) => p.path.endsWith(".json") && p.path !== previewPageData?.path
87
  );
88
 
89
  let modifiedHtml = html;
90
 
 
 
 
91
  // Inject all CSS files
92
  if (cssFiles.length > 0) {
93
  const allCssContent = cssFiles
@@ -190,9 +360,33 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
190
  });
191
  }
192
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  return modifiedHtml;
194
  },
195
- [pages, previewPageData?.path]
196
  );
197
 
198
  useEffect(() => {
@@ -201,24 +395,33 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
201
  const timeSinceLastUpdate = now - lastUpdateTimeRef.current;
202
 
203
  if (lastUpdateTimeRef.current === 0 || timeSinceLastUpdate >= 3000) {
204
- const processedHtml = injectAssetsIntoHtml(previewPageData.html);
 
 
 
205
  setThrottledHtml(processedHtml);
206
  lastUpdateTimeRef.current = now;
207
  } else {
208
  const timeUntilNextUpdate = 3000 - timeSinceLastUpdate;
209
  const timer = setTimeout(() => {
210
- const processedHtml = injectAssetsIntoHtml(previewPageData.html);
 
 
 
211
  setThrottledHtml(processedHtml);
212
  lastUpdateTimeRef.current = Date.now();
213
  }, timeUntilNextUpdate);
214
  return () => clearTimeout(timer);
215
  }
216
  }
217
- }, [isNew, previewPageData?.html, injectAssetsIntoHtml]);
218
 
219
  useEffect(() => {
220
  if (!isAiWorking && !globalAiLoading && previewPageData?.html) {
221
- const processedHtml = injectAssetsIntoHtml(previewPageData.html);
 
 
 
222
  setStableHtml(processedHtml);
223
  }
224
  }, [
@@ -227,6 +430,7 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
227
  previewPageData?.html,
228
  injectAssetsIntoHtml,
229
  previewPage,
 
230
  ]);
231
 
232
  useEffect(() => {
@@ -236,7 +440,10 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
236
  !isAiWorking &&
237
  !globalAiLoading
238
  ) {
239
- const processedHtml = injectAssetsIntoHtml(previewPageData.html);
 
 
 
240
  setStableHtml(processedHtml);
241
  }
242
  }, [
@@ -245,6 +452,7 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
245
  isAiWorking,
246
  globalAiLoading,
247
  injectAssetsIntoHtml,
 
248
  ]);
249
 
250
  const setupIframeListeners = () => {
@@ -265,6 +473,17 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
265
  }
266
  };
267
 
 
 
 
 
 
 
 
 
 
 
 
268
  useEffect(() => {
269
  const cleanupListeners = () => {
270
  if (iframeRef?.current?.contentDocument) {
@@ -479,7 +698,7 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
479
  normalizedHref = normalizedHref + ".html";
480
  }
481
 
482
- const isPageExist = pages.some((page) => {
483
  const pagePath = page.path.replace(/^\.?\//, "");
484
  return pagePath === normalizedHref;
485
  });
@@ -537,44 +756,34 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
537
  </div>
538
  ) : (
539
  <>
540
- {!isNew && hasUnsavedChanges && !currentCommit && (
 
 
 
 
 
 
 
 
 
 
541
  <div className="top-0 left-0 right-0 z-20 bg-amber-500/90 backdrop-blur-sm border-b border-amber-600 px-4 py-2 flex items-center justify-between gap-3 text-sm w-full">
542
  <div className="flex items-center gap-2 flex-1">
543
  <TriangleAlert className="size-4 text-amber-900 flex-shrink-0" />
544
  <span className="text-amber-900 font-medium">
545
- Preview with unsaved changes. If you experience redirection
546
- errors, try refreshing the preview.
547
  </span>
548
  </div>
549
  <button
550
  onClick={refreshIframe}
551
- className="px-3 py-1 bg-amber-900 hover:bg-amber-800 text-amber-50 rounded-md font-medium transition-colors whitespace-nowrap flex items-center gap-1.5"
552
  >
553
- <RefreshCcw className="size-4 text-amber-50 flex-shrink-0" />
554
  Refresh
555
  </button>
556
  </div>
557
  )}
558
- {!isNew &&
559
- !hasUnsavedChanges &&
560
- !currentCommit &&
561
- project?.private && (
562
- <div className="top-0 left-0 right-0 z-20 bg-amber-500/90 backdrop-blur-sm border-b border-amber-600 px-4 py-2 flex items-center justify-between gap-3 text-sm w-full">
563
- <div className="flex items-center gap-2 flex-1">
564
- <TriangleAlert className="size-4 text-amber-900 flex-shrink-0" />
565
- <span className="text-amber-900 font-medium">
566
- Private project preview. Some features may not work.
567
- </span>
568
- </div>
569
- <button
570
- onClick={refreshIframe}
571
- className="px-3 py-1 bg-amber-900 hover:bg-amber-800 text-amber-50 rounded-md font-medium transition-colors whitespace-nowrap flex items-center gap-1.5"
572
- >
573
- <RefreshCcw className="size-4 text-amber-50 flex-shrink-0" />
574
- Refresh
575
- </button>
576
- </div>
577
- )}
578
  <iframe
579
  key={iframeKey}
580
  id="preview-iframe"
@@ -587,12 +796,11 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
587
  }
588
  )}
589
  src={
590
- currentCommit
591
- ? `https://${project?.space_id?.replaceAll(
592
- "/",
593
- "-"
594
- )}--rev-${currentCommit.slice(0, 7)}.static.hf.space`
595
- : !isNew && !hasUnsavedChanges && project?.space_id
596
  ? `https://${project.space_id.replaceAll(
597
  "/",
598
  "-"
@@ -600,7 +808,11 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
600
  : undefined
601
  }
602
  srcDoc={
603
- !currentCommit && (isNew || hasUnsavedChanges || project?.private)
 
 
 
 
604
  ? isNew
605
  ? throttledHtml || defaultHTML
606
  : stableHtml
@@ -608,8 +820,10 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
608
  }
609
  onLoad={() => {
610
  if (
611
- !currentCommit &&
612
- (isNew || hasUnsavedChanges || project?.private)
 
 
613
  ) {
614
  if (iframeRef?.current?.contentWindow?.document?.body) {
615
  iframeRef.current.contentWindow.document.body.scrollIntoView({
 
16
  import { api } from "@/lib/api";
17
  import { toast } from "sonner";
18
  import { RefreshCcw, TriangleAlert } from "lucide-react";
19
+ import { Page } from "@/types";
20
+
21
+ export const Preview = ({
22
+ isNew,
23
+ namespace,
24
+ repoId,
25
+ }: {
26
+ isNew: boolean;
27
+ namespace?: string;
28
+ repoId?: string;
29
+ }) => {
30
  const {
31
  project,
32
  device,
 
61
  const [throttledHtml, setThrottledHtml] = useState<string>("");
62
  const lastUpdateTimeRef = useRef<number>(0);
63
  const [iframeKey, setIframeKey] = useState(0);
64
+ const [commitPages, setCommitPages] = useState<Page[]>([]);
65
+ const [isLoadingCommitPages, setIsLoadingCommitPages] = useState(false);
66
+ const prevCommitRef = useRef<string | null>(null);
67
 
68
  useEffect(() => {
69
  if (!previewPage && pages.length > 0) {
 
75
  }
76
  }, [pages, previewPage]);
77
 
78
+ const pagesToUse = currentCommit ? commitPages : pages;
79
+
80
  const previewPageData = useMemo(() => {
81
+ const found = pagesToUse.find((p) => {
82
  const normalizedPagePath = p.path.replace(/^\.?\//, "");
83
  const normalizedPreviewPage = previewPage.replace(/^\.?\//, "");
84
  return normalizedPagePath === normalizedPreviewPage;
85
  });
86
+ return found || (pagesToUse.length > 0 ? pagesToUse[0] : currentPageData);
87
+ }, [pagesToUse, previewPage, currentPageData]);
88
+
89
+ // Fetch commit pages when currentCommit changes
90
+ useEffect(() => {
91
+ if (currentCommit && namespace && repoId) {
92
+ setIsLoadingCommitPages(true);
93
+ api
94
+ .get(`/me/projects/${namespace}/${repoId}/commits/${currentCommit}`)
95
+ .then((res) => {
96
+ if (res.data.ok) {
97
+ setCommitPages(res.data.pages);
98
+ // Set preview page to index.html if available
99
+ const indexPage = res.data.pages.find(
100
+ (p: Page) =>
101
+ p.path === "index.html" || p.path === "index" || p.path === "/"
102
+ );
103
+ if (indexPage) {
104
+ setPreviewPage(indexPage.path);
105
+ }
106
+ // Refresh iframe to show commit version
107
+ setIframeKey((prev) => prev + 1);
108
+ }
109
+ })
110
+ .catch((err) => {
111
+ toast.error(
112
+ err.response?.data?.error || "Failed to fetch commit pages"
113
+ );
114
+ })
115
+ .finally(() => {
116
+ setIsLoadingCommitPages(false);
117
+ });
118
+ } else if (!currentCommit && prevCommitRef.current !== null) {
119
+ // Only clear commitPages when transitioning from a commit to no commit
120
+ setCommitPages([]);
121
+ }
122
+ prevCommitRef.current = currentCommit;
123
+ }, [currentCommit, namespace, repoId]);
124
+
125
+ // Create navigation interception script
126
+ const createNavigationScript = useCallback((availablePages: Page[]) => {
127
+ const pagePaths = availablePages.map((p) => p.path.replace(/^\.?\//, ""));
128
+ return `
129
+ (function() {
130
+ const availablePages = ${JSON.stringify(pagePaths)};
131
+
132
+ function normalizePath(path) {
133
+ let normalized = path.replace(/^\.?\//, "");
134
+ if (normalized === "" || normalized === "/") {
135
+ normalized = "index.html";
136
+ }
137
+ const hashIndex = normalized.indexOf("#");
138
+ if (hashIndex !== -1) {
139
+ normalized = normalized.substring(0, hashIndex);
140
+ }
141
+ if (!normalized.includes(".")) {
142
+ normalized = normalized + ".html";
143
+ }
144
+ return normalized;
145
+ }
146
+
147
+ function handleNavigation(url) {
148
+ if (!url) return;
149
+
150
+ // Handle hash-only navigation
151
+ if (url.startsWith("#")) {
152
+ const targetElement = document.querySelector(url);
153
+ if (targetElement) {
154
+ targetElement.scrollIntoView({ behavior: "smooth" });
155
+ }
156
+ // Search in shadow DOM
157
+ const searchInShadows = (root) => {
158
+ const elements = root.querySelectorAll("*");
159
+ for (const el of elements) {
160
+ if (el.shadowRoot) {
161
+ const found = el.shadowRoot.querySelector(url);
162
+ if (found) {
163
+ found.scrollIntoView({ behavior: "smooth" });
164
+ return;
165
+ }
166
+ searchInShadows(el.shadowRoot);
167
+ }
168
+ }
169
+ };
170
+ searchInShadows(document);
171
+ return;
172
+ }
173
+
174
+ // Handle external URLs
175
+ if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//")) {
176
+ window.open(url, "_blank");
177
+ return;
178
+ }
179
+
180
+ const normalizedPath = normalizePath(url);
181
+ if (availablePages.includes(normalizedPath)) {
182
+ // Dispatch custom event to notify parent
183
+ window.parent.postMessage({ type: 'navigate', path: normalizedPath }, '*');
184
+ } else {
185
+ console.warn('Page not found:', normalizedPath);
186
+ }
187
+ }
188
+
189
+ // Intercept window.location methods
190
+ const originalAssign = window.location.assign;
191
+ const originalReplace = window.location.replace;
192
+
193
+ window.location.assign = function(url) {
194
+ handleNavigation(url);
195
+ };
196
+
197
+ window.location.replace = function(url) {
198
+ handleNavigation(url);
199
+ };
200
+
201
+ // Intercept window.location.href setter
202
+ try {
203
+ let currentHref = window.location.href;
204
+ Object.defineProperty(window.location, 'href', {
205
+ get: function() {
206
+ return currentHref;
207
+ },
208
+ set: function(url) {
209
+ handleNavigation(url);
210
+ },
211
+ configurable: true
212
+ });
213
+ } catch (e) {
214
+ // Fallback: use proxy if defineProperty fails
215
+ console.warn('Could not intercept location.href:', e);
216
+ }
217
+
218
+ // Intercept link clicks
219
+ document.addEventListener('click', function(e) {
220
+ const anchor = e.target.closest('a');
221
+ if (anchor && anchor.href) {
222
+ const href = anchor.getAttribute('href');
223
+ if (href && !href.startsWith('http://') && !href.startsWith('https://') && !href.startsWith('//') && !href.startsWith('mailto:') && !href.startsWith('tel:')) {
224
+ e.preventDefault();
225
+ handleNavigation(href);
226
+ }
227
+ }
228
+ }, true);
229
+
230
+ // Intercept form submissions
231
+ document.addEventListener('submit', function(e) {
232
+ const form = e.target;
233
+ if (form.action && !form.action.startsWith('http://') && !form.action.startsWith('https://') && !form.action.startsWith('//')) {
234
+ e.preventDefault();
235
+ handleNavigation(form.action);
236
+ }
237
+ }, true);
238
+ })();
239
+ `;
240
+ }, []);
241
 
242
  const injectAssetsIntoHtml = useCallback(
243
+ (html: string, pagesToUse: Page[] = pages): string => {
244
  if (!html) return html;
245
 
246
+ const cssFiles = pagesToUse.filter(
247
  (p) => p.path.endsWith(".css") && p.path !== previewPageData?.path
248
  );
249
+ const jsFiles = pagesToUse.filter(
250
  (p) => p.path.endsWith(".js") && p.path !== previewPageData?.path
251
  );
252
+ const jsonFiles = pagesToUse.filter(
253
  (p) => p.path.endsWith(".json") && p.path !== previewPageData?.path
254
  );
255
 
256
  let modifiedHtml = html;
257
 
258
+ // Inject navigation script for srcDoc
259
+ const navigationScript = createNavigationScript(pagesToUse);
260
+
261
  // Inject all CSS files
262
  if (cssFiles.length > 0) {
263
  const allCssContent = cssFiles
 
360
  });
361
  }
362
 
363
+ // Inject navigation script early in the document
364
+ if (navigationScript) {
365
+ // Try to inject right after <head> or <body> opening tag
366
+ if (modifiedHtml.includes("<head>")) {
367
+ modifiedHtml = modifiedHtml.replace(
368
+ "<head>",
369
+ `<head>\n<script>${navigationScript}</script>`
370
+ );
371
+ } else if (modifiedHtml.includes("<body>")) {
372
+ modifiedHtml = modifiedHtml.replace(
373
+ "<body>",
374
+ `<body>\n<script>${navigationScript}</script>`
375
+ );
376
+ } else if (modifiedHtml.includes("</body>")) {
377
+ modifiedHtml = modifiedHtml.replace(
378
+ "</body>",
379
+ `<script>${navigationScript}</script>\n</body>`
380
+ );
381
+ } else {
382
+ modifiedHtml =
383
+ `<script>${navigationScript}</script>\n` + modifiedHtml;
384
+ }
385
+ }
386
+
387
  return modifiedHtml;
388
  },
389
+ [pages, previewPageData?.path, createNavigationScript]
390
  );
391
 
392
  useEffect(() => {
 
395
  const timeSinceLastUpdate = now - lastUpdateTimeRef.current;
396
 
397
  if (lastUpdateTimeRef.current === 0 || timeSinceLastUpdate >= 3000) {
398
+ const processedHtml = injectAssetsIntoHtml(
399
+ previewPageData.html,
400
+ pagesToUse
401
+ );
402
  setThrottledHtml(processedHtml);
403
  lastUpdateTimeRef.current = now;
404
  } else {
405
  const timeUntilNextUpdate = 3000 - timeSinceLastUpdate;
406
  const timer = setTimeout(() => {
407
+ const processedHtml = injectAssetsIntoHtml(
408
+ previewPageData.html,
409
+ pagesToUse
410
+ );
411
  setThrottledHtml(processedHtml);
412
  lastUpdateTimeRef.current = Date.now();
413
  }, timeUntilNextUpdate);
414
  return () => clearTimeout(timer);
415
  }
416
  }
417
+ }, [isNew, previewPageData?.html, injectAssetsIntoHtml, pagesToUse]);
418
 
419
  useEffect(() => {
420
  if (!isAiWorking && !globalAiLoading && previewPageData?.html) {
421
+ const processedHtml = injectAssetsIntoHtml(
422
+ previewPageData.html,
423
+ pagesToUse
424
+ );
425
  setStableHtml(processedHtml);
426
  }
427
  }, [
 
430
  previewPageData?.html,
431
  injectAssetsIntoHtml,
432
  previewPage,
433
+ pagesToUse,
434
  ]);
435
 
436
  useEffect(() => {
 
440
  !isAiWorking &&
441
  !globalAiLoading
442
  ) {
443
+ const processedHtml = injectAssetsIntoHtml(
444
+ previewPageData.html,
445
+ pagesToUse
446
+ );
447
  setStableHtml(processedHtml);
448
  }
449
  }, [
 
452
  isAiWorking,
453
  globalAiLoading,
454
  injectAssetsIntoHtml,
455
+ pagesToUse,
456
  ]);
457
 
458
  const setupIframeListeners = () => {
 
473
  }
474
  };
475
 
476
+ // Listen for navigation messages from iframe
477
+ useEffect(() => {
478
+ const handleMessage = (event: MessageEvent) => {
479
+ if (event.data?.type === "navigate" && event.data?.path) {
480
+ setPreviewPage(event.data.path);
481
+ }
482
+ };
483
+ window.addEventListener("message", handleMessage);
484
+ return () => window.removeEventListener("message", handleMessage);
485
+ }, [setPreviewPage]);
486
+
487
  useEffect(() => {
488
  const cleanupListeners = () => {
489
  if (iframeRef?.current?.contentDocument) {
 
698
  normalizedHref = normalizedHref + ".html";
699
  }
700
 
701
+ const isPageExist = pagesToUse.some((page) => {
702
  const pagePath = page.path.replace(/^\.?\//, "");
703
  return pagePath === normalizedHref;
704
  });
 
756
  </div>
757
  ) : (
758
  <>
759
+ {isLoadingCommitPages && (
760
+ <div className="top-0 left-0 right-0 z-20 bg-blue-500/90 backdrop-blur-sm border-b border-blue-600 px-4 py-2 flex items-center justify-center gap-3 text-sm w-full">
761
+ <div className="flex items-center gap-2">
762
+ <AiLoading
763
+ text="Loading commit version..."
764
+ className="flex-row"
765
+ />
766
+ </div>
767
+ </div>
768
+ )}
769
+ {!isNew && !currentCommit && (
770
  <div className="top-0 left-0 right-0 z-20 bg-amber-500/90 backdrop-blur-sm border-b border-amber-600 px-4 py-2 flex items-center justify-between gap-3 text-sm w-full">
771
  <div className="flex items-center gap-2 flex-1">
772
  <TriangleAlert className="size-4 text-amber-900 flex-shrink-0" />
773
  <span className="text-amber-900 font-medium">
774
+ Preview version of the project. Try refreshing the preview if
775
+ you experience any issues.
776
  </span>
777
  </div>
778
  <button
779
  onClick={refreshIframe}
780
+ className="cursor-pointer text-xs px-3 py-1 bg-amber-900 hover:bg-amber-800 text-amber-50 rounded-md font-medium transition-colors whitespace-nowrap flex items-center gap-1.5"
781
  >
782
+ <RefreshCcw className="size-3 text-amber-50 flex-shrink-0" />
783
  Refresh
784
  </button>
785
  </div>
786
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
787
  <iframe
788
  key={iframeKey}
789
  id="preview-iframe"
 
796
  }
797
  )}
798
  src={
799
+ !currentCommit &&
800
+ !isNew &&
801
+ !hasUnsavedChanges &&
802
+ project?.space_id &&
803
+ !project?.private
 
804
  ? `https://${project.space_id.replaceAll(
805
  "/",
806
  "-"
 
808
  : undefined
809
  }
810
  srcDoc={
811
+ currentCommit
812
+ ? commitPages.length > 0 && previewPageData?.html
813
+ ? injectAssetsIntoHtml(previewPageData.html, commitPages)
814
+ : defaultHTML
815
+ : isNew || hasUnsavedChanges || project?.private
816
  ? isNew
817
  ? throttledHtml || defaultHTML
818
  : stableHtml
 
820
  }
821
  onLoad={() => {
822
  if (
823
+ currentCommit ||
824
+ isNew ||
825
+ hasUnsavedChanges ||
826
+ project?.private
827
  ) {
828
  if (iframeRef?.current?.contentWindow?.document?.body) {
829
  iframeRef.current.contentWindow.document.body.scrollIntoView({