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

Seoable Laravel Package

marshmallow/seoable

marshmallow/seoable is a Laravel package that adds SEO metadata to Eloquent models, letting you manage meta titles, descriptions, and social sharing tags through a simple, reusable trait for consistent per-record SEO.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package via Composer and confirm which Nova versions you are on before wiring it into a production resource — the v5.2.0 release widened marshmallow/nova-fields-help to ^v2.0 || ^3.0, which is the dependency that actually decides your Nova compatibility. Check which branch Composer resolves for you:

composer require marshmallow/seoable
composer show marshmallow/nova-fields-help

Typical first steps:

  1. Understand the storage model before touching migrations. The v5.2.0 notes reference a seoable relationship (an N+1 fix landed on it), so the payload is not necessarily a flat column on your model — it may be a polymorphic/morphOne side table. Publish and read the migration stub rather than guessing:

    php artisan vendor:publish --provider="Marshmallow\Seoable\SeoableServiceProvider"
    php artisan migrate
    

    Then inspect the created table(s). If it is a relation, you do not add a seo column yourself; if it is a JSON column, you do. Confirm by saving one record and looking at the schema.

  2. Add the field to a resource. The intended usage is a single Nova field that renders all the SEO inputs at once:

    use Marshmallow\Seoable\Seoable; // verify exact namespace
    
    public function fields(Request $request)
    {
        return [
            ID::make()->sortable(),
            Text::make('Title'),
            Slug::make('Slug'),
            Seoable::make('SEO'),
        ];
    }
    
  3. First use case. Point it at a Page or Post resource where editors manage title, description, Open Graph image, canonical URL, and robots directives per record. Once the field renders, save and dump the record ($page->seoable vs $page->seo) — that single check tells you whether you are working with a relation or a cast column, which determines everything downstream.

Implementation Patterns

Eager-load the SEO relation on index screens. v5.2.0 specifically fixes an N+1 on the seoable relationship, which means the naive index/list path was querying per row. Make the eager load explicit in your resource so you are not relying on the upstream fix alone:

public static $with = ['seoable'];
// or, per-query:
Page::with('seoable')->paginate();

Use $with/with() rather than lazy access in fields(), and re-check your own custom resolve/indexQuery overrides — those are the usual places this regresses.

Store the whole SEO block on the model, render it in Blade. The field is only the admin half. Either the package ships a partial, or you write a small one that reads the SEO payload and emits <title>, <meta name="description">, canonical, and the og:* tags. Whichever route, resolve the payload through one place — a seo() accessor or a view composer — not inline in ten templates.

Pair og_image with a media library. Nova's File field stores a path, not a URL. Resolve it through your disk and make it absolute:

'og_image' => $page->seo['og_image']
    ? Storage::disk('public')->url($page->seo['og_image'])
    : asset('images/default-og.png'),

Fallbacks beat empty tags. Editors leave fields blank. Layer them in an accessor so the head is never broken:

  • meta_title → $model->title
  • meta_description → Str::limit(strip_tags($model->body), 155)
  • og_title → meta_title
  • canonical → url()->current()

Per-locale SEO. If the app is multilingual, decide up front whether the payload is {"en": {...}, "nl": {...}} or one relation row per locale, because changing it later is a data migration. Nova will not merge locales for you.

Reusable trait on resources. Wrap the resource-side wiring (field + $with + rules + defaults) in a trait so every content resource gets identical SEO setup without copy-paste drift.

Respect robots at render time. Honour index/follow toggles in the partial (<meta name="robots" content="noindex, nofollow">) and exclude those records from any sitemap generator.

Gotchas and Tips

  • The nova-fields-help constraint is the real compatibility switch. ^2.0 || ^3.0 means the package can resolve against two different Nova generations depending on what your app already requires. After composer update, run composer show marshmallow/nova-fields-help and verify you landed on the branch matching your Nova version — a silent downgrade here presents as a blank field card, not an error.
  • N+1 is fixed upstream, not automatically in your code. If you build custom index queries, filters, or a dashboard lens, keep ->with('seoable') on them; the release only covers the package's own paths.
  • Relationship vs column. If a seoable relation is in play, $fillable is irrelevant but $with and cascade deletes are not. If you also have a legacy seo JSON column from an earlier version, plan the backfill before you delete anything.
  • Nova 4 vs Nova 5 rendering. Custom field Vue components are version-sensitive. If the card renders blank, run php artisan nova:publish and rebuild assets; check whether the package ships pre-built dist/ or requires npm run prod inside vendor/.
  • Null vs empty. Payloads come back null on records created before the migration ran. Guard with $page->seo ?? [] or array_merge($defaults, $page->seo ?? []) rather than indexing directly.
  • Character limits are UX, not schema. Validate/truncate meta_description at ~155–160 chars for display, but keep the column TEXT/JSON — a VARCHAR(160) will eventually eat an editor's work.
  • Cache invalidation. Bust cache keys in the model's saved/deleted events (and on the related SEO model if it is a separate table), not inside the Nova field.
  • Absolute URLs only. Relative og:image values are silently ignored by social crawlers — test with a real share preview, not just the DOM.
  • Debugging a misbehaving field. Nova::script() in a service provider plus the browser console usually reveals a Vue prop mismatch. In tinker, compare $page->seo / $page->seoable with Seoable::make('seo')->resolve($page) to separate a data problem from a rendering one.
  • Extension points. Expect to subclass for custom meta pairs (Twitter cards, schema.org JSON-LD). Copy the component into resources/js/fields and register it rather than patching inside vendor/.
  • Still a small-dependency package. A handful of PRs and one dependency bump is not a large maintainer signal. Pin the version, and add a smoke test (Nova::actingAs($admin)->get('/nova/resources/pages/1/edit')) plus an index test asserting query count stays flat, so both a Nova upgrade and an N+1 regression are caught in CI rather than by an editor.
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.
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
noith/settings