Skip to content

Instantly share code, notes, and snippets.

@tuhuynh27
Created May 2, 2026 05:44
Show Gist options
  • Select an option

  • Save tuhuynh27/4cd9edd7e626027697f1ea91466f2f7e to your computer and use it in GitHub Desktop.

Select an option

Save tuhuynh27/4cd9edd7e626027697f1ea91466f2f7e to your computer and use it in GitHub Desktop.
TCPW Height Guide

Embed Height Reporter — Setup Guide

This guide is for the embed app at rate.tapchiphowall.com (a separate Nuxt project). It explains how to make the embed report its content height to the parent (tapchiphowall.com / en.tapchiphowall.com) so the parent's <iframe> resizes correctly when content inside the embed changes (tab switches, async data loads, etc.).

Why this is needed

Browsers enforce same-origin policy: tapchiphowall.com cannot read the height of an iframe served from rate.tapchiphowall.com. The parent has no way to detect content reflows inside the embed unless the embed cooperates by posting its current height via window.parent.postMessage(...).

Polling from the parent only works if the embed responds to height-request messages. The cleanest fix is for the embed to push height updates whenever its own content changes — no polling required.

Step 1 — Add the composable

Create composables/useEmbedHeightReporter.ts in the embed app (rate.tapchiphowall.com's repo):

const ALLOWED_PARENT_ORIGINS = [
  'https://tapchiphowall.com',
  'https://en.tapchiphowall.com',
  'http://localhost:3000' // tapchiphowall-app dev server
]

export function useEmbedHeightReporter() {
  if (import.meta.server) return

  onMounted(() => {
    if (window.self === window.top) return // not embedded — skip

    // Pick the parent origin from referrer; fall back to first allowed.
    const referrerOrigin = (() => {
      try { return new URL(document.referrer).origin } catch { return '' }
    })()
    const targetOrigin = ALLOWED_PARENT_ORIGINS.includes(referrerOrigin)
      ? referrerOrigin
      : ALLOWED_PARENT_ORIGINS[0]

    let lastHeight = 0
    let raf = 0

    const postHeight = () => {
      cancelAnimationFrame(raf)
      raf = requestAnimationFrame(() => {
        const h = Math.ceil(document.documentElement.scrollHeight)
        if (h === lastHeight || h <= 0) return
        lastHeight = h
        window.parent.postMessage({ type: 'height', height: h }, targetOrigin)
      })
    }

    // 1. ResizeObserver catches layout reflows (tab switches, content swaps).
    const ro = new ResizeObserver(postHeight)
    ro.observe(document.documentElement)
    ro.observe(document.body)

    // 2. MutationObserver catches DOM swaps that don't change box size
    //    immediately (e.g. tab switch followed by async content render).
    const mo = new MutationObserver(postHeight)
    mo.observe(document.body, { childList: true, subtree: true })

    // 3. Respond to explicit polls from the parent (so the parent's
    //    watchdog still works as a safety net).
    const onMessage = (e: MessageEvent) => {
      if (!ALLOWED_PARENT_ORIGINS.includes(e.origin)) return
      const d = e.data
      const isRequest =
        d === 'requestHeight' || d === 'getHeight' ||
        d?.type === 'requestHeight' || d?.type === 'getHeight' ||
        d?.action === 'getHeight'
      if (!isRequest) return
      lastHeight = 0 // force re-send even if value unchanged
      postHeight()
    }
    window.addEventListener('message', onMessage)
    window.addEventListener('load', postHeight)
    window.addEventListener('resize', postHeight)
    postHeight() // initial

    onBeforeUnmount(() => {
      ro.disconnect()
      mo.disconnect()
      window.removeEventListener('message', onMessage)
      window.removeEventListener('load', postHeight)
      window.removeEventListener('resize', postHeight)
      cancelAnimationFrame(raf)
    })
  })
}

Step 2 — Use it in the embedded page

In the embed app, edit pages/ecb.vue (or whichever page is loaded inside the iframe):

<script setup>
useEmbedHeightReporter()
</script>

That's it. The composable auto-imports because it's in composables/.

Step 3 — Test locally

The parent (tapchiphowall-app) runs on http://localhost:3000. The embed needs to be reachable from the parent during dev — pick one of:

Option A: Run both apps locally

  1. In the embed app, start the dev server on a different port:
    pnpm dev --port 3100
  2. In the parent app (tapchiphowall-app), temporarily change the iframe URL in pages/interest-rate.vue to point at http://localhost:3100/ecb instead of https://rate.tapchiphowall.com/ecb.
  3. Run the parent: pnpm dev (defaults to port 3000).
  4. Visit http://localhost:3000/interest-rate.
  5. Open DevTools console and watch for height messages — switching tabs inside the embed should produce a stream of { type: 'height', height: ... } postMessages, and the outer iframe should resize immediately.

Option B: Test the embed against staging

If you don't want to run two Nuxt servers, deploy the embed change to a preview URL first and update the parent iframe to point at the preview URL temporarily.

Verifying it works

In the parent's DevTools console, run:

window.addEventListener('message', e => {
  if (e.origin.endsWith('tapchiphowall.com') || e.origin.includes('localhost')) {
    console.log('FROM EMBED:', e.origin, e.data)
  }
})

Then switch tabs inside the embed. You should see { type: 'height', height: <number> } log on each interaction.

Step 4 — Deploy

  1. Deploy the embed app change to rate.tapchiphowall.com.
  2. No changes are required on the tapchiphowall-app parent side — extractHeight() in pages/interest-rate.vue already handles the { type: 'height', height: ... } payload via its data.height branch.
  3. (Optional cleanup) Once you've confirmed push-based updates work, you can relax the parent's polling interval in pages/interest-rate.vue from 500 back to 2000 ms or higher — it becomes a redundant safety net.

Troubleshooting

Nothing logs in the parent's console after deploying.

  • Check the embed has actually shipped — view source on rate.tapchiphowall.com/ecb and confirm the composable code is present.
  • Check event.origin on the embed side — if referrerOrigin doesn't match an allowed parent, the embed posts to the fallback (tapchiphowall.com) and en.tapchiphowall.com parents won't see it. Add the missing origin to ALLOWED_PARENT_ORIGINS.

Messages arrive but the iframe doesn't resize.

  • Open DevTools and inspect the <iframe> height attribute. If it's updating but visually clipped, look for overflow: hidden or max-height on a wrapper.

Infinite resize loop.

  • The lastHeight guard prevents re-posting an unchanged height, but if the embed's content height genuinely oscillates (e.g. a chart that resizes to fit the iframe, which then resizes to fit the chart), add a small min-change threshold:
    if (Math.abs(h - lastHeight) < 4) return

Embed loads in a sandboxed iframe and window.parent is null.

  • Check the parent iframe's sandbox attribute. The current parent doesn't set one, so this shouldn't happen — but if it ever does, allow-scripts must be present for the embed to run, and posting to parent doesn't require any extra sandbox token.

Why a composable, not a plugin

  • A composable is opt-in per page. Safer if rate.tapchiphowall.com later adds non-embedded routes.
  • A client plugin (plugins/embed-height.client.ts) runs on every page automatically — convenient if all routes will always be embedded. Same body, just wrap in defineNuxtPlugin(() => { /* setup */ }) and call the setup directly instead of inside onMounted.

For a single-page embed (/ecb only), either works; the composable is recommended for explicitness.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment