shweshi/opengraph
shweshi/opengraph is a Laravel package for extracting Open Graph metadata from any URL through a simple facade. Quickly retrieve titles, descriptions, images, and other OG tags to build rich link previews, social sharing cards, and SEO-friendly content.
shweshi/open-graph is a thin Laravel wrapper around a standalone Open Graph scraper: you hand it a URL, it fetches the page and returns an array of og:* metadata (title, description, image, url, type, site_name, and whatever else the page declares).
Minimal steps
composer require shweshi/open-graph — the service provider and OpenGraph facade are auto-discovered, no manual registration on modern Laravel. v1.2.1 bumps the constraint for Laravel 13, so both Laravel 12 and 13 installs are covered; no API changes came with it.php artisan vendor:publish --provider="Shweshi\OpenGraph\OpenGraphServiceProvider"
The published config/opengraph.php is small — treat the published file as the source of truth for user-agent/timeout keys rather than copying values from blog posts.use Shweshi\OpenGraph\Facades\OpenGraph;
$meta = OpenGraph::fetch('https://example.com/article');
$meta['title'] ?? null;
$meta['image'] ?? null;
Look first at the published config and the return shape from one or two real URLs (one big site, one obscure blog) before designing anything around it.
Wrap it, don't sprinkle it. Inject the package behind your own service so swapping scrapers later is a one-file change:
final class LinkPreviewFetcher
{
public function fetch(string $url): ?LinkPreview
{
try {
$meta = OpenGraph::fetch($url);
} catch (\Throwable $e) {
report($e);
return null;
}
return LinkPreview::fromArray($url, $meta);
}
}
Cache aggressively, keyed by URL. Scraping is a slow outbound HTTP call; the metadata for a given URL barely changes. Cache::remember('og:'.sha1($url), now()->addDay(), fn () => $fetcher->fetch($url)) is the single highest-value line of code in this integration.
Queue the fetch. A synchronous scrape inside a controller adds hundreds of milliseconds to seconds of latency and couples your request timeout to a third party's. Dispatch a job on form submit (or on model save), store the result on the record, and let the UI poll or simply render once present.
Store previews as polymorphic media-ish records. One link_previews table keyed by normalized URL (or a morph relation) lets multiple models share cached previews and gives you a place to record fetch status, fetched_at, and failure count.
Refresh stale previews on a schedule. A nightly job re-fetching previews older than N days keeps images and titles current without putting load on the request path.
Fall back gracefully. Many pages have partial or missing OG tags. Layer a fallback: og:title → <title> → hostname; og:image → twitter:image → placeholder. If you need that fallback, parse the HTML yourself with DOMDocument in the same job rather than adding a second scraper package.
Validate before fetching. Allow only http/https, reject credentials in the URL, and block hosts resolving to private/loopback ranges — this endpoint lets a user make your server fetch arbitrary URLs. Combine with a RateLimiter per user so nobody uses you as a scraping proxy.
Render with a Blade component. A <x-link-preview :preview="$preview" /> keeps image-ratio, rel="noopener", and lazy-loading concerns out of your controllers.
Http::fake() will not intercept it. The core library does its own HTTP transport rather than going through Laravel's HTTP client, so tests that fake the Laravel client will still hit the network. Stub your own LinkPreviewFetcher interface in tests instead — that is another reason to wrap the package.try/catch at the boundary and persist a "failed" state so you don't retry in a loop.fetch() without scheme and IP-range validation, and consider fetching only from a queue with egress restrictions.og:image values exist in the wild. The package does not reliably resolve them to absolute URLs — normalize against the page URL before storing.og:image is fine for a mock, not for production; mirror to your own storage when the preview is durable.composer update shweshi/open-graph --with-dependencies and check for a Symfony/Illuminate HTTP constraint conflict if you were previously pinned to Laravel 12. Lockfile churn, not behavior changes, is the thing to review in the diff.How can I help you explore Laravel packages today?