Engineering Journal
Ginexys
Ginexys

Postmortem: The Web Component That Swapped the DOM and Broke Everything Silently

2026-05-31

TLDR

To deduplicate tool HTML across ten SEO landing pages while retaining unique <head> metadata, we initially built a custom web component (<gx-app-shell>) that fetched canonical HTML and overwrote document.body.innerHTML. This introduced non-deterministic race conditions: scripts executed and attached event listeners against initial DOM elements before the component wiped and replaced the body. We solved this by replacing the custom element with a 14-line <iframe> wrapper.
Integration PatternDOM State IsolationEvent Listener IntegrityDebugging & Reliability
innerHTML Swap ComponentZero (Mutates host document)Non-deterministic (Attaches to dead DOM)Catastrophic (Silent click failures)
Full Viewport <iframe> Shell100% Native Browser SandboxPerfect (Scripts load in own window)Instant (Isolated DevTools context)

Dynamic HTML inclusion scripts trigger integration vulnerabilities

We needed to create 10 distinct landing pages for search engine optimization (SEO), each carrying unique titles, meta descriptions, and schema structured data, while embedding the exact same underlying PDF Processor tool.

Rather than copying the 500-line tool HTML into 10 separate HTML entry files, we built a custom web component (<gx-app-shell>). The component fetched /tools/pdf-processor/, extracted its <body> content, and replaced the current page's document.body.innerHTML.


innerHTML swaps break event listeners by destroying DOM nodes

The issue was a severe DOM race condition:

  1. The host page loaded script tags (like pdf-processor.js), which attached click handlers during DOMContentLoaded.
  2. Concurrently, <gx-app-shell> fetched the canonical HTML payload asynchronously.
  3. Upon receiving the payload, <gx-app-shell> executed document.body.innerHTML = canonicalBody.innerHTML.
  Browser Engine               pdf-processor.js            <gx-app-shell>            document.body
        |                             |                           |                        |
        |--- 1. DOMContentLoaded ---->|                           |                        |
        |                             |--- 2. Attach handlers --->|                        |
        |                             |    (To initial nodes)     |                        |
        |--- 3. Fetch canonical HTML ---------------------------->|                        |
        |                             |                           |--- 4. Swap innerHTML ->| (Original nodes
        |                             |                           |                         |  destroyed)
        |                             |<-- 5. Handlers remain on orphaned nodes ------------|

Replacing document.body.innerHTML destroyed the original DOM elements and replaced them with fresh, un-wired DOM nodes. The event handlers attached in step 1 were left pointing to orphaned, unmounted DOM nodes in browser memory.

Production symptoms

Users experienced a non-deterministic UI:

Replacing dynamic injection with sandboxed iframes resolves DOM races

We deleted the custom component and replaced each SEO page entry point with a clean, 14-line HTML wrapper using an <iframe>:

<!DOCTYPE html>
<html lang="en">
<head>
  <title>Extract Tables & Text from PDF | GINEXYS</title>
  <meta name="description" content="Free offline PDF table and geometry extractor." />
  <link rel="canonical" href="https://ginexys.com/tools/pdf-processor/" />
</head>
<body style="margin:0; padding:0; overflow:hidden;">
  <iframe 
    src="/tools/pdf-processor/?view=editor" 
    style="width:100%; height:100vh; border:none;" 
    allow="clipboard-write">
  </iframe>
</body>
</html>

Because the application runs inside its own isolated <iframe> window context, its internal scripts initialize deterministically against its own DOM. Nothing races, and the host landing page retains its unique SEO metadata.

Rule of thumb: Never use innerHTML swaps to inject running interactive web applications into host documents. Use native <iframe> elements to guarantee clean DOM lifecycle and event listener isolation.
Read this post in the full Engineering Journal →