Engineering Journal
Ginexys
Ginexys

One URL Convention for Three Surfaces: How to Deep Link a Multi-Tool Web App Without Forking the Tools

2026-05-31

TLDR

Three surfaces, a standalone page, an OS shell iframe, and a VS Code webview, need to open the same tool in a specific mode. The cleanest contract: ?view=<mode> as the universal signal. Each tool reads it once on init. The OS shell appends it to iframe src. The VS Code extension injects it as a global before scripts run. The URL bar reflects the active mode via replaceState. One convention, no per-surface fork.


The Problem

A multi-tool web app often ships tools at canonical URLs that work standalone, and also embeds those same tools in a shell or container for a richer experience. Each surface needs to open a tool in a specific mode (editor, diff, analyzer). The naive approach is per-surface initialization code: the shell injects a global, the VS Code extension injects a different global, the standalone page reads yet another mechanism. The tool ends up with three init paths and three places to maintain.

The correct approach: one signal that all three surfaces write and one reader that handles all three.


The Universal Signal: ?view=

Each tool reads a single query parameter on init:

const queryView = new URLSearchParams(location.search).get('view');
if (queryView) {
  const btn = document.querySelector(.tab-btn[data-view="${queryView}"]);
  if (btn && !btn.disabled) btn.click();
}

This is the entire mode-routing path. No per-surface check, no global variable, no postMessage listener needed at the tool level.

Three callers write to this contract:

The global-first, query-fallback order lets the extension override the URL signal if needed without changing the URL.

OS Shell: Appending the View to iframe src

The shell's openApp function accepts an opts argument:

function buildToolSrc(src, view) {
  if (!view) return src;
  const sep = src.includes('?') ? '&' : '?';
  return src + sep + 'view=' + encodeURIComponent(view);
}

function openApp(app, opts) { if (windowMap.has(app.id)) { const win = windowMap.get(app.id); if (opts?.view && win.currentView !== opts.view) { const iframe = win.el.querySelector('iframe'); if (iframe) iframe.src = buildToolSrc(app.src, opts.view); win.currentView = opts.view; } bringToFront(app.id); return; } // new window creation: buildToolSrc(app.src, opts?.view) }

A second openApp call with a different view re-points the iframe src. Same window, new mode. No DOM swap, no re-injection of globals.


Bidirectional URL Sync

The URL bar should reflect the active tool and mode. This is implemented with replaceState, not pushState.

function syncUrl(appId, view) {
  const slug = PUBLIC_SLUG[appId]; // internal id → public url segment
  let path;
  if (!slug) path = '/';
  else if (view) path = '/app/' + slug + '/' + view + '/';
  else path = '/app/' + slug + '/';

if (location.pathname !== path) { history.replaceState({ appId, view }, '', path); } }

replaceState not pushState because every focus change updates the URL. pushState would create a history entry for every dock click, turning the back button into a tour through every window activation. replaceState keeps the URL accurate without polluting history. Only real navigations (direct URL entry, back/forward) create history entries.

Real navigation is handled by the popstate listener:

window.addEventListener('popstate', () => {
  const parsed = parseDeepLink(location.pathname);
  if (!parsed) return;
  const appId = ALIAS_MAP[parsed.app] || parsed.app;
  const app = APPS.find(a => a.id === appId);
  if (app) openApp(app, { view: parsed.view });
});

Server-Side Routing for Path-Form Deep Links

Path-form deep links (/app/pdf/editor/) need the server to serve the shell HTML without a redirect. A redirect loses the path before JavaScript runs.

Cloudflare Pages _redirects:

/app/*  /index.html  200

Status 200 is critical. A 302 would redirect to /index.html at a different URL, losing the original path from window.location.pathname before parseDeepLink can read it.


What Stays Unchanged

Standalone canonical URLs work exactly as before. Existing bookmarks to /tools/pdf-processor/ open the standalone tool with no shell, no chrome, no query parameter. The VS Code extension is unaffected. It loads the canonical index.html into a webview and injects its own global. The ?view= reader checks the global first and falls back to the query string, so the extension's path wins when present.

Three surfaces, one convention, no per-surface tool fork.

Read this post in the full Engineering Journal →