diff --git a/packages/app/src/components/canvas/LiveArtboard.tsx b/packages/app/src/components/canvas/LiveArtboard.tsx
index b3e7ce7..b071710 100644
--- a/packages/app/src/components/canvas/LiveArtboard.tsx
+++ b/packages/app/src/components/canvas/LiveArtboard.tsx
@@ -124,11 +124,13 @@ export function LiveArtboard({
pendingStylesFetchRef.current = selectedComponentId;
}
onReady?.();
- // Start a 4-second timer: if React never commits, this is a static page.
+ // Start an 8-second timer: if React never commits (including the
+ // 2-second retroactive captureExistingTree safety net), this is
+ // genuinely a static page.
if (staticTimerRef.current) clearTimeout(staticTimerRef.current);
staticTimerRef.current = setTimeout(() => {
if (!hasReactRef.current) onStaticPageDetected?.();
- }, 4000);
+ }, 8000);
break;
case 'FIBER_TREE_UPDATE':
// Mark that this iframe contains a live React app.
diff --git a/packages/live-sdk/src/hook.ts b/packages/live-sdk/src/hook.ts
index 88c3356..8b64444 100644
--- a/packages/live-sdk/src/hook.ts
+++ b/packages/live-sdk/src/hook.ts
@@ -442,9 +442,18 @@ function installFiberHook(): void {
// Current route first
addRoute(window.location.pathname, document.title || undefined);
- // Scan real elements
+ // Scan real elements — accept root-relative paths regardless of origin
+ // so that CLI-proxied pages (where links still point to the original domain)
+ // are handled correctly alongside direct same-origin connections.
document.querySelectorAll('a[href]').forEach((a) => {
try {
+ const rawHref = (a.getAttribute('href') ?? '').trim();
+ // Root-relative paths are always valid routes.
+ if (rawHref.startsWith('/')) {
+ addRoute(rawHref, a.textContent ?? undefined);
+ return;
+ }
+ // Absolute URLs — only add if same-origin (direct connection).
const url = new URL(a.href, window.location.href);
if (url.origin !== window.location.origin) return;
addRoute(url.pathname, a.textContent ?? undefined);
diff --git a/packages/renderer/src/fiber-hook.ts b/packages/renderer/src/fiber-hook.ts
index 7354cfb..05dc618 100644
--- a/packages/renderer/src/fiber-hook.ts
+++ b/packages/renderer/src/fiber-hook.ts
@@ -868,14 +868,25 @@ export function buildProxyFiberHookScript(): string {
} catch (e) { /* Next.js not present */ }
})();
- // ③ same-origin links from the rendered DOM
+ // ③ links — relative paths AND same-origin absolute URLs.
+ // When running through the CLI proxy, the page's links still point to the
+ // original domain (e.g. intraining.com), not the proxy (localhost:4170).
+ // A strict same-origin check would reject all of them. Instead, accept
+ // any href that is already a root-relative path ("/courses"), plus
+ // same-origin absolute URLs for direct (non-proxied) connections.
var anchors = document.querySelectorAll('a[href]');
for (var i = 0; i < anchors.length; i++) {
try {
+ var rawHref = (anchors[i].getAttribute('href') || '').trim();
+ // Root-relative paths — always valid routes regardless of origin
+ if (rawHref.charAt(0) === '/') {
+ addRoute(rawHref, (anchors[i].textContent || '').trim() || undefined);
+ continue;
+ }
+ // Absolute URLs — only add if same-origin (direct connection)
var url = new URL(anchors[i].href, window.location.href);
if (url.origin !== window.location.origin) continue;
- // Skip hash-only links
- if (!url.pathname || url.hash && !url.pathname) continue;
+ if (!url.pathname || (url.hash && !url.pathname)) continue;
addRoute(url.pathname, (anchors[i].textContent || '').trim() || undefined);
} catch (e) { /* skip malformed hrefs */ }
}
@@ -899,12 +910,68 @@ export function buildProxyFiberHookScript(): string {
if (routes.length > 0) post({ type: 'ROUTES_DISCOVERED', routes: routes });
}
+ // ── Retroactive fiber capture ─────────────────────────────────────────────
+ // onCommitFiberRoot only fires on FUTURE commits. If React already completed
+ // its first render (hydration) before our hook script was evaluated, we miss
+ // the initial tree entirely and the 4-second static-page timer fires falsely.
+ //
+ // Fix: scan common React root containers for __reactFiber$ DOM annotations
+ // that React writes onto every host element. Walk the found fiber up to the
+ // HostRoot (fiber.return chain) and serialize it exactly as onCommitFiberRoot
+ // does. Two attempts cover the two race modes:
+ // Attempt 1 (immediate) – hook ran after hydration; DOM is populated now.
+ // Attempt 2 (2 s delay) – hook ran before hydration or Suspense deferred.
+ function captureExistingTree() {
+ // Find any DOM element that React has annotated with a fiber reference.
+ var candidates = [
+ document.getElementById('__next'),
+ document.getElementById('root'),
+ document.getElementById('app'),
+ document.body,
+ ];
+
+ var fiber = null;
+ for (var ci = 0; ci < candidates.length; ci++) {
+ var el = candidates[ci];
+ if (!el) continue;
+ var keys = Object.keys(el);
+ for (var ki = 0; ki < keys.length; ki++) {
+ if (keys[ki].indexOf('__reactFiber$') === 0) {
+ fiber = el[keys[ki]];
+ break;
+ }
+ }
+ if (fiber) break;
+ }
+
+ if (!fiber) return; // React not yet mounted on any known container.
+
+ // Walk up to the HostRoot (the sentinel fiber React builds the tree from).
+ var f = fiber;
+ while (f.return) f = f.return;
+
+ // Rebuild maps and serialize — identical to what onCommitFiberRoot does.
+ nodeMap = {};
+ fiberMap = new WeakMap();
+ var tree = serializeFiber(f, '');
+ if (!tree) return; // Nothing serializable yet.
+
+ reapplyOverrides();
+ post({ type: 'FIBER_TREE_UPDATE', root: tree });
+ if (selectedNodeId) updateHighlight();
+ }
+
// ── Ready signal (includes root font size for rem→px normalisation) ────────
var rootFontSizePx = parseFloat(
window.getComputedStyle(document.documentElement).getPropertyValue('font-size') || '16'
) || 16;
post({ type: 'READY', rootFontSizePx: rootFontSizePx });
+ // Attempt 1: capture already-mounted React tree immediately (handles the
+ // common case where hydration completed before the hook script ran).
+ captureExistingTree();
setTimeout(discoverRoutes, 800);
+ // Attempt 2: safety-net capture 2 s later for deferred hydration / Suspense.
+ setTimeout(captureExistingTree, 2000);
// Re-discover on SPA navigation (Next.js App Router fires popstate on push)
window.addEventListener('popstate', function() { setTimeout(discoverRoutes, 100); });
})();`;