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

Running a multi-tool engineering application across standalone browser pages, OS shell iframe containers, and native VS Code webview panels usually leads to fragmented routing logic and duplicated code paths. By standardizing on a single query contract (?view=<mode>), we created a unified initialization flow. All surfaces set this query parameter, while individual tools maintain URL state via history.replaceState without polluting browser back-button history.
Surface TargetSignal Transport StrategyMode Initialization FlowHistory Impact
Standalone PageDirect URL query string (?view=editor)Direct DOM tab activation on mountStandard page load history
OS Shell WindowAppended to <iframe> src URLDynamic iframe.src update on view togglePreserved (no shell navigation)
VS Code Webviewwindow.__GINEXYS_INITIAL_MODE__Global override with query fallbackIn-memory webview state

Host environment disparities break unified tool initialization

Building our multi-tool developer suite meant deploying the exact same set of tools (PDF processor, table formatter, schema editor) across three host environments:

  1. Canonical standalone pages: Deep-linkable web pages (https://ginexys.com/tools/pdf/?view=editor).
  2. OS desktop shell: Multi-window layout running sub-tools inside <iframe> elements.
  3. VS Code extension: Integrated webview panels embedded inside the IDE.
Without a single unified routing contract, we would have been forced to write custom initialization hooks and messaging bridge scripts for each environment, creating three separate entry points per tool.

Standalone Browser Page         OS Shell Window Manager      VS Code Webview Panel
(URL Query: ?view=editor)     (iframe.src: ?view=editor)    (window.__GINEXYS_MODE__)
            \                             |                             /
             \                            |                            /
              v                           v                           v
            +-----------------------------------------------------------+
            |                   Universal Mode Reader                   |
            +-----------------------------------------------------------+
                                          |
                                          v
                         [Activate Corresponding Tab DOM]
                                          |
                                          v
                         [Sync URL via history.replaceState]

Transient state tracking pollutes browser history and routing

Attempting to sync tool modes across window manager iframe boundaries commonly creates two issues:

  1. History stack pollution (pushState abuse): Calling history.pushState every time a user switches windows or toggles a tool tab fills the browser history with dozens of transient UI interactions. Clicking "Back" forces the user through a tour of previous window clicks rather than leaving the page.
  2. CDN path loss on redirects: Using HTTP 302 redirects on deep-link routes (/app/pdf/editor/ -> /index.html) strips the original path from window.location.pathname before client-side JavaScript can parse the intended target mode.

Standardizing query parameters simplifies cross-surface integration

Client-side query parsers resolve modes independently

Every tool executes a single initialization check on startup, requiring zero knowledge of its host container:
// Universal mode reader executed by all tools on mount
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();
}

Iframe source updates align window manager coordinates

When the OS shell opens or updates a window mode, it updates the target iframe's src attribute:
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; } }

State replacements prevent back-button pollution

To keep the main browser location bar updated without creating spam history entries, the shell calls replaceState:
function syncUrl(appId, view) {
  const slug = PUBLIC_SLUG[appId];
  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); } }

Edge routing rules preserve path parameters for client parsing

To support clean path-style deep links (/app/pdf/editor/), Cloudflare Pages is configured with a 200 rewrite rule:
/app/*  /index.html  200

Because the status code is 200 rather than 302, the browser location bar retains /app/pdf/editor/, allowing client-side JS to parse window.location.pathname on mount and open the appropriate tool.

Rule of thumb: Use history.replaceState when updating URLs to reflect internal UI focus or panel toggles. Reserve history.pushState strictly for major user-driven page navigations.
Read this post in the full Engineering Journal →