markup/oembed-bundle
Symfony bundle that adds oEmbed support, turning media URLs from providers like YouTube, Vimeo, and Twitter into ready-to-use embed HTML via configurable services and Twig helpers.
This is a Symfony2 bundle, not a Laravel package. Treat it as a reference implementation to read and port, not a composer require away from working in a Laravel app.
Resources/config/services.xml and the two interesting classes: the consumer (fetches oEmbed JSON for a URL) and the controller (serves your own content as an oEmbed provider). Those two files are 90% of the value; the Twig extension is a thin wrapper.composer require markup/oembed-bundle --no-scripts in a scratch Symfony2-era project. In a modern Laravel app the solver will almost certainly conflict (the bundle pins symfony/symfony 2.x components, which collide with Laravel's own symfony/* constraints). Don't burn time fighting it.html in a Blade view. That's the "day-to-day" use — embedding user-pasted media links.Consumer pattern (embedding third-party media)
The bundle's flow is: URL → discovery → fetch → render. In Laravel the natural shape is a service + cache + Blade component:
// config/oembed.php
return ['providers' => [
'youtube.com' => 'https://www.youtube.com/oembed',
'vimeo.com' => 'https://vimeo.com/api/oembed.json',
]];
class OEmbed
{
public function forUrl(string $url): ?array
{
$host = parse_url($url, PHP_URL_HOST);
$endpoint = config("oembed.providers.$host");
if (!$endpoint) return null;
return Cache::remember("oembed:$url", now()->addDay(), fn () =>
Http::timeout(3)->get($endpoint, ['url' => $url, 'format' => 'json'])
->throw()->json()
);
}
}
Provider pattern (being embedded by others)
Mirror the bundle's controller: a route like GET /oembed?url=...&format=json&maxwidth=... that resolves your model from the URL and returns the oEmbed 1.0 payload:
{"version":"1.0","type":"video","provider_name":"…","provider_url":"…",
"title":"…","html":"<iframe …></iframe>","width":640,"height":360}
Validate url against your own domain, honour maxwidth/maxheight, and return 404 for unknown URLs (the spec expects it). Discovery is a <link rel="alternate" type="application/json+oembed" href="…"> tag in your page <head> — that's how consumers find your endpoint, so add it to your layout too.
Rendering
The bundle exposes a Twig function/filter; the Laravel equivalent is a Blade component that swallows the "no oEmbed available" case:
<x-oembed :url="$post->media_url" />
Inside, fall back to a plain link when forUrl() returns null — that's the bundle's weakest spot and where you'll add the most value.
Integration tips
OEmbed as a singleton in a service provider and inject Illuminate\Http\Client\Factory so you can Http::fake() in tests.embed/embed handles discovery, caching, and dozens of providers in one).AppKernel, not Laravel's package discovery; there is no service provider. Even if the solver cooperates, nothing boots it.{!! $oembed['html'] !!} blindly. The html field is attacker-influenceable if you let users choose arbitrary endpoint URLs. Allowlist provider hosts by domain and sanitize the returned HTML.https only), resolve the host, and block private/link-local ranges — otherwise http://169.254.169.254/ becomes an oEmbed "provider".HEAD, some require format=json explicitly (YouTube returns 401 without it), some return xml by default. Hardcode known endpoints for the top 5 providers and only fall back to <link> discovery otherwise.type you didn't handle (photo, link, rich vs video) or a missing html for link types. In Symfony you'd use the profiler; in Laravel, Http::fake() plus a dd of the decoded payload is faster.maxwidth. Honour it by scaling width/height proportionally on the provider side; most consumers ignore it anyway and CSS on your side is more reliable.How can I help you explore Laravel packages today?