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

Seo Bundle Laravel Package

gurtok/seo-bundle

gurtok/seo-bundle is a Laravel/PHP package for adding SEO metadata to applications. It offers helpers for meta tags, titles, descriptions, canonical links, Open Graph tags, and sitemap support to streamline basic on-page SEO setup.

View on GitHub
Deep Wiki
Context7

Getting Started

gurtok/seo-bundle gives you a single SeoManager service that renders meta tags (title, description, canonical, Open Graph, Twitter cards) from data you set per request. Start here:

  1. Install and upgrade: composer update gurtok/seo-bundle (v2.2.0 is fully backward-compatible; nothing to migrate).
  2. Find the manager first: SeoManager is the entry point. Inject it into controllers, view composers, or middleware — everything else in the bundle hangs off it.
  3. Blade output: render the tags once in your layout <head> (typically {!! $seo !!} or the package's dedicated Blade component/directive — check the package view or README for the exact render call; it is the only line you usually need).
  4. First use case: a "detail page" — set title, description, canonical URL, and a relative image path, and let 2.2.0 do the rest:
public function show(Post $post, SeoManager $seo)
{
    $seo->setTitle($post->title)
        ->setDescription($post->excerpt)
        ->setCanonical(route('posts.show', $post))
        ->setImage('/images/posts/'.$post->slug.'.jpg'); // relative — resolved to absolute in 2.2.0

    return view('posts.show', compact('post'));
}

Before 2.2.0 you had to hand-build absolute URLs for og:image/twitter:image and repeat title/description three times. Now one set of values is enough.

Implementation Patterns

Set once, inherit everywhere. The 2.2.0 fallback chain means you should stop specifying OG/Twitter variants explicitly. og:title/twitter:title derive from title; og:description/twitter:description derive from description; og:url derives from the canonical URL. Override only when the social copy genuinely differs (e.g. a shortened headline for sharing).

Centralize in a view composer or middleware. Rather than injecting SeoManager into every controller, resolve defaults once for the whole request:

// app/Providers/AppServiceProvider.php  (boot)
View::composer('layouts.app', function ($view) {
    $seo = app(SeoManager::class);
    $seo->setTitle(config('app.name'))
        ->setImage('/images/og-default.jpg'); // still works for pages that never override
    $view->with('seo', $seo);
});

Then per-page overrides in the controller mutate the same singleton instance. This is the pattern that gets you sane defaults on every page with near-zero per-page code.

Swap the resolver for a CDN or S3 disk. This is the main extension point of the release. Implement ImageUrlResolverInterface and bind it — do not edit vendor code or pre-absolutize paths at every call site:

namespace App\Seo;

use Gurtok\SeoBundle\Contracts\ImageUrlResolverInterface;

class CdnImageUrlResolver implements ImageUrlResolverInterface
{
    public function resolve(string $path): string
    {
        if (preg_match('#^(https?:)?//#', $path)) {
            return $path; // already absolute or protocol-relative
        }

        return rtrim(config('filesystems.disks.s3.url'), '/').'/'.ltrim($path, '/');
    }
}
// AppServiceProvider::register()
$this->app->bind(ImageUrlResolverInterface::class, \App\Seo\CdnImageUrlResolver::class);

Bind in AppServiceProvider (not boot) so the override is registered after the package's own default binding — last binding wins for bind(), and the package registers its default during its own provider registration. Verify with app(ImageUrlResolverInterface::class) in php artisan tinker.

Queue-safe / CLI-safe images. Anything that generates SEO output in a job or console command has no HTTP host — the default resolver's absolute-URL logic has to lean on APP_URL / a configured base. If you render SEO tags inside emails, PDFs, or sitemaps, supply your own resolver so you never depend on the ambient request.

Multi-locale and canonical hygiene. Because og:url now silently falls back to canonical, make sure setCanonical() is actually called (or that the package's default derives it from the current route). A missing canonical used to mean a missing og:url; now it can mean nothing is emitted for either — the fallback is a fallback, not a guarantee.

Gotchas and Tips

  • Your "tag absent" tests will fail. If you assert that og:title / twitter:description / og:url are missing when unset, 2.2.0 now emits them via fallbacks. This is a test-suite breakage, not a runtime breakage — update assertions to expect the inherited value.
  • Relative vs. absolute detection. images/logo.jpg, /images/logo.jpg, //cdn.example.com/x.jpg, and https://... are different inputs. Check how your resolver (and the bundled one) treats each before assuming a leading slash is required. Protocol-relative URLs (//host/...) are the classic source of a doubled https://https:// or a stray // — guard for them explicitly in any custom resolver.
  • Don't resolve twice. If you already stored an absolute URL in the database (common when migrations pre-dated 2.2.0), the resolver should pass it through untouched. A naive base_url . '/' . $path blows those up.
  • SeoManager constructor changed (optional argument). Backward-compatible for container-resolved instances, but if you new SeoManager(...) manually or have a subclass with a hard-coded constructor signature, update it. Prefer app(SeoManager::class) or a proper binding over manual instantiation.
  • Override the resolver, not the manager. Subclassing SeoManager to tweak image URLs will fight the new resolver. ImageUrlResolverInterface exists precisely so you don't have to touch the manager.
  • Debugging tip: dump the resolver output directly — app(ImageUrlResolverInterface::class)->resolve('/images/logo.jpg'). If the wrong class comes back, you have a binding-order problem in your service provider, not a package bug.
  • Cache carefully. Page caches and full-page CDN caches built before the upgrade still contain the old tag set. Clear response caches after deploying v2.2.0, and remember that a resolver depending on config() or tenant context means you cannot share cached HTML across domains.
  • Defaults are opinionated, not magic. The fallbacks are one level deep only (og:* → base field). They won't rescue an empty description. If you need computed fallbacks (excerpt from body, title from route name), set them yourself in a view composer.
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.
jinas/dhivatar
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