One URL Convention for Three Surfaces: How to Deep Link a Multi-Tool Web App Without Forking the Tools
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 Target | Signal Transport Strategy | Mode Initialization Flow | History Impact |
|---|---|---|---|
| Standalone Page | Direct URL query string (?view=editor) | Direct DOM tab activation on mount | Standard page load history |
| OS Shell Window | Appended to <iframe> src URL | Dynamic iframe.src update on view toggle | Preserved (no shell navigation) |
| VS Code Webview | window.__GINEXYS_INITIAL_MODE__ | Global override with query fallback | In-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:
- Canonical standalone pages: Deep-linkable web pages (
https://ginexys.com/tools/pdf/?view=editor). - OS desktop shell: Multi-window layout running sub-tools inside
<iframe>elements. - VS Code extension: Integrated webview panels embedded inside the IDE.
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:
- History stack pollution (
pushStateabuse): Callinghistory.pushStateevery 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. - CDN path loss on redirects: Using HTTP 302 redirects on deep-link routes (
/app/pdf/editor/->/index.html) strips the original path fromwindow.location.pathnamebefore 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'ssrc 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 callsreplaceState:
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: Usehistory.replaceStatewhen updating URLs to reflect internal UI focus or panel toggles. Reservehistory.pushStatestrictly for major user-driven page navigations.