Engineering Journal
Ginexys
Ginexys

Deeplinks Postmortem

2026-05-30

What I got wrong shipping web-component shells for tool sub-pages

TLDR

I shipped three web components (gx-pdf-shell, gx-tafne-shell, gx-schema-shell) to deduplicate 10 sub-page HTMLs into thin SEO landing pages. They worked until they didn't. The shell's document.body.innerHTML swap raced with the canonical app's init and broke extraction, export, and modals. This week I tore them out, replaced the architecture with iframe wrappers, and discovered the VSCode extension had already solved the problem the right way three weeks earlier.

What the wrapper was supposed to do

Each tool ships at a canonical URL. PDF processor lives at /tools/pdf-processor/. Schema editor at /tools/schema-editor/. TAFNE at /tools/table-formatter/.

For SEO, each tool also has mode-specific sub-pages. /tools/pdf-processor/editor/ indexes for "PDF code editor." /tools/pdf-processor/visual-diff/ indexes for "PDF diff." /tools/table-formatter/lab-mode/ indexes for "table validation pipeline."

These sub-pages need unique <head> metadata (title, OG image, schema.org breadcrumbs) but should not duplicate the tool itself.

My solution: <gx-pdf-shell active-view="editor"> web component. Each sub-page is a 40-line HTML with unique metadata and one custom element. The element fetches the canonical index.html, parses it, injects head assets, then sets document.body.innerHTML to the canonical body. Then activates the right tab.

Tight on lines. Cute. Wrong.

How it broke

The shell ran document.body.innerHTML = doc.body.innerHTML. This wiped the body that app.js had already attached jQuery handlers to.

But app.js was still loading. It uses $(() => {...}) for init. Sometimes the callback fired against the OLD body, sometimes against the NEW one. Sometimes it never fired because the new body's module script reference loaded before the old one finished bootstrapping global state.

Symptoms: file dialog opens because that handler lives in inline body script. Tabs switch because the click delegation runs at document. But click "Open PDF" and nothing extracts. Click "Export" and the dropdown does nothing. Click a Pro feature and the modal call hits undefined.

No console errors. Nothing visibly broken. Just a tool that looks normal and does nothing.

Why I missed it for three weeks

The tests I ran were always at the canonical URL. /tools/pdf-processor/ skipped the wrapper. Worked fine.

Then I added a Vite plugin to redirect bare /tools/pdf-processor/ to /editor/, because PDF.js workers were pinging the root URL and Vite was returning HTML instead of JS. The comment said "Prevents Vite HMR ping failures." Real concern at the time.

That redirect routed every user visit through the wrapper. My standalone-canonical test path stopped existing.

What I threw away

The three web component files. 8KB of code that was thoughtful, isomorphic, light-DOM, no shadow root, even handled URL sync on tab switch. All deleted.

Also threw away the assumption that "one canonical app loaded into many SEO pages" needs a web component. It doesn't. It needs an iframe.

What replaced it

The VSCode extension had been doing it right since May 20. Each tool's index.html loads as-is into a webview. html-rewriter.ts adjusts paths for the webview origin. pendingMode injects window.__GINEXYS_INITIAL_MODE__ before scripts run. Tool reads the global on init and activates the right mode.

I mirrored this on the web. Each sub-page becomes a thin HTML with unique SEO <head> and a single full-viewport <iframe src="/tools/pdf-processor/?view=editor">. The canonical app reads URLSearchParams and activates the requested mode. No DOM swap, no race, no wrapper.

OS shell deep links use the same primitive. ginexys.com/app/pdf/editor/ opens the OS desktop with PDF tool window in editor mode. The window's iframe src is /tools/pdf-processor/?view=editor. Same contract, two callers.

What survived

The unique per-mode SEO metadata. The tool icons in the dock. The IPC bus. The Pro waitlist Supabase table. The free-tier gate. None of this needed to change.

The standalone canonical tool also stayed exactly as it was. Bookmarks to /tools/pdf-processor/ still work. Google rankings unchanged.

Lesson

When you have one source of truth and many SEO surfaces, the question to ask is not "what component can I write to deduplicate this." The question is "what isolation boundary already exists in the browser that I can reuse."

The iframe is the answer. It always was. I just thought I was smart enough to skip it.

Read this post in the full Engineering Journal →