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

Seokit Laravel Package

larament/seokit

SeoKit is a Laravel toolkit for managing meta tags, Open Graph, Twitter cards, and JSON-LD through one clean API, with model traits, caching, and polymorphic SEO records.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and run the installer — it publishes both the config file and the migration, so there is no reason to publish them by hand:

composer require larament/seokit
php artisan seokit:install
php artisan migrate

The installer is the first place to look, because everything else hangs off two published files: config/seokit.php (site name, default image disk, caching, defaults) and the seo table migration. Open the config immediately and confirm site_name — since v1.1.0 it defaults to config('app.name'), which is fine for most apps but wrong if your marketing name differs from the application name. Also check the disk key (or SEOKIT_DISK env var); when null it inherits config('filesystems.default'), which is the behaviour you usually want.

Then add the directive to your single layout, inside <head>:

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    @seoKit
</head>

The first real use case is the controller-driven one — it requires no schema changes and no model work:

use Larament\SeoKit\Facades\SeoKit;

public function show(Post $post)
{
    SeoKit::title($post->title)
        ->description($post->excerpt)
        ->image($post->featured_image)
        ->canonical(route('posts.show', $post));

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

That is a complete, shippable SEO layer for a brochure site or blog. Stop there until you actually need per-record editorial control; the database-backed path is additive, not a prerequisite.

Orientation note: the README is deliberately thin and points at the GitHub wiki (Getting Started, Core Concepts, Advanced SEO Management, Database-Backed SEO, Cookbooks and Examples). Read the wiki, not the README, before designing anything non-trivial. There is also skills/seokit-development/SKILL.md in the repo, which is a compact full-API reference and is genuinely faster to scan than the wiki for method signatures.

Implementation Patterns

Facade-first for dynamic pages. The facade is a per-request singleton that accumulates state, so set it in the controller, a middleware, or a view composer — anywhere that runs before the layout renders. For a blog, the controller is right. For a resource with a shared parent (e.g. a product inside a category), prefer a small SeoService or a view()->composer() so the fallback chain lives in one place rather than being copy-pasted across fifteen controllers.

Base defaults via middleware, overrides via controllers. A clean pattern is a SetDefaultSeo middleware that sets title suffix behaviour, default OG image, and site-level Twitter handle once, then let controllers override per page. This keeps the "every page has an image" guarantee without repeating it.

Model-driven SEO with HasSeo. Add the trait to models that need editorial control, and define a fallback so records without a seo row still render correctly:

use Larament\SeoKit\Traits\HasSeo;

class Post extends Model
{
    use HasSeo;

    public function fallbackSeoData(): SeoData
    {
        return SeoData::make()
            ->title($this->title)
            ->description(Str::limit(strip_tags($this->body), 155))
            ->image($this->featured_image)
            ->canonical(route('posts.show', $this));
    }
}

The merge semantics matter: non-empty database values win, fallback fills the gaps. As of v1.3.1, fallback applies even when no seo relationship row exists at all — before that fix, the fallback was silently dropped. Make sure your fallback returns SeoData, and use the isEmpty() helper if you need to branch on whether an editor actually filled anything in.

Two traits, two jobs. HasSeo gives you the polymorphic seo() relationship plus tag preparation. HasSeoData is the lighter-weight one for models that supply SEO data programmatically without wanting a persisted relation. Don't add both reflexively — pick based on whether editors need to override values in the admin panel.

Disk handling is automatic on the Seo model. Since v1.7.0, creating a record with a relative image path persists the configured default disk into og_image_disk / twitter_image_disk; passing a full external URL (detected via Str::isUrl()) clears the disk column to null. This means you can write:

$post->seo()->create([
    'title' => $request->title,
    'og_image' => 'seo/posts/cover.jpg', // disk inferred
]);

and it just works. Only specify a disk explicitly when the image lives somewhere other than the default.

Eager-load the relation. $post->seo inside prepareSeoTags() is a lazy query per model. On any index or listing page that renders SEO for multiple records, add ->with('seo') — otherwise you get an N+1 that is invisible until you look at the query log.

Inertia. Since v1.1.0 the package detects Inertia routes and adjusts title rendering, but @seoKit is a Blade directive — it cannot render inside a Vue/React head. Either keep a Blade root template that hosts the directive and let Inertia swap the body, or expose MetaTags::toArray() (which includes the title as of v1.5.0) as an Inertia prop and render tags client-side via your head component. Pick one and be consistent; mixing them produces duplicate <title> elements.

Keywords. MetaTags::keywords() exists and there is a keywords column as of v1.4.0. Use it if you have a legacy/internal requirement, but do not build a workflow around it — Google has ignored the meta keywords tag for well over a decade.

Gotchas and Tips

The directive renders once per layout. If you have multiple layouts (app, auth, error, mail-preview) each needs @seoKit in <head>, and none of them should be included twice in a single response. Duplicate meta tags from a nested layout inclusion is the single most common bug with packages like this.

Ordering matters with the facade. Because state accumulates on a singleton, calling SeoKit::title() in a middleware that runs after the response is built does nothing. If a page's tags look stale or empty, dd(SeoKit::toArray()) in the controller first, then check middleware order, then check whether a view composer is overwriting you.

Long-lived workers. Under Octane, RoadRunner, or Swoole the facade singleton may persist across requests unless the package resets it. If you see one page's title bleeding into the next, that is the cause — verify reset behaviour before deploying to an Octane setup, and clear state in a terminating middleware if needed.

Caching is built in — so invalidate it. The package caches SEO resolution to avoid queries. After an editor updates a Seo record, if the front-end still shows the old title, the cache is the culprit, not the merge logic. Hook your model's saved/deleted events to flush, or configure a short TTL.

Config cache. After vendor:publish, run php artisan config:clear in development. A stale cached seokit.site_name or seokit.disk after an env change is a classic half-hour of confusion.

Canonical is not automatic. Nothing generates a sensible canonical for you in the general case, and paginated or filtered URLs (?page=2, ?utm_source=..., ?sort=price) will happily self-canonicalise. Set canonical() explicitly on any listing page, and be deliberate about whether paginated pages canonicalise to themselves or to page one.

Fallbacks can mask missing data. Because fallback merge is silent, a record with a hand-written description and a fallback-generated title looks identical to a fully-editorialised one. If the content team needs to know what is still unedited, expose isEmpty() on the raw seo relation in the admin UI rather than reading the merged result.

Image paths vs URLs is the sharp edge. Passing storage/posts/x.jpg when you meant https://cdn.example.com/x.jpg will get treated as a disk path and generate a URL against your filesystem disk — usually a 404 in production where the local disk isn't the CDN. Be consistent about which field you store what in, and remember v1.7.0's Str::isUrl() check is the switch.

Scope check. SeoKit covers meta tags, OG, Twitter cards, and JSON-LD. It does not generate sitemap.xml or robots.txt. If your SEO plan assumes those are included, budget for spatie/laravel-sitemap (or equivalent) separately — discovering this mid-sprint is common with "complete SEO package" framing.

Testing. Assert on rendered HTML rather than internals: $response->assertSee('<meta property="og:title"', false) and check the @seoKit output in a feature test per route group. Because the facade is stateful, use refreshApplication() between assertions if you test multiple pages in one test method.

Extension points. MetaTags is the object you want to reach for customisation — getTitle() and toArray() (v1.5.0) let you build your own renderer or Inertia prop without forking. If you need a tag type the package doesn't emit, render it manually alongside @seoKit rather than fighting the API.

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.
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
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager