VibeWeek
Home/Grow/Embed Widgets & oEmbed Implementation

Embed Widgets & oEmbed Implementation

⬅️ Day 6: Grow Overview

If you're building a B2B SaaS in 2026 with content users want to share — forms (Tally / Typeform), schedulers (Calendly), videos (Loom), boards (Trello), polls (Mentimeter), data viz (Datawrapper) — letting users embed your product on their site is a powerful growth loop. Embeds drive: brand exposure to embedder's audience, viral discovery, ease of use for end-users (don't need account). The naive approach: provide a script tag and call it done. The structured approach: choose iframe vs JS widget, secure the embed (CSP, sandboxing, postMessage), implement oEmbed for rich-link unfurls (Notion, Slack, Twitter), responsive sizing, theming, accessibility. (See markdown-rendering-sanitization-chat for inverse — rendering external embeds inside your product.)

1. Decide embed type — iframe vs JS widget vs oEmbed

Pick embed type.

iframe (most common):
- Simple HTML: <iframe src="https://yourapp.com/embed/abc">
- Cross-origin isolation
- Full control over content
- Pro: secure (sandboxed); easy to implement
- Con: rigid sizing; less interactive

JS widget (richer):
- <script src="https://yourapp.com/widget.js"></script>
- Renders into <div data-widget-id="abc">
- Pro: dynamic; interactive; theming
- Con: more complex; security surface; CORS

oEmbed (rich-link unfurls):
- Spec for converting URL → embed code
- Used by: Twitter, YouTube, Spotify, Notion
- When user pastes link, host fetches /oembed?url=... → returns HTML
- Pro: works in many platforms automatically
- Con: spec-bound; less customizable

Combinations:
- iframe + oEmbed: most common for content
- JS widget + oEmbed: for interactive
- All three: full coverage

For [PRODUCT], decision factors:

Form / scheduler / poll → iframe + oEmbed
Video / interactive widget → iframe + JS widget hybrid
Live data / dashboards → JS widget for richness
Social platform compatibility → oEmbed required

Output:
1. Recommendation
2. iframe vs JS for primary
3. oEmbed support
4. Browser compatibility
5. Mobile considerations

The 2026 default for most B2B SaaS embeds: iframe + oEmbed. Iframe gives security; oEmbed gives social-platform compatibility.

2. Build the iframe embed

Implement iframe embed.

URL pattern:
- /embed/[id] (clean)
- Or: /embed?id=[id] (legacy)
- Public URL (no auth required usually)

Embed page setup:

Minimal layout (no nav / footer):
- Strip product chrome
- Just the content
- Branding minimal (small "Powered by [Product]" or hidden)

Responsive:
- viewport meta tag
- Max-width constraint
- height: auto
- Or: send postMessage on resize

Embed snippet:

<iframe
  src="https://yourapp.com/embed/abc123"
  width="100%"
  height="600"
  frameborder="0"
  allow="..." (camera, microphone, etc. if needed)
  sandbox="allow-scripts allow-forms allow-same-origin"
  title="Form by Yourapp"
></iframe>

Sandbox attributes:
- allow-scripts (run JS)
- allow-forms (submit forms)
- allow-same-origin (cookies / localStorage)
- allow-popups (open new window)
- Don't allow: allow-top-navigation (security)

Cross-frame communication:

postMessage for resize:
- Embed sends current height to parent
- Parent listens; resizes iframe

window.parent.postMessage({
  type: 'resize',
  height: document.body.scrollHeight
}, '*');

Parent:
window.addEventListener('message', (e) => {
  if (e.origin !== 'https://yourapp.com') return; // SECURITY
  if (e.data.type === 'resize') {
    iframe.style.height = e.data.height + 'px';
  }
});

Output:
1. Embed URL pattern
2. Embed-page layout
3. Sandbox config
4. PostMessage protocol
5. Test on different host sites

The postMessage origin check: critical for security. Without it, any site can spoof messages from your embed. Always verify origin.

3. Build JS widget embed

Implement JS widget embed.

Snippet:

<script src="https://yourapp.com/widget.js" async></script>
<div data-widget="abc123"></div>

Widget script (widget.js):

(function() {
  // 1. Find all widget targets
  const widgets = document.querySelectorAll('[data-widget]');
  
  widgets.forEach(el => {
    const id = el.dataset.widget;
    
    // 2. Create iframe inside (or render React)
    const iframe = document.createElement('iframe');
    iframe.src = `https://yourapp.com/embed/${id}`;
    iframe.style.width = '100%';
    iframe.style.border = 'none';
    el.appendChild(iframe);
    
    // 3. Listen for resize messages
    window.addEventListener('message', (e) => {
      if (e.origin !== 'https://yourapp.com') return;
      if (e.data.widgetId === id && e.data.type === 'resize') {
        iframe.style.height = e.data.height + 'px';
      }
    });
  });
})();

Why JS over iframe-direct:
- Auto-resize (script handles)
- Multiple widgets per page
- Custom config via data attributes
- Theme inheritance (read CSS vars from parent)

Theme inheritance:
- Read computed colors from parent
- Pass via URL params to iframe
- Iframe applies theme

Cleanup:
- Widget can be removed from DOM
- Listener cleanup needed
- MutationObserver to detect removal

Hosting:
- CDN (Cloudflare / Vercel)
- Versioning: yourapp.com/widget/v1.js
- Cache headers (long for versioned; short for latest)
- Subresource Integrity (SRI) hash for security

Output:
1. Widget script architecture
2. Init flow
3. Theme handling
4. Lifecycle (init / cleanup)
5. CDN + versioning

The Calendly widget pattern: tiny JS bootstrap that injects iframe. Combines iframe security with JS flexibility.

4. oEmbed — rich-link unfurls

oEmbed = "when someone pastes a link to your product into Twitter/Notion/Slack, it auto-embeds."

Implement oEmbed.

Spec basics:

When third-party platform sees a URL, they request:
GET https://yourapp.com/oembed?url=https://yourapp.com/embed/abc&format=json

You respond:
{
  "type": "rich",
  "version": "1.0",
  "title": "Form Title",
  "html": "<iframe src='...' width='600' height='400'></iframe>",
  "width": 600,
  "height": 400,
  "provider_name": "Yourapp",
  "provider_url": "https://yourapp.com"
}

oEmbed types:
- photo (image)
- video (player)
- link (just metadata; no embed)
- rich (HTML embed)

Discovery (auto-detect):

Add to your embed page <head>:
<link rel="alternate" type="application/json+oembed" href="https://yourapp.com/oembed?url=...&format=json">
<link rel="alternate" type="text/xml+oembed" href="https://yourapp.com/oembed?url=...&format=xml">

Now Notion / Slack / Twitter auto-discover oEmbed URL.

Implementation:

Endpoint: /oembed?url=...
- Validate URL belongs to you
- Fetch resource info
- Return JSON / XML

Allowlist:
- Only embed your-domain URLs
- Validate input (don't embed arbitrary URLs)

Cache:
- Long cache (oEmbed responses don't change often)
- Edge-cached via CDN

OEmbed providers list:
- Some platforms maintain list of oEmbed providers
- Submit to Embed.ly to be auto-recognized
- Otherwise: only platforms that auto-discover

Output:
1. /oembed endpoint
2. Discovery <link> tags
3. Validation
4. Caching
5. Provider listing submission

The "pasting URL becomes embed in Notion" magic: oEmbed makes that work. ~30 lines of code; huge UX win.

5. Embed customization — theming + branding

Embedders want to match their site's look.

Allow customization.

URL parameters:

/embed/abc?theme=dark
/embed/abc?primary=#3366ff
/embed/abc?layout=compact

Common params:
- theme (light / dark / auto)
- primary color
- accent color
- font (system / serif / mono)
- layout (compact / spacious)
- branding (show / hide)

Theme handling:

Auto-detect parent:
- Read prefers-color-scheme
- Read parent CSS vars (limited cross-origin)
- Default to light

Custom CSS via URL params:
- Allow hex colors only (validate)
- Don't allow arbitrary CSS (XSS risk)
- Sanitize inputs

Branding:

Default: visible "Powered by [Product]"
- Free tier: required
- Paid tier: optional / removable

Removal:
- Higher tier or paid customer
- Validate via API key in embed URL

Pricing:
- Free embeds: branded; rate-limited
- Paid: unbranded; high-volume; custom domain

Custom domain (white-label):

embed.customer.com → CNAME to your service
- Customer's branding
- SSL via Cloudflare / Vercel
- Validate domain ownership

Output:
1. Customization params
2. Theme matching
3. Branding control
4. Custom domain (if white-label)
5. Tier-based features

The "Powered by Yourapp" growth loop: every embed = brand exposure. 1000 embedded forms × 100 viewers each = 100K impressions. Worth more than ad spend; don't remove easily.

6. Security — sandbox, CSP, isolation

Embeds are a security surface. Lock it down.

Secure embeds.

Iframe sandbox:

Required:
- sandbox="allow-scripts allow-forms allow-same-origin"
- Restrict where possible

Don't allow:
- allow-top-navigation (escape iframe)
- allow-modals (popup attacks)
- allow-pointer-lock (UX abuse)

Content Security Policy (CSP):

In embed page:
Content-Security-Policy: 
  default-src 'self';
  script-src 'self' 'unsafe-inline';
  style-src 'self' 'unsafe-inline';
  frame-ancestors *; (allow embedding)

Restrict frame-ancestors:
- 'none' = nobody can embed
- 'self' = only your domain
- '*' = anyone (typical for public embed)
- specific URLs = allowlist

X-Frame-Options:
- Deprecated; use CSP frame-ancestors
- For older browsers

PostMessage validation:

Always check e.origin:
- Don't trust messages from any origin
- Allowlist: ['https://yourapp.com', 'https://embed.yourapp.com']
- Reject mismatch

Authentication:

Public embeds:
- No auth required
- Public-readable resource

Private embeds:
- Token in URL
- Or: short-lived signed URL
- Auth via embedding party's session (postMessage to parent)

API rate limiting:

Per-domain:
- Track which sites embed you
- Rate-limit per embedding domain
- Detect abuse (1000 embeds from suspicious site)

Per-IP:
- Standard rate limiting

XSS prevention:

User-generated content in embed:
- Sanitize before display
- See markdown-rendering-sanitization-chat

Sensitive data:
- Don't embed forms with sensitive data on third-party sites
- Or: warn user
- Or: require auth

Output:
1. Sandbox config
2. CSP headers
3. PostMessage origin check
4. Auth strategy
5. Rate limiting
6. XSS mitigation

The frame-ancestors discipline: define who can embed you. "*" allows anyone (needed for public embeds); specific allowlist for private content.

7. Responsive embeds

Embeds need to work on mobile + various sizes.

Make embeds responsive.

Aspect ratio container:

CSS for parent (preferred):
.embed-container {
  position: relative;
  padding-bottom: 75%; /* aspect ratio */
  height: 0;
}
.embed-container iframe {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
}

Auto-resize via postMessage:
- Embed measures content height
- Sends to parent
- Parent updates iframe height

Width:
- 100% of parent (most cases)
- Max-width for desktop (avoid stretching)

Mobile:
- Touch-friendly inputs (44px+ tap targets)
- Reduce padding
- Hide non-critical UI
- Test on mobile Safari + Chrome

Print:
- Often hidden
- Show "View online" placeholder
- Or: print-friendly CSS

Different aspect ratios:

Form: variable height (postMessage)
Video: 16:9 typical
Calendar: variable
Dashboard: variable

Output:
1. Aspect ratio CSS
2. PostMessage resize protocol
3. Mobile UX
4. Print handling
5. Test plan

The "embed too tall on mobile" problem: forms with many fields stretch on mobile. Solve with postMessage-based dynamic height; iframe grows as form completes.

8. Embed analytics

Track embed performance.

Track embed analytics.

Events to track:

Embed render:
- Where embedded (referrer)
- Embed ID (which resource)
- Timestamp

Embed interaction:
- View
- Submit / click / play
- Time spent

Tracking implementation:

Server-side (basic):
- Log embed page hits
- Referrer header tells you where embedded
- Group by domain → embedders dashboard

Client-side (richer):
- JS in embed sends events to your analytics
- See product analytics (PostHog / Amplitude)
- Privacy: GDPR / cookie compliance

Privacy considerations:

Cookies:
- Iframe cookies are third-party (limited by browsers)
- Use first-party server-side tracking
- Or: cookieless analytics

GDPR:
- Iframe inherits parent site's consent
- But your embed creates separate processing
- DPA with embedding parties (rare; usually privacy notice covers)

Anti-patterns:
- Tracking pixels without disclosure
- Cross-site fingerprinting
- Sharing data with embedders without consent

Embedder dashboard:

Show your customer:
- How many sites embed their content
- View / interaction rates
- Top embedding domains
- Conversion rates

This is differentiation: customers choose your embed because of insight.

Output:
1. Event tracking
2. Server vs client analytics
3. Privacy compliance
4. Embedder dashboard
5. Anti-pattern checklist

The "embed insights drive retention" insight: customers stay if your embed analytics show value. "Your form was viewed 1000 times across 5 sites" beats "Powered by [Product]."

9. Common products — patterns

Different SaaS types have different embed patterns.

Common embed patterns by product type.

Forms (Typeform, Tally, Google Forms):
- Iframe + responsive
- Auto-resize on field expansion
- Submit handled in iframe
- oEmbed for paste-as-link
- Examples: <iframe src="https://tally.so/forms/abc">

Schedulers (Calendly, Cal.com, SavvyCal):
- Iframe with calendar
- Inline + popup widget options
- Pre-fill via URL params
- Booking confirmation in-iframe
- oEmbed for social platforms

Video (Loom, Vimeo, Wistia):
- Iframe with video player
- Lazy-load (poster image until clicked)
- oEmbed required for Twitter / Notion / etc.
- Aspect-ratio responsive

Social embeds (Twitter, Reddit, Reddit):
- oEmbed primary (link → embed)
- Iframe + JS combination
- Tweet rendering shows author / engagement

Polls / surveys (Mentimeter, Polly):
- Real-time iframe
- WebSocket for live updates
- Respondent ID via cookie / fingerprint

Dashboards (Datawrapper, Mode, Geckoboard):
- JS widget for richness
- Theming critical
- Often interactive

E-commerce widgets (Stripe Checkout, Shopify Buy Button):
- iframe-popup for payment (security + PCI)
- "Buy" button → iframe overlay

Forms / quizzes / lead capture:
- iframe + tracking pixels
- Anti-bot (captcha)
- Privacy-respecting

For [PRODUCT TYPE], output:
1. Recommended pattern
2. Iframe vs JS vs oEmbed mix
3. Specific gotchas
4. Pre-fill / config options
5. Mobile / accessibility

The Calendly pattern: small JS bootstrap → injects iframe → handles resize + analytics. Standard for interactive widgets.

10. Distribution + adoption

Embeds drive growth if customers actually embed.

Drive embed adoption.

In-product affordance:

"Share" button on resource:
- Options: Copy link / Embed / Email / Social
- Embed → modal with snippet

Embed snippet:
- Code snippet (iframe / JS)
- Customization options (theme, size)
- Copy button
- Preview

Onboarding:

Suggest embed during onboarding:
- "Add to your site"
- Customer education on benefits

Customer-facing:
- Help docs: "How to embed"
- Per-platform guides (Squarespace, Webflow, WordPress, etc.)
- Video tutorials

Platform integrations:

WordPress plugin:
- One-click embed
- Easier than copy-paste

Webflow:
- Custom code embed

Notion:
- oEmbed auto-works
- Document the URL pattern

Growth loops:

Tier-based branding:
- Free: branded ("Powered by")
- Paid: unbranded
- Upgrade incentive

Embed analytics for embedders:
- Show value (views, conversions)
- Lock-in via insights

Anti-patterns:
- Hidden behind paywall (free tier should embed)
- Complex setup
- No customization (looks foreign)
- Heavy / slow (impacts embedder's site)

Output:
1. In-product share UI
2. Customer education
3. Platform integrations
4. Tier strategy
5. Performance optimization

The "embed must load fast" rule: iframe / JS that takes 5s to load slows embedder's site. Embedders cut you. Optimize bundle size + cache aggressively.

What Done Looks Like

A v1 embed system:

  • iframe embed at /embed/[id]
  • Sandbox + CSP configured
  • PostMessage protocol with origin check
  • Responsive (auto-resize)
  • oEmbed endpoint at /oembed
  • Discovery tags
  • Theme customization (URL params)
  • Branding (free) / unbranded (paid)
  • Embed analytics tracked
  • Embedder dashboard
  • In-product "Embed" share button
  • Help docs per platform

Add later when product is mature:

  • JS widget for richer interaction
  • Custom domain (white-label)
  • Platform-specific plugins (WordPress, Webflow)
  • Multi-embed analytics
  • A/B test embed variants
  • Embedder API

The mistake to avoid: no origin check on postMessage. Anyone can spoof; security hole.

The second mistake: embed too heavy. Embedders' sites slow down; cut you.

The third mistake: no oEmbed. Twitter / Notion / Slack pastes don't render; lose social platform reach.

See Also