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

Essence Laravel Package

fg/essence

fg/essence is a PHP package for extracting media and metadata from web pages. Given a URL, it retrieves titles, descriptions, images, videos, and embedded content via Open Graph, Twitter Cards, oEmbed, and other providers—ideal for link previews and rich cards.

View on GitHub
Deep Wiki
Context7

Getting Started

Essence is a standalone HTTP scraper that turns a URL into a normalized Essence\Media object (title, description, thumbnail, embed HTML, etc.) by trying a chain of providers — oEmbed endpoints first (YouTube, Vimeo, Twitter, SoundCloud, …) and then OpenGraph / Twitter Cards / generic HTML extraction. The fg/essence package itself has no Laravel service provider, so day one is: install, bind it, and call extract().

composer require fg/essence

Where to look first:

  1. Essence\Essence — the only class you normally touch (extract, extractAll, replace).
  2. Essence\Media — the value object you render. It implements ArrayAccess, __toString() (returns html), and is json_encode-able.
  3. Essence\Di\Container — the config array keys you can override: Http, Cache, Dom, Log, providers.
  4. vendor/fg/essence/src/Provider/ — the provider classes, and the bundled oEmbed endpoint list.

Register it as a singleton so the per-request cache is shared:

// app/Providers/EssenceServiceProvider.php
public function register(): void
{
    $this->app->singleton(\Essence\Essence::class, fn () => new \Essence\Essence([
        'Cache' => new \App\Support\EssenceCache($this->app['cache']->store()),
        'Log'   => new \App\Support\EssenceLogger($this->app['log']),
    ]));
}

First use case — "paste a link, get a preview card" (link unfurling for a post composer):

$Media = app(\Essence\Essence::class)->extract($request->string('url'));

if (! $Media) {
    return response()->json(['error' => 'unsupported'], 422);
}

return response()->json([
    'type'      => $Media->type,          // video | image | rich | link | audio
    'title'     => $Media->title,
    'provider'  => $Media->providerName,
    'thumbnail' => $Media->thumbnailUrl,
    'html'      => in_array($Media->type, ['video', 'rich'], true) ? $Media->html : null,
]);

extract() returns null (not an exception) for unsupported/failed URLs — treat "no media" as a normal branch, not an error.

Implementation Patterns

Guard the outbound request. extract() performs a real HTTP GET against a user-supplied URL, which is textbook SSRF. Validate before you call it:

$url = $request->validate(['url' => ['required', 'url:http,https']])['url'];

if (! filter_var(parse_url($url, PHP_URL_HOST), FILTER_VALIDATE_IP) === false) { /* block IP literals */ }
// and resolve DNS / block 127.0.0.0/8, 10/8, 172.16/12, 192.168/16, 169.254/16, ::1

Prefer doing this in a FormRequest rule or a small IsPublicUrl rule so every call site inherits it.

Queue it, don't inline it. Extraction is 1–3 network round trips. For "unfurl every link in a submitted post", dispatch a job and store the resulting Media JSON on the model, rather than calling extract() in the request cycle.

class UnfurlLink implements ShouldQueue
{
    public function handle(\Essence\Essence $essence): void
    {
        $media = $essence->extract($this->link->url);
        $this->link->update(['preview' => $media?->properties, 'fetched_at' => now()]);
    }
}

Persist $Media->properties, not the object. Media is a plain data bag; JSON-serialize it into a json column and render from the stored copy. This is also your cache — you avoid re-hitting YouTube for every page view.

replace() for rich-text bodies. If you render user posts containing bare URLs, this scans the text and substitutes embeds:

$html = app(\Essence\Essence::class)->replace(nl2br(e($post->body)));

Because the returned string contains provider html (raw <iframe>/<blockquote>), only run this on text you control or sanitize afterwards. Don't {!! !!} the output of replace() on untrusted content without an HTML sanitizer.

Batch extraction. extractAll(array $urls) returns a map keyed by URL and shares the container cache, which is cheaper than a loop of extract() only if you've swapped in a persistent cache (see below).

Swap the cache for Laravel's. The default Essence\Cache\Volatile lives only for the current process, so every request pays full price. Implement the package cache interface against a Laravel store:

final class EssenceCache implements \Essence\Cache\CacheInterface
{
    public function __construct(private \Illuminate\Contracts\Cache\Repository $cache) {}

    private function key(string $key): string
    {
        return 'essence:'.sha1($key); // keys can be full URLs — hash for safety
    }

    public function has(string $key): bool { return $this->cache->has($this->key($key)); }
    public function get(string $key) { return $this->cache->get($this->key($key)); }
    public function set(string $key, $data): void { $this->cache->put($this->key($key), $data, now()->addHours(6)); }
}

TTL should be short-ish (hours, not days) — thumbnails, titles, and tweet text change.

Route logs into Laravel's logger. The default logger is silent, which makes "why did extraction return null?" invisible. Implement Essence\Log\Logger and forward to Log::debug() / Log::warning() with the URL in context.

Test without the network. Inject a fake Http implementation into the container so extract() reads fixtures:

$essence = new \Essence\Essence([
    'Http'    => new FakeHttpClient(resource_path('fixtures/youtube.html')),
    'Cache'   => new \Essence\Cache\Volatile(),
    'Log'     => new \Essence\Log\Logger(), // or your adapter
    'Dom'     => new \Essence\Dom\Parser(),
]);

This is also the pattern for local dev and CI where outbound HTTP is blocked.

Add your own provider (or an internal one). If you own a video platform, subclass Essence\Provider\Provider, declare the $tags it matches, and implement embed resolution. Pass the full providers array — note this replaces the defaults rather than appending, so merge your class onto the package's default list explicitly.

Gotchas and Tips

  • null is the failure mode. extract() swallows exceptions and returns null. Without a real logger you cannot distinguish "unsupported domain" from "DNS timeout" from "YouTube changed markup". Wire the logger first, before debugging anything else.
  • Protocol-relative URLs. oEmbed responses frequently return //i.ytimg.com/... for thumbnailUrl and sometimes relative url. Normalize against the provider URL before putting it in an <img src>.
  • html is third-party markup. It's what the provider gives you, which is usually fine, but it is not sanitized and can change shape without notice. Sanitize if it lands anywhere near user-editable content, and always render it inside a sandboxed container.
  • type drives rendering, and it can be absent. Handle video/rich (has html), image/audio (has url/thumbnailUrl), and link (title/description only). Never assume html is present just because extraction succeeded.
  • The bundled oEmbed provider list is static and aging. It ships inside the library; endpoints get retired and new services are never added. Since the last release was Jan 2021, expect some providers to be dead — verify the ones you actually depend on, and be ready to add your own provider class.
  • PHP version drift. The library predates PHP 8.1+'s stricter ArrayAccess/Iterator signatures and null-to-string deprecations. Run the test suite on your target PHP before committing to it; if you see deprecation noise, pin the version and suppress locally rather than patching vendor code. Wrap the whole thing behind a small LinkPreviewService interface so a replacement (or a fork) is a one-file swap.
  • Timeouts default low and are not retried. Configure the Http instance (timeout, user agent, redirect limit) in the container rather than relying on defaults; some providers behave differently to non-browser UAs. Add your own retry around the service call, not inside it.
  • The cache key includes the URL as given. http:// vs https://, youtube.com/watch?v=X vs youtu.be/X, and trailing-slash variants are all distinct entries. Normalize URLs (lowercase host, strip utm_*, resolve shorteners) before calling extract() or your cache hit rate will be poor.
  • extract() is blocking and synchronous. There's no async API and no concurrency; parallel unfurling means multiple queued jobs or a curl-multi wrapper of your own.
  • Media::__toString() returns html. Handy in Blade ({{ $media }}), but surprising when you expected the URL — cast explicitly to be obvious about it.
  • Don't trust it for anything security-relevant. Extraction follows redirects and fetches whatever it's told; that redirect chain is part of the SSRF surface. If you also pass user-controlled URLs to image proxies or thumbnails, re-validate those separately.
  • Extension points, in order of usefulness: custom Cache (biggest win), custom Log, custom Http, custom providers. There is no config file and no Laravel facade — everything is constructor injection into Essence, which is why the singleton binding is the single highest-leverage thing you'll write.
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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo