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

Simple Seo Bundle Laravel Package

ibrows/simple-seo-bundle

A lightweight Symfony bundle for basic SEO management, helping you define page titles, meta descriptions, and related metadata through simple configuration. Easy to install and integrate into existing Symfony applications.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the reality check that shapes everything below: ibrows/simple-seo-bundle is a Symfony bundle, not a Laravel package. Its integration surface is config/bundles.php (or legacy AppKernel::registerBundles()), DI service definitions, Doctrine ORM entity mapping, a config tree, and a Twig extension. None of that boots under Laravel's service container. So as a Laravel developer, your day-to-day leverage is the pattern, not the package — you reimplement the equivalent in ~2–5 days and own it outright. The rest of this section shows both paths honestly: what using the bundle looks like if you're actually on (legacy) Symfony, and what the Laravel translation looks like.

If you're genuinely on Symfony

  1. Audit composer.json before anything else. This bundle has 0 stars, 0 dependents, and no visible release cadence — expect a PHP 5.x/7.x floor and Symfony 2.x/3.x constraints. If composer require ibrows/simple-seo-bundle doesn't resolve against your lockfile, the decision is already made for you.
  2. Register the bundle. Modern: add IbrowsSimpleSeoBundle::class => ['all' => true] to config/bundles.php. Legacy: add new Ibrows\SimpleSeoBundle\IbrowsSimpleSeoBundle() to AppKernel::registerBundles().
  3. Configure site-wide defaults. The bundle reads from a config tree (typically config/packages/ibrows_simple_seo.yaml or legacy config.yml under ibrows_simple_seo:). Put your global fallback title, description, and keywords here — this is the "every page has sane metadata even if nobody set anything" tier.
  4. Where to look first in the source: DependencyInjection/ (config tree + service wiring), Entity/ (the metadata record — this tells you whether rows are keyed by URL/path, route name, or per-entity columns), Twig/ (the renderer), and Resources/ (templates and any admin routes).
  5. First use case (30 minutes): set global defaults, override the title/description in one controller, render in base.html.twig, confirm the override beats the default.

If you're on Laravel (the realistic case)

You are not installing this package. You are extracting its schema idea into three files:

app/Models/SeoMetadata.php          // route_name/url_path, locale, title, description, keywords, canonical, robots
app/Services/SeoResolver.php       // route/URL lookup -> model-level -> global default -> null
resources/views/components/seo.blade.php  // <title> + <meta> rendering, escaped

Ship the read path first with defaults so no page regresses, then add authoring (Filament/Nova CRUD) as a separate ticket. 80% of the real effort is admin authoring, and the Symfony bundle's UI cannot help you there.

Implementation Patterns

The three-tier resolution pattern (the actual value of this package)

The bundle's design is a fallback hierarchy. Copy the hierarchy, not the code:

  1. Explicit override — a row in seo_metadata for the current route name (or URL path) + locale.
  2. Contextual fallback — the model being rendered ($post->title, $product->name) plus its excerpt.
  3. Global default — from config, guaranteed non-empty.

Whichever tier wins, the renderer always emits something. The single biggest failure mode of hand-rolled SEO is a tier returning null and the template rendering <title></title> with no error.

Symfony usage pattern

// In a controller — override for this page
$seo->setTitle('Checkout — Acme');
$seo->setDescription('Secure checkout in under two minutes.');
$seo->setCanonical($this->generateUrl('checkout', [], UrlGeneratorInterface::ABSOLUTE_URL));
{# base.html.twig — the extension emits tags with defaults applied #}
{{ seo_render_meta_tags() }}

The day-to-day workflow is: defaults in config → per-controller overrides for the handful of pages that matter → admin CRUD for the rest.

Laravel translation — concrete shape

Schema: one table, unique index on (url_path, locale) — or better, (route_name, params_hash, locale) if you want slug-rename safety (see Gotchas).

Schema::create('seo_metadata', function (Blueprint $t) {
    $t->id();
    $t->string('route_name')->nullable()->index();
    $t->string('url_path')->nullable()->index();
    $t->string('locale', 10)->default('en');
    $t->string('title')->nullable();
    $t->text('description')->nullable();
    $t->string('keywords')->nullable();
    $t->string('canonical')->nullable();
    $t->string('robots')->default('index,follow');
    $t->timestamps();
    $t->unique(['url_path', 'locale']);
});

Resolver service:

class SeoResolver
{
    public function __construct(private SeoMetadata $model) {}

    public function resolve(string $route, ?string $path = null, string $locale = 'en'): SeoData
    {
        $row = Cache::remember(
            "seo:{$locale}:{$route}",
            now()->addDay(),
            fn () => $this->model->where('route_name', $route)
                ->orWhere('url_path', $path)
                ->where('locale', $locale)
                ->first()
        );

        return SeoData::from([
            'title'       => $row?->title ?? config('seo.default_title'),
            'description' => $row?->description ?? config('seo.default_description'),
            'canonical'   => $row?->canonical ?? url()->current(),
            'robots'      => $row?->robots ?? 'index,follow',
        ]);
    }
}

Blade component (escape-by-default):

<title>{{ $seo->title }}</title>
<meta name="description" content="{{ $seo->description }}">
<link rel="canonical" href="{{ $seo->canonical }}">
<meta name="robots" content="{{ $seo->robots }}">

Cache invalidation: forget seo:{$locale}:{$route} in the model's saved/deleted events and on any slug-change observer. A per-route cache key without invalidation is how stale metadata lives forever.

Authoring tier

Add Filament/Nova resource (or a plain CRUD) for seo_metadata. This is the piece the bundle would have given you and the piece you most need to build. Backfill top-N revenue URLs first, then the long tail.

Gotchas and Tips

  • Do not composer require this into a Laravel app "just to reference it." You'll pull in Symfony components you never invoke, expand the version-conflict surface for zero runtime benefit, and inherit an owner-less dependency. Copy the schema into a design doc; leave the package out of composer.json.
  • No community = no CVE path, no Symfony-compatibility PRs, no one to ask. At 0 stars / 0 dependents there is no evidence this code has survived contact with a production system anyone still runs. If you must use it on Symfony, fork it into your org on day one, pin the commit, record provenance, and assign an owner.
  • The path-keyed failure mode. If metadata rows are keyed by URL path (a strong likelihood given "per route or controller"), a slug rename orphans rows silently — the old row lingers, the new URL falls back to defaults. Prefer a route_name key, or add a rename hook that migrates rows on slug change.
  • Silent SEO regressions. Resolution returning null produces an empty <title> with no exception. Mitigate with enforced config defaults plus a monitoring check (e.g. a scheduled assertion that your top-N URLs return non-empty title and description).
  • Escape the output. Descriptions rendered "raw" (a common shortcut for allowing light markup) are an XSS vector. Escape by default; if you need limited HTML, use an explicit allowlist sanitizer.
  • Doctrine coupling (Symfony path). The entity mapping is tied to Doctrine ORM major versions. Across Symfony 3→4→5→6, Doctrine DBAL/ORM APIs changed materially — a "version bump" is really a port. Budget accordingly rather than treating composer update as the fix.
  • Scope is narrower than the word "SEO" suggests. This covers the three classic meta fields plus canonical. It does not cover sitemaps, JSON-LD/structured data, hreflang, redirect management, Open Graph/Twitter cards, or Core Web Vitals. Do not let a roadmap checkbox say "SEO: done" because this is wired up — that conflates meta tags with organic performance.
  • Multi-locale is a trap if you're not deliberate. Fallback tiers multiply the chance of duplicate titles/descriptions across locales. Add a uniqueness check (title per locale) at the authoring layer, not just in code review.
  • Extension point worth keeping: the bundle's "extensible structure for custom SEO fields" is a hint. In your reimplementation, model extras (og_image, og_type, hreflang) as nullable columns rather than a serialized JSON blob — they're queryable and migrate cleanly.
  • Testing. With no upstream tests to lean on, write your own: one test asserting the fallback chain (override → model → default), one asserting escaping, one asserting cache invalidation on save. That's the whole surface, and it's cheap insurance for a piece of code every page depends on.
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.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route