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

Filament Title With Slug Laravel Package

blendbyte/filament-title-with-slug

Filament v5 field combining title and slug with automatic slug generation, live permalink preview, and inline editing. Supports Laravel 11–13.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install (one command, no build step)

composer require blendbyte/filament-title-with-slug

Nothing else is strictly required. In v3 the stylesheet is registered through Filament's asset system, so there is no Tailwind/Vite import to add (removing a legacy @import from a v1/v2 install is part of the upgrade).

2. Check the prerequisites before anything else

Requirement Value
Package version ^3.0 (v1/v2 are deprecated, security fixes only)
Filament ^5.0 — this will not work on Filament v4 or earlier
Laravel ^11 / ^12 / ^13
PHP 8.2+

If your app is on Filament v4, stop here — you need blendbyte/filament-title-with-slug:^2.x (namespace Camya\Filament\Forms\Components) or an app upgrade first.

3. Make sure the data model supports it

The component binds to title and slug by default, so the table needs both columns plus a unique index:

Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->string('slug')->unique();
    $table->timestamps();
});

4. First use case — drop it into a resource form

use Blendbyte\FilamentTitleWithSlug\TitleWithSlugInput;

public static function form(Form $form): Form
{
    return $form->schema([
        TitleWithSlugInput::make()->columnSpan('full'),

        // ... rest of the form
    ]);
}

That's it: you now get title input → auto slug on typing, a live permalink preview, an inline edit affordance with OK / Cancel / Reset, and a "Visit" link on the edit page.

5. Where to look first

  • The README's table of contents is the de-facto documentation — it is the only docs surface for this package. Skim All available parameters early; it is the fastest way to know what is configurable vs. what you'd have to override.

  • Publish the config and read it before hardcoding options into every resource:

    php artisan vendor:publish --tag="filament-title-with-slug-config"
    

    Project-wide defaults (urlHost, urlPath, labels, etc.) live there — set them once, override per-resource.

  • Publish translations if you ship a non-English admin panel:

    php artisan vendor:publish --tag="filament-title-with-slug-translations"
    # → lang/vendor/filament-title-with-slug
    

    Bundled: en, fr, pt_BR, de, nl, id, ar.

  • The docs/examples/*.jpg screenshots in the repo are the quickest way to see the rendered states (host hidden, readonly, dark mode, repeater) before you build.

6. Verify the read path works too

The package only handles input. Your front end still needs a route that accepts the slug — model binding by slug (getRouteKeyName()), or an explicit lookup — otherwise the "Visit" link and the permalink preview are lying to editors.

Implementation Patterns

Centralize per-resource configuration

Because make() is a long parameter list, wrap it in your own factory so each resource stays readable and you can swap the package later:

namespace App\Filament\Forms\Components;

use Blendbyte\FilamentTitleWithSlug\TitleWithSlugInput;

class PostTitleSlug
{
    public static function make(): TitleWithSlugInput
    {
        return TitleWithSlugInput::make(
            titleLabel: 'Post title',
            titlePlaceholder: 'e.g. How we shipped v2',
            slugLabel: 'Permalink',
            urlPath: '/blog/',
            titleRules: ['required', 'string', 'min:3', 'max:120'],
        )->columnSpan('full');
    }
}

This also gives you one place to fix if the namespace changes again (it already changed once from Camya\... to Blendbyte\...).

Map to non-standard column names

TitleWithSlugInput::make(
    fieldTitle: 'name',
    fieldSlug: 'identifier',
)

Use this on models where the slug column is handle, path, permalink, etc. Remember to update the table unique index to match.

Preview vs. reality: use urlVisitLinkRoute for the Visit link

The permalink preview is display-only config (urlHost, urlPath, urlHostVisible). The Visit link should be generated from your actual route so it can never drift from the app:

TitleWithSlugInput::make(
    urlHost: 'example.com',        // short host, just for the preview
    urlPath: '/product/',
    urlVisitLinkRoute: fn (?Model $record) => $record?->slug
        ? route('product.show', ['slug' => $record->slug])
        : null,
)

Because the link is route-driven, a bare host in the preview is fine — and on the create page $record is null, so the link cleanly disappears rather than pointing at a 404.

Readonly by context (the most useful day-to-day pattern)

Lock slugs once a post is published so links don't break; keep them editable while drafting:

TitleWithSlugInput::make(
    slugIsReadonly: fn ($context) => $context === 'edit',
    titleIsReadonly: fn ($context) => false,
)

With slugIsReadonly === true the slug row degrades to a static permalink display — no edit link, no OK/Cancel/Reset buttons. That is usually what you want for anything with external backlinks.

Validation: get the unique rule right early

Title rules are passed straight through; the slug gets a unique rule automatically. Customize it through slugRuleUniqueParameters, which maps to Filament's ->unique():

TitleWithSlugInput::make(
    titleRules: ['required', 'string', 'max:120'],
    slugRuleUniqueParameters: [
        'ignoreRecord' => true,
        'modifyRuleUsing' => fn (Unique $rule) => $rule->where('is_published', 1),
        'ignorable' => fn (?Model $record) => $record,
    ],
)

Use modifyRuleUsing for soft-deleted rows, tenant scoping, or "unique per parent page". Use ignorable so the current record is excluded on edit. Available keys: ignorable, modifyRuleUsing, ignoreRecord, table, column.

Custom messages go on the resource, keyed on the component state path:

protected $messages = [
    'data.slug.regex' => 'Invalid slug. Use only a–z, 0–9, and hyphens.',
];

Custom slugifier — change the rule with it

If you replace Str::slug(), replace the regex in the same call or you'll produce slugs your validator rejects:

TitleWithSlugInput::make(
    slugSlugifier: fn ($string) => preg_replace('/[^a-z]/', '', $string),
    slugRuleRegex: '/^[a-z]*$/',
)

Keep both in one wrapper method so they can never be edited apart.

Translatable titles via titleField

For spatie/filament-translatable or similar, pass the field in and let the package wire the slug generation:

TitleWithSlugInput::make(
    titleField: TranslatableInput::make('title')->label('Post title')->required(),
)

Caveat: once titleField is supplied, all title* parameters (titleLabel, titleRules, titlePlaceholder, titleExtraInputAttributes) are ignored — configure them on the field itself. titleAfterStateUpdated and titleFieldWrapper still apply:

TitleWithSlugInput::make(
    titleField: MyTranslatableField::make('title'),
    titleAfterStateUpdated: function (Set $set, string $state) {
        // runs after every title update
    },
)

Repeaters / nested relationship forms

Works inside a Repeater as long as each nested model has its own title and slug columns:

Repeater::make('FAQEntries')
    ->relationship()
    ->collapsible()
    ->schema([
        TitleWithSlugInput::make(
            fieldTitle: 'title',
            fieldSlug: 'slug',
            urlPath: '/faq/',
            urlHostVisible: false,
            titlePlaceholder: 'Insert FAQ title...',
        ),
    ]),

Nested slugs are rarely globally unique — see the gotchas below.

Table + front end integration

The component is form-only. For the list view, render the public URL yourself:

TextColumn::make('slug')
    ->formatStateUsing(fn ($state) => url("/blog/{$state}"))
    ->url(fn ($record) => route('blog.show', $record->slug))
    ->openUrlInNewTab(),

And make the front end resolve slugs consistently, e.g. public function getRouteKeyName(): string { return 'slug'; } on the model.

Homepage / root slug

For a pages resource where the front page has no path segment:

TitleWithSlugInput::make(slugRules: [])

An empty slug would be auto-regenerated, so / is used as the sentinel value for "homepage" — it bypasses regeneration. Handle '/' explicitly in your routing.

Gotchas and Tips

The namespace change is the #1 upgrade trap. Searching for TitleWithSlugInput across the codebase is not enough — grep for the old namespace Camya\Filament\Forms\Components and for any vendor:publish'd config/translations from the old package. Re-publish with --force after swapping the composer package.

Don't mix v1/v2 and v3 expectations. make() signature and parameter names are unchanged, but the CSS story differs: v3 auto-registers assets. Leaving an old manual @import in app.css produces duplicated or conflicting styles that are painful to trace. After any change here, run php artisan filament:assets (or optimize:clear) — stale Filament asset manifests are a classic "my styles didn't update" red herring.

Empty slug regenerates. You cannot have a legitimately blank slug. The documented escape hatch is slugRules: [] plus the / sentinel for a homepage. If you build any "no slug" workflow, test that it survives a title edit — an empty value will be re-slugified from the title.

Manual overrides vs. the Reset button. The inline editor exposes OK / Cancel / Reset, and Reset is the user's path back to auto-generation. Do not build your own "regenerate slug" action on top of it without testing how it interacts with the component's internal state — the component owns that cycle, not your form.

Unique rule ≠ unique index. Filament will validate uniqueness against one table/column combination, but your database index may be composite (tenant_id + slug, parent_id + slug). Set both consistently, or add modifyRuleUsing to scope the query to the tenant. This is the most common source of a production QueryException on a form that "validated fine".

Repeater uniqueness is deferred. Inside a Repeater with ->relationship(), rows may not exist in the DB at validation time, so a table-level unique rule can behave nothing like the user expects (two new rows with the same slug can both pass). Either accept duplicates inside repeaters, scope uniqueness to the parent, or validate on save.

urlHost is cosmetic; urlVisitLinkRoute is authoritative. Teams frequently hardcode urlHost: 'https://myapp.com' and then wonder why the Visit link breaks on staging or under a subdirectory install. Pass a short host for the preview and a named route for the link.

Passing titleField silently disables title* options. There's no warning — titleLabel, titleRules, titlePlaceholder, titleExtraInputAttributes just do nothing. If a title validation error isn't firing, check whether you set titleField and forgot to add ->required() to the field itself.

Custom slugifier without a matching regex ships broken data. slugSlugifier and slugRuleRegex are independent parameters. Treat them as an atomic pair, ideally inside your wrapper factory.

Scope of the package is intentionally tiny. 18 stars, 0 dependents, one active release line. That's not disqualifying — the surface area is small and the code is easy to audit / copy — but budget for it: pin the version ("blendbyte/filament-title-with-slug": "^3.0"), avoid undocumented parameters, and keep the wrapper class so that a fork, a local copy, or a hand-rolled TextInput + Set closure pair is a one-file swap if maintenance stalls. Also note the README's own caveat that PHP 8.2 is not tested against Laravel 13 — if you're on L13, run the form in a real request before committing.

Debugging checklist when the preview or slug misbehaves:

  1. php artisan vendor:publish --tag="filament-title-with-slug-config" and confirm the file reflects what you think it does — config defaults quietly override your assumptions, and unpublished config is invisible.
  2. Verify the state path: custom fieldTitle/fieldSlug change data.slug to data.<yourField>, so $messages keys and any ->afterStateUpdated() hooks must be updated too.
  3. Check that the resource form is actually rendering the package field and not a leftover TextInput::make('slug') from a half-finished migration — two fields on the same state path is a confusing failure mode.
  4. Test with a Livewire test against the actual form (livewire(EditPost::class)->fillForm([...])->assertHasErrors([...])) rather than reasoning about the component's internals; the inline edit UI is a client-state concern that's easier to verify in the browser than in a unit test.

Extension points worth knowing: titleAfterStateUpdated (post-title hook), titleFieldWrapper (wrap the title input for layout), titleExtraInputAttributes (styling without CSS overrides), slugRuleUniqueParameters (scoped uniqueness), slugSlugifier + slugRuleRegex (naming scheme), and urlVisitLinkRoute (link generation). Anything beyond those — multi-column composite slugs, slug history tables, slug redirects — is your code, not the package's; plan for it in a model observer or service class rather than trying to bend the field.

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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package