Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Php Link Preview Laravel Package

hazaveh/php-link-preview

hazaveh/php-link-preview is a PHP library that fetches a URL and extracts Open Graph and meta data to build rich link previews with title, description, image, and related details for cards and embeds.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require hazaveh/php-link-preview

This is a framework-agnostic package — there is no service provider, no config file to publish, and no migrations. Before writing anything, open vendor/hazaveh/php-link-preview/src and read the entry class plus the parser. The whole surface is a few files, and you need to know exactly (a) what the entry method is called, (b) what the returned preview object exposes, and (c) how the HTTP fetch is done, because that last part determines whether you can fake it in tests.

The shape of a first call looks like this (confirm the exact static name and accessor names against the README — they differ by version):

use Hazaveh\LinkPreview\LinkPreview;

$preview = LinkPreview::url('https://laravel.com')->get();

$preview->getTitle();       // <title> or og:title
$preview->getDescription(); // og:description or meta description
$preview->getImage();       // og:image

Your first real use case should be the one everybody builds: a preview card for a URL a user pasted into a form. Prove it end to end in tinker before wiring anything up:

php artisan tinker
>>> $p = \Hazaveh\LinkPreview\LinkPreview::url('https://github.com')->get();
>>> dump($p);

If that dump has a title, a description, and an absolute image URL, you are 80% done — the remaining work is caching, validation, and rendering, which is the same work you'd do around any scraper.

Implementation Patterns

Wrap it, don't call it from controllers. The package returns its own object, and you don't want its class name sprinkled through views and jobs. Normalize into your own DTO immediately:

namespace App\Services;

use Hazaveh\LinkPreview\LinkPreview;

class PreviewFetcher
{
    public function fetch(string $url): ?LinkPreviewData
    {
        try {
            $preview = LinkPreview::url($url)->get();
        } catch (\Throwable $e) {
            report($e);
            return null;
        }

        return new LinkPreviewData(
            url:         $url,
            title:       $this->clean($preview->getTitle()) ?: parse_url($url, PHP_URL_HOST),
            description: $this->clean($preview->getDescription()),
            image:       $this->absoluteImage($preview->getImage(), $url),
        );
    }

    private function clean(?string $s): ?string
    {
        return $s ? trim(html_entity_decode($s, ENT_QUOTES | ENT_HTML5, 'UTF-8')) : null;
    }
}

That single class is where every quirk gets absorbed: entity decoding, empty-title fallback to hostname, relative image resolution.

Cache hard, key on a normalized URL. This is an outbound network call to a third party; never do it on a page render without a cache in front. Normalize first (lowercase host, strip fragment, drop utm_*, resolve schemeless input to https://), hash it, and cache both hits and misses:

$key = 'lp:' . sha1($normalized);

return Cache::remember($key, now()->addDays(7), function () use ($normalized) {
    return $this->fetcher->fetch($normalized);
});

Use a much shorter TTL (minutes, not days) when the fetch returns null, so a temporarily-down site doesn't stay broken for a week.

Fetch asynchronously for user-submitted links. Preview generation is 1–5 seconds of blocking I/O. If a user pastes a link into a message or post, save the post immediately with a nullable preview, dispatch a queued job, and broadcast/poll for the card. This also gives you retries and a natural place to rate-limit outbound requests per host.

Store previews as rows, not just cache. A link_previews table keyed by url_hash gives you something to join against, lets you attach previews to multiple posts pointing at the same URL, and survives a cache flush:

LinkPreview::updateOrCreate(
    ['url_hash' => sha1($normalized)],
    ['url' => $normalized, 'title' => ..., 'description' => ..., 'image' => ..., 'fetched_at' => now()],
);

Layer your fallbacks in the service, not the view. In practice the parser gives you OG tags when they exist and <title> otherwise, but many sites have og:image missing while twitter:image is present, and some have neither. Decide the precedence once — og:* → twitter:* → plain <meta name> → <title> → hostname — and encode it in PreviewFetcher.

Render with a Blade component that receives the DTO and handles the null case (a bare link, no card). Guard the image with onerror or a placeholder, because a surprising number of URLs return og:image pointing at a 404.

Gotchas and Tips

SSRF is the real risk, and the package will not protect you. If the URL comes from a user (pasted link, webhook, imported feed), you are handing an arbitrary string to a server-side HTTP client. Before fetching: validate scheme is http/https, resolve the hostname, and reject private/loopback/link-local ranges (10/8, 172.16/12, 192.168/16, 127/8, 169.254/16, ::1, fc00::/7). Check the resolved IP too, not just the hostname — DNS can point anywhere — and re-check after redirects if the client follows them.

Timeouts. Default connect/read timeouts in a small scraper are often generous enough to tie up a PHP-FPM worker. If the package exposes a timeout or a client injection point, set something like 5s connect / 10s total. If it doesn't, that's your cue to wrap the fetch in your own client or fork the small amount of code.

Relative and protocol-relative image URLs. og:image is frequently /images/og.png or //cdn.example.com/x.png. Always resolve against the final URL after redirects, not the URL you requested. A tiny resolver:

private function absoluteImage(?string $img, string $base): ?string
{
    if (!$img) return null;
    if (str_starts_with($img, '//')) return 'https:' . $img;
    if (preg_match('#^https?://#i', $img)) return $img;
    return rtrim($base, '/') . '/' . ltrim($img, '/');
}

Non-HTML responses. A URL ending in .pdf, .jpg, or serving application/json will either throw or produce garbage. Check the Content-Type header before parsing when you can; otherwise treat "no title and no description" as a soft failure rather than storing an empty row.

Charset. Pages in ISO-8859-1 or Shift-JIS produce mojibake titles unless the HTML parser respects the <meta charset> or you convert explicitly. Store the title as-is only if it round-trips as valid UTF-8; mb_convert_encoding with mb_detect_encoding as a last resort.

JS-rendered sites return nothing useful. Twitter, Instagram, and most SPA marketing pages serve OG tags server-side, so they work; many app dashboards do not. Accept the fallback (title → hostname) rather than reaching for headless Chrome on day one.

HTML entities. Titles come back with &amp;, &#039;, and &mdash; intact. Decode once, in the service, with ENT_QUOTES | ENT_HTML5, and never in the view — otherwise you'll double-decode user-visible text.

Redirects change the canonical URL. If example.com/x 301s to example.com/y, cache under the requested URL (that's what the user pasted) but store the resolved one alongside it. Deduplicating on the resolved URL saves fetch volume later.

Test it by faking at the boundary, not by hitting the network. If the package's HTTP client is injectable, point it at a fake. If it isn't (likely for a package this size), the pragmatic move is to test your PreviewFetcher's post-processing directly with fixture preview objects or fixture HTML fed to the parser class, and keep the network call behind an interface so you can swap in an in-memory implementation in tests. Never let CI hit real URLs — they rate-limit, go down, and make the suite flaky.

Set a User-Agent. Many sites return 403 or a stripped-down page to requests with the default PHP UA. Pass a real browser-like UA and your success rate on stubborn sites improves measurably, even with no other changes.

Coupling risk is low but real. This is a 20-star, single-maintainer package — small, MIT, and simple enough to vendor or fork if it goes quiet. Keeping the package behind your own PreviewFetcher and DTO means replacing it later is a one-file change. Don't type the package's classes in your controllers, models, or Blade templates.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings