dusterio/link-preview
A Laravel package for generating rich link previews from URLs. It extracts Open Graph and page metadata such as title, description, and image, letting you render Facebook-style link cards in your application.
composer require dusterio/link-preview
On Laravel 5.5+ the service provider is auto-discovered. On older versions, register the package provider in config/app.php. If the package ships a config file, publish it with php artisan vendor:publish and check it for timeout / user-agent options before doing anything else.
The whole API is one call:
use Dusterio\LinkPreview\Client;
$preview = (new Client('https://laravel.com'))->getPreview('og');
$preview->getUrl();
$preview->getTitle();
$preview->getDescription();
$preview->getCover(); // the preview image URL
getPreview('og') asks for the Open Graph generator; the client fetches the page, parses the HTML, and hands back a Preview value object.
Where to look first, in this order:
src/Client.php — the entrypoint. See how it wires a parser to a generator, and what it throws.src/Parsers/ — the class that actually makes the HTTP request and parses the DOM. This is where timeouts and user-agent live.src/Generators/ — how raw meta tags become title/description/cover.src/Models/Preview.php — the DTO you will end up serialising to JSON.First use case: a "paste a URL" field in a comment box or admin panel that renders a Slack-style card. Get that working end-to-end (input → Client → array → Blade partial) before touching caching or queues — it will immediately show you which sites return nothing.
Wrap it in a service, never call it from a controller or a Blade view.
final class LinkPreviewService
{
public function for(string $url): ?array
{
try {
$preview = (new Client($url))->getPreview('og');
return [
'url' => $preview->getUrl() ?: $url,
'title' => $preview->getTitle(),
'description' => $preview->getDescription(),
'image' => $preview->getCover(),
];
} catch (\Throwable $e) {
report($e);
return null;
}
}
}
Returning null on failure (rather than throwing upward) keeps the caller simple and makes it obvious where the "no preview available" branch is rendered.
Cache aggressively — the same handful of URLs get pasted repeatedly. Cache::remember('preview:'.sha1($url), now()->addDay(), ...) is enough for most apps and cuts remote requests by an order of magnitude. Cache the null failure case too, but with a short TTL (5–15 minutes) so a temporarily-down host doesn't get retried on every keystroke.
Queue anything user-triggered. A form submission should dispatch a job and return immediately; the job fills the cache. The UI then polls or renders a placeholder.
dispatch(new FetchLinkPreview($url));
Persist the resolved array, not the object. Store it in a json column on the model that owns the link (comment, bookmark, ticket) so the card survives the source site changing or disappearing, and so listing pages never make outbound calls.
Render one Blade partial and reuse it everywhere — a _card.blade.php that gracefully degrades to a bare link when title or image is missing. Escape everything with {{ }}; the title comes from a third party.
Layer a fallback. Try the Open Graph generator first; if the title comes back empty, fall back to a plain meta-tags extraction or to the URL's host as the display label. Sites without OG tags are common enough that you will ship this on day two.
Extension point: if you need a tag the package doesn't surface (e.g. og:video), copy the pattern of the shipped generator — read the meta tags from the parsed document, populate a Preview, and register your generator with the client. Keep that class in your app, not in vendor/.
getPreview() makes a live HTTP request on the calling process. On a web request that's a slow-user-visible hang; behind a queue it's fine.169.254.169.254 or an internal host. Validate the scheme, resolve DNS and reject private/link-local ranges, or route fetches through a proxy you control.connect_timeout and timeout to a few seconds each.composer why-not guzzlehttp/guzzle ^7 and run the test suite before upgrading PHP. Pin the package version in composer.json rather than floating.og:image can be relative or protocol-relative (//cdn.example.com/x.jpg). Normalise to an absolute URL before storing, or your <img> breaks.mb_convert_encoding($s, 'UTF-8', 'auto') if you see it.LinkPreviewService interface lets you swap in embed/embed or a headless fetcher later without touching call sites.How can I help you explore Laravel packages today?