I wanted a calculator that a stranger could drop into their blog with one paste. What I ended up learning was mostly about what not to put inside an iframe.
The constraints, in the order they bit me.
1. The widget cannot carry your site chrome
The first version embedded the whole page: header nav, footer, cookie banner, ad slots. On my site that reads as a finished product. Inside someone else's article it reads as a website crammed into a letterbox, and the first thing the host does is delete it.
So the embed route has to render only the tool. In Vue that meant one flag on the route and one v-if in the app shell:
<!-- App.vue -->
<header v-if="!bare">…</header>
<PrivacyBanner v-if="!bare" variant="bar" />
<main>
<div :class="bare ? 'px-3 py-3' : 'mx-auto max-w-5xl px-4 py-8'">
<RouterView />
</div>
</main>
<footer v-if="!bare">…</footer>
const bare = computed(() => route.meta.bare === true)
Two lines of state, a handful of v-ifs. The alternative — a second app entry point — is the kind of duplication that quietly drifts until the embedded version shows last quarter's formula.
2. No ads, and say so
An ad inside your iframe renders on someone else's page. That is their inventory, their layout and their audience. It is also the fastest way to get the embed pulled and the domain remembered badly.
The same applies to analytics in any form that a host would notice. Mine loads a site-wide tag in index.html, which the embed inherits — that one I accepted because it is aggregate pageview counting and nothing about the widget's inputs leaves the browser. Anything more invasive would have to be stripped per-route.
3. noindex the embed, canonical it to the real page
Here is the trap: the embed page is a near-duplicate of the tool page. Left indexable, it competes with the page you actually want to rank.
<meta name="robots" content="noindex, follow">
<link rel="canonical" href="https://example.com/tools/ltv-cac-calculator">
follow rather than noindex, nofollow: crawlers should still walk the attribution link back to the real page. And the canonical points at the tool page, not at itself — the embed is a rendering of that page, not a page of its own.
Also: leave the embeds out of the sitemap. If your sitemap is generated from a route table, exclude them explicitly, otherwise you are telling Google "please index these noindex pages" — which it will report as an error for weeks.
4. Size is a suggestion the host will ignore
<iframe width="100%" height="640"> is what I ship, and roughly half of hosts will paste it into a column that is 500px wide. That is fine — the widget is responsive — but it means height cannot be computed from width. A two-column form-plus-result layout that fits at 900px stacks at 500px and gets taller.
Fix: measure it, do not guess. A 20-line script in the embed page posts its height to the parent:
const ro = new ResizeObserver(() => {
parent.postMessage(
{ type: 'resize', id: 'ltv-cac', height: document.documentElement.scrollHeight },
'*'
)
})
ro.observe(document.documentElement)
and the host's one-liner listens. Ship the fixed-height iframe as the default so a host who pastes and walks away still gets something usable, and treat auto-resize as the upgrade.
<script>
addEventListener('message', (e) => {
if (e.data?.type !== 'resize') return
const f = document.querySelector(`iframe[data-embed="${e.data.id}"]`)
if (f) f.style.height = e.data.height + 'px'
})
</script>
5. loading="lazy" is not optional
An embed at the top of a long article otherwise blocks that article's first paint for a host who never asked for it. It is one attribute:
<iframe src="…" loading="lazy" style="border:1px solid #e2e8f0;border-radius:12px"></iframe>
6. The attribution link is the entire point
Be honest about the incentive: the reason to build an embed is the link. So put it somewhere a human actually reads, make it small and legible, and do not hide it:
<a href="https://example.com/tools/ltv-cac-calculator" target="_blank" rel="noopener">
LTV:CAC Ratio Calculator
</a> by Example — free, no signup, nothing stored.
Two things I would not do: target="_top" on that link (it hijacks the host's navigation) and a "powered by" that only appears after 30 days (nobody trusts it, and it gets patched out).
What the whole thing cost
One route per tool, one bare-rendering page, one component that prints the snippet with a copy button. No build step, no script tag, no third-party runtime — which also means nothing to sandbox, no CSP negotiation with the host, and no way for the embed to break their page.
An iframe is the boring answer. For a widget that must never interfere with someone else's site, boring is the feature.
The live version, if you want to see the bare rendering: embed page, and the copy-paste block it belongs to.
Top comments (0)