Postmortem: The Web Component That Swapped the DOM and Broke Everything Silently
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 Pattern | DOM State Isolation | Event Listener Integrity | Debugging & Reliability |
|---|---|---|---|
innerHTML Swap Component | Zero (Mutates host document) | Non-deterministic (Attaches to dead DOM) | Catastrophic (Silent click failures) |
Full Viewport <iframe> Shell | 100% Native Browser Sandbox | Perfect (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:
- The host page loaded script tags (like
pdf-processor.js), which attached click handlers duringDOMContentLoaded. - Concurrently,
<gx-app-shell>fetched the canonical HTML payload asynchronously. - Upon receiving the payload,
<gx-app-shell>executeddocument.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:- Tab buttons looked visually correct and highlighted on hover, but clicking them did nothing.
- File picker dialogs refused to open.
- Clicking "Process File" produced zero output, with no console errors or warnings logged.
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 useinnerHTMLswaps to inject running interactive web applications into host documents. Use native<iframe>elements to guarantee clean DOM lifecycle and event listener isolation.