Content Embedding (Embed) Implementation
Embed is not just <iframe src="...">. Proper implementation includes security (sandbox, CSP), performance (lazy-loading, aspect-ratio), oEmbed protocol for automatic generation, and support for custom providers.
oEmbed: The Standard for Automatic Embedding
oEmbed is a JSON protocol that allows you to retrieve HTML code for embedding by URL. Most platforms support it.
interface OEmbedResponse {
type: 'rich' | 'video' | 'photo' | 'link'
html?: string
width?: number
height?: number
thumbnail_url?: string
title?: string
author_name?: string
}
async function fetchOEmbed(url: string, maxWidth = 800): Promise<OEmbedResponse> {
// Most providers accept requests directly, but due to CORS — through a proxy
const proxyUrl = `/api/oembed?url=${encodeURIComponent(url)}&maxwidth=${maxWidth}`
const res = await fetch(proxyUrl)
if (!res.ok) throw new Error(`oEmbed failed: ${res.status}`)
return res.json()
}
// Laravel: proxy for oEmbed requests
// app/Http/Controllers/OEmbedController.php
class OEmbedController extends Controller
{
private array $providers = [
'youtube.com' => 'https://www.youtube.com/oembed',
'youtu.be' => 'https://www.youtube.com/oembed',
'vimeo.com' => 'https://vimeo.com/api/oembed.json',
'twitter.com' => 'https://publish.twitter.com/oembed',
'x.com' => 'https://publish.twitter.com/oembed',
'instagram.com' => 'https://graph.facebook.com/v18.0/instagram_oembed',
'soundcloud.com' => 'https://soundcloud.com/oembed',
'spotify.com' => 'https://open.spotify.com/oembed',
'tiktok.com' => 'https://www.tiktok.com/oembed',
'codepen.io' => 'https://codepen.io/api/oembed',
];
public function fetch(Request $request): JsonResponse
{
$url = $request->validate(['url' => 'required|url'])['url'];
$host = parse_url($url, PHP_URL_HOST);
$host = preg_replace('/^www\./', '', $host);
$endpoint = collect($this->providers)
->first(fn($v, $k) => str_contains($host, $k));
if (!$endpoint) {
return response()->json(['error' => 'Provider not supported'], 422);
}
$response = Http::timeout(5)->get($endpoint, [
'url' => $url,
'maxwidth' => $request->integer('maxwidth', 800),
'format' => 'json',
]);
return response()->json($response->json(), $response->status());
}
}
Safe HTML Rendering from oEmbed
HTML from external sources cannot be inserted directly via innerHTML. Use DOMPurify:
npm install dompurify @types/dompurify
import DOMPurify from 'dompurify'
// Config: allow iframes only from trusted domains
const ALLOWED_IFRAME_ORIGINS = [
'https://www.youtube.com',
'https://player.vimeo.com',
'https://open.spotify.com',
'https://w.soundcloud.com',
'https://www.tiktok.com',
'https://codepen.io',
]
DOMPurify.addHook('uponSanitizeElement', (node, data) => {
if (data.tagName === 'iframe') {
const src = node.getAttribute('src') || ''
const allowed = ALLOWED_IFRAME_ORIGINS.some(origin => src.startsWith(origin))
if (!allowed) {
node.remove()
}
}
})
const config = {
ADD_TAGS: ['iframe'],
ADD_ATTR: ['allowfullscreen', 'frameborder', 'scrolling', 'allow', 'referrerpolicy'],
}
function SafeEmbed({ html }: { html: string }) {
const clean = DOMPurify.sanitize(html, config)
return <div dangerouslySetInnerHTML={{ __html: clean }} className="embed-wrapper" />
}
Responsive Embed: Removing Fixed Dimensions
oEmbed often returns width="560" height="315" directly in HTML. This breaks responsiveness. Fix it via CSS or post-processing:
function makeResponsive(html: string): string {
const parser = new DOMParser()
const doc = parser.parseFromString(html, 'text/html')
doc.querySelectorAll('iframe').forEach(iframe => {
const w = parseInt(iframe.getAttribute('width') || '0')
const h = parseInt(iframe.getAttribute('height') || '0')
if (w && h) {
const ratio = (h / w * 100).toFixed(4)
iframe.setAttribute('width', '100%')
iframe.removeAttribute('height')
iframe.style.aspectRatio = `${w}/${h}`
}
})
return doc.body.innerHTML
}
.embed-wrapper {
width: 100%;
}
.embed-wrapper iframe {
width: 100%;
border: none;
aspect-ratio: 16/9;
}
Lazy Loading via Intersection Observer
import { useEffect, useRef, useState } from 'react'
function LazyEmbed({ url, label }: { url: string; label: string }) {
const [loaded, setLoaded] = useState(false)
const [embedHtml, setEmbedHtml] = useState('')
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setLoaded(true)
observer.disconnect()
}
},
{ rootMargin: '300px' }
)
if (containerRef.current) observer.observe(containerRef.current)
return () => observer.disconnect()
}, [])
useEffect(() => {
if (!loaded) return
fetchOEmbed(url).then(data => {
if (data.html) setEmbedHtml(makeResponsive(data.html))
})
}, [loaded, url])
return (
<div ref={containerRef} className="embed-container" aria-label={label}>
{embedHtml
? <SafeEmbed html={embedHtml} />
: <div className="embed-placeholder">Loading...</div>
}
</div>
)
}
Custom Provider: Figma Embedding
// Figma does not support oEmbed — build manually
function buildFigmaEmbed(url: string, options = { width: 800, height: 450 }): string {
const embedUrl = `https://www.figma.com/embed?embed_host=share&url=${encodeURIComponent(url)}`
return `<iframe
src="${embedUrl}"
width="${options.width}"
height="${options.height}"
allowfullscreen
style="border: 1px solid rgba(0, 0, 0, 0.1);"
></iframe>`
}
// Determine type by URL and choose strategy
function resolveEmbed(url: string): Promise<string> {
if (url.includes('figma.com')) {
return Promise.resolve(buildFigmaEmbed(url))
}
if (url.includes('github.com') && url.includes('/blob/')) {
return buildGistEmbed(url)
}
return fetchOEmbed(url).then(d => makeResponsive(d.html ?? ''))
}
Content Security Policy and iframe Sandbox
// Laravel middleware for CSP headers
class ContentSecurityPolicy
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$frameAncestors = [
"'self'",
'https://www.youtube.com',
'https://player.vimeo.com',
];
$response->headers->set(
'Content-Security-Policy',
implode('; ', [
"default-src 'self'",
"frame-src " . implode(' ', $frameAncestors),
"script-src 'self' 'nonce-" . $request->attributes->get('csp_nonce') . "'",
])
);
return $response;
}
}
<!-- sandbox limits iframe capabilities -->
<iframe
src="https://www.youtube.com/embed/..."
sandbox="allow-scripts allow-same-origin allow-presentation allow-fullscreen"
loading="lazy"
referrerpolicy="no-referrer-when-downgrade"
></iframe>
The sandbox attribute without allow-same-origin completely isolates the iframe, but breaks most players. The minimal secure set is allow-scripts allow-same-origin.
Caching oEmbed Responses
// Cache oEmbed response for 24 hours — platforms change embed code rarely
public function fetch(Request $request): JsonResponse
{
$url = $request->input('url');
$cacheKey = 'oembed:' . md5($url);
$data = Cache::remember($cacheKey, now()->addDay(), function () use ($url) {
// ... request to provider
});
return response()->json($data);
}
Timeline
A basic oEmbed proxy with 10 providers — 1 day. With safe rendering, responsive design, lazy-loading, and caching — 2–3 days. A full-featured editor with WYSIWYG embed insertion by URL (like Notion) — 1 week.







