chriskonnertz/open-graph
A PHP library for generating Open Graph meta tags. Build and render structured og: properties for titles, descriptions, images, URLs, and more with a simple API suitable for Laravel or any PHP project, helping control how pages appear when shared on social media.
Install it with Composer — there is no Laravel auto-discovery, service provider, facade, or config file:
composer require chriskonnertz/open-graph
The entry point is ChrisKonnertz\OpenGraph\OpenGraph. The smallest useful flow is: create a builder, set the four spec-required properties (og:title, og:type, og:url, og:image), add optional properties, then call render() and echo the resulting <meta> string:
use ChrisKonnertz\OpenGraph\OpenGraph;
$og = new OpenGraph();
$og->title('Getting Started with Open Graph')
->type('article')
->url('https://example.com/posts/1')
->image('https://example.com/img/cover.jpg')
->description('A short summary for social cards.');
echo $og->render();
Where to look first:
__call magic, so signatures are not obvious from IDE autocomplete).src/OpenGraph.php for the root builder and render() behavior.src/Tags/ for the typed tag groups (Basic, Article, Book, Music, Profile, Video) and the property names each one supports.The first real use case in a Laravel app is a shared layout that renders page-specific tags. Wire a scoped binding and a view composer so every controller can contribute defaults, then override per page:
// app/Providers/AppServiceProvider.php
use ChrisKonnertz\OpenGraph\OpenGraph;
public function register(): void
{
$this->app->scoped(OpenGraph::class, function () {
$og = new OpenGraph();
$og->siteName(config('app.name'))
->locale(str_replace('-', '_', app()->getLocale()))
->type('website');
return $og;
});
}
{{-- resources/views/layouts/app.blade.php, inside <head> --}}
{!! app(\ChrisKonnertz\OpenGraph\OpenGraph::class)->render() !!}
Do not call url()->current() inside the provider's register(); the request may not be bound yet. Set request-dependent values in a middleware or view composer instead.
Keep site-wide defaults in one place and let controllers/views override only what changes:
// app/Providers/ViewServiceProvider.php
use ChrisKonnertz\OpenGraph\OpenGraph;
use Illuminate\Support\Facades\View;
View::composer('layouts.app', function ($view) {
$og = app(OpenGraph::class);
if (! $og->has('og:title')) { // or track state yourself
$og->title(config('app.name'));
}
$og->url(url()->current());
$view->with('og', $og);
});
Because the same scoped instance is shared for the request, a controller can pre-populate it before the layout renders:
public function show(Post $post)
{
app(OpenGraph::class)
->title($post->title)
->type('article')
->description(Str::limit(strip_tags($post->body), 160))
->image($post->coverUrl());
return view('posts.show', compact('post'));
}
Do not scatter property calls across controllers. Put the mapping in a small class per resource so it is testable and reusable from controllers, API resources, or jobs:
namespace App\OpenGraph;
use ChrisKonnertz\OpenGraph\OpenGraph;
class PostOpenGraph
{
public function __construct(private OpenGraph $og) {}
public function build(\App\Models\Post $post): OpenGraph
{
$this->og
->title($post->title)
->type('article')
->url(route('posts.show', $post))
->description(Str::limit($post->excerpt ?? strip_tags($post->body), 160))
->image($post->coverUrl());
// Typed tag group: check the README/src for the exact method names.
$this->og->article()
->publishedTime($post->published_at->toIso8601String())
->author($post->author->name)
->section($post->category->name);
return $this->og;
}
}
Social cards commonly need og:image:width, og:image:height, og:image:alt, og:locale:alternate, and twitter:* tags. The library focuses on Open Graph, so fall back to a generic property/tag method for these (verify the exact method name in your version):
$og->image($post->coverUrl());
$og->tag('og:image:width', '1200');
$og->tag('og:image:height', '630');
$og->tag('og:image:alt', $post->title);
$og->tag('twitter:card', 'summary_large_image');
For multiple images, determine whether repeated image() calls append or overwrite in your version. If they overwrite, build the extra tags manually with the generic method.
The builder is cheap, but you can still cache the rendered string when the underlying entity changes rarely:
$html = Cache::remember(
"og:post:{$post->id}:".app()->getLocale(),
now()->addHour(),
fn () => app(PostOpenGraph::class)->build($post)->render()
);
{!! $html !!}
Always include app()->getLocale() (and any A/B or tenant dimension) in the cache key, because og:locale and translated titles differ per locale. Do not cache rendered tags for authenticated, user-specific pages.
Treat render() as HTML and assert with XPath rather than string matching. This catches escaping and duplicate-property bugs:
$html = app(PostOpenGraph::class)->build($post)->render();
$dom = new DOMDocument();
libxml_use_internal_errors(true);
$dom->loadHTML($html);
$xp = new DOMXPath($dom);
$this->assertSame(
$post->title,
$xp->evaluate('string(//meta[@property="og:title"]/@content)')
);
$this->assertSame(1, $xp->query('//meta[@property="og:type"]')->length);
<head>; most scrapers tolerate any position, but some CMS/SEO plugins do not.og:url and og:image (route(), asset(), or a filesystem disk URL), never relative paths.<link rel="canonical">; the package does not emit it.partials.meta) instead of each layout.render() before relying on it in production; fork or wrap it if you hit deprecations.og:title, og:type, og:url, or og:image, which are the four properties the Open Graph protocol expects. Add an assertion or a required check in your builder class.render() HTML-escapes values in the version you install. If it does, do not pre-escape with e()/htmlspecialchars(), or you will double-encode (&amp;). If it does not, escape user content yourself before passing it in. Never echo raw user input into {!! !!} without knowing which behavior applies.__call, so a misspelled property may throw a generic exception or, worse, silently produce nothing depending on the tag class. Keep the property names in one builder class and cover them with tests.og:/article:/music: prefixes for you. Generic tag()/property calls usually require the full property name (og:image:alt, not image:alt). Mixing the two styles is the most common source of malformed output.en_US), not hyphens (en-US). Convert from Laravel's app()->getLocale() explicitly.music:album, and video:tag can be repeated. Understand whether a second call appends or overwrites; for genuinely nested structures (e.g. music:song with its own :disc/:track), the typed helpers may not cover every case, so fall back to generic tags.og:type vocabulary is limited. Valid types are website, article, book, profile, and the music.*/video.* families. product is widely used in the wild but is not an official type; decide deliberately and document it.og:image must be an absolute, publicly reachable URL. Add og:image:width, og:image:height, and og:image:alt via generic tags. Relative paths or authenticated URLs will fail silently in scrapers.scoped (or a fresh instance) so url()->current() and per-page values do not leak between requests under Octane or long-running workers. Resolve request-dependent values in a middleware/composer, not in register().dd($og->render()) in a route or php artisan tinker session is usually faster than reading the magic-method chain.OpenGraph for app-wide defaults, or add a small custom tag class/helper if you need a property family the package does not ship. Wrapping the package behind your own MetaBuilder interface makes it trivial to swap later if the package becomes unmaintainable.render() will catch upgrades, double-escaping, duplicate og:title tags, and accidental relative image URLs far earlier than a social-card preview will.How can I help you explore Laravel packages today?