blendbyte / filament-title-with-slug
Title + slug field for Filament v5 with auto-slug generation, live permalink preview, and inline editing.
Package info
github.com/blendbyte/filament-title-with-slug
pkg:composer/blendbyte/filament-title-with-slug
Requires
- php: ^8.2
- filament/filament: ^5.0
- illuminate/contracts: ^11.0|^12.0|^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- laravel/pint: ^1.0
- nunomaduro/larastan: ^3.0
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- phpstan/extension-installer: ^1.1
- phpstan/phpstan-deprecation-rules: ^2.0
README
Title With Slug — Permalink Field for Filament v5
Title + slug field for Filament v5 with auto-slug generation, live permalink preview, and inline editing.
use Blendbyte\FilamentTitleWithSlug\TitleWithSlugInput; TitleWithSlugInput::make()
Requirements
| Package (blendbyte) | Filament | PHP | Laravel | Namespace |
|---|---|---|---|---|
^3.x |
^5.0 |
^8.2 – ^8.5 |
^11 / ^12 / ^13* |
Blendbyte\FilamentTitleWithSlug |
^2.x |
^4.0 |
^8.2 |
^11 / ^12 |
Camya\Filament\Forms\Components |
^1.x |
^3.0 |
^8.1 |
^10 / ^11 / ^12 |
Camya\Filament\Forms\Components |
*PHP 8.2 is not tested against Laravel 13.
Looking for Filament v2 support? The original package is at camya/filament-title-with-slug.
Features
- Auto-generates a URL-safe slug from the title
- Live permalink preview (
https://example.com/blog/my-post) - Inline slug editing with OK / Cancel / Reset
- "Visit" link to the published URL
- Readonly mode for title and/or slug independently
- Custom title field — pass any Filament field (e.g. a translatable input) as the title
- Fully configurable: labels, placeholder, URL host & path, validation rules, slugifier
- Translatable UI strings (7 languages included)
- Dark mode supported
- No Tailwind build step required
Table of Contents
- Upgrading
- Installation
- Translation
- Usage & Examples
- Basic usage
- Custom title field
- Change model field names
- Change labels and placeholder
- Permalink preview: hide host
- Permalink preview: change host and path
- Visit link via named route
- Hide the Visit link
- Style the title input
- Readonly title or slug
- Validation rules
- Custom unique validation
- Custom error messages
- Custom slugifier
- Dark mode
- Empty homepage slug
- Within a repeater
- URL slug sandwich
- Slug as subdomain
- Config file defaults
- All available parameters
- Credits
Upgrading
All upgrades to v3.x share one breaking change: the namespace changed from Camya\Filament\Forms\Components (used in v1 and v2) to Blendbyte\FilamentTitleWithSlug.
Find every import of the component in your app and update it:
// Before (v1.x and v2.x) use Camya\Filament\Forms\Components\TitleWithSlugInput; // After (v3.x) use Blendbyte\FilamentTitleWithSlug\TitleWithSlugInput;
The TitleWithSlugInput::make() call signature and all parameter names are unchanged.
From v2.x (Filament v4 → v5)
composer require blendbyte/filament-title-with-slug:^3.0
- Update the namespace as shown above.
- Remove any manual CSS import for this package from your Tailwind config or Vite pipeline — v3 registers its stylesheet automatically via Filament's asset system. No build step required.
New in v3: the titleField parameter for passing a custom field (e.g. translatable) as the title input.
From v1.x (Filament v3 → v5)
composer require blendbyte/filament-title-with-slug:^3.0
- Update the namespace as shown above.
- Remove any manual CSS imports (see above).
- See the Filament upgrade guide for app-level Filament changes.
From camya/filament-title-with-slug (Filament v2)
1. Swap the package:
composer remove camya/filament-title-with-slug composer require blendbyte/filament-title-with-slug:^3.0
2. Update the namespace as shown above.
3. Re-publish config and translations if you had published them previously:
php artisan vendor:publish --tag="filament-title-with-slug-config" --force php artisan vendor:publish --tag="filament-title-with-slug-translations" --force
4. Remove any manual CSS imports — v3 registers its stylesheet automatically.
5. See the Filament upgrade guide for app-level Filament changes.
Installation
composer require blendbyte/filament-title-with-slug
Optionally publish the config file:
php artisan vendor:publish --tag="filament-title-with-slug-config"
Translation
Publish the translation files:
php artisan vendor:publish --tag="filament-title-with-slug-translations"
Published translations land at lang/vendor/filament-title-with-slug.
Included languages: English, French, Brazilian Portuguese, German, Dutch, Indonesian, Arabic.
Added a translation? Share it on our GitHub Discussions page.
Usage & Examples
Basic usage
Add TitleWithSlugInput to any Filament form schema. It binds to title and slug by default:
use Blendbyte\FilamentTitleWithSlug\TitleWithSlugInput; class PostResource extends Resource { public static function form(Form $form): Form { return $form->schema([ TitleWithSlugInput::make(), ]); } }
Tip: Use
->columnSpan('full')to make the component span the full form width.
Custom title field
Pass any Filament field as the title input via titleField. This is useful when you need a translatable input or another custom field type from a third-party package:
TitleWithSlugInput::make( titleField: \Spatie\FilamentTranslatable\Forms\Components\TranslatableInput::make('title'), )
The package automatically wires slug auto-generation onto the provided field. The field name is derived from getName(), so you don't need to pass fieldTitle separately unless you want to override it.
All title-specific parameters (titleLabel, titleRules, titlePlaceholder, etc.) are ignored when titleField is provided — configure those directly on your field before passing it in.
The titleAfterStateUpdated and titleFieldWrapper parameters still work regardless:
TitleWithSlugInput::make( titleField: MyTranslatableField::make('title') ->label('Post Title') ->required(), titleAfterStateUpdated: function (Set $set, string $state) { // runs after every title update }, )
Change model field names
Default field names are title and slug. Override them:
TitleWithSlugInput::make( fieldTitle: 'name', fieldSlug: 'identifier', )
Change labels and placeholder
TitleWithSlugInput::make( urlPath: '/book/', urlVisitLinkLabel: 'Visit Book', titleLabel: 'Title', titlePlaceholder: 'Insert the title...', slugLabel: 'Link:', )
Permalink preview: hide host
TitleWithSlugInput::make( urlHostVisible: false, )
Permalink preview: change host and path
TitleWithSlugInput::make( urlPath: '/category/', urlHost: 'https://project.local', )
Visit link via named route
Use a named route to generate the Visit link URL:
TitleWithSlugInput::make( urlPath: '/product/', urlHost: 'camya.com', urlVisitLinkRoute: fn(?Model $record) => $record?->slug ? route('product.show', ['slug' => $record->slug]) : null, )
Because the Visit URL is now route-generated, you can use a short urlHost just for the permalink preview display.
Hide the Visit link
TitleWithSlugInput::make( urlVisitLinkVisible: false, )
Style the title input
Pass extra HTML attributes directly to the title <input> element:
TitleWithSlugInput::make( titleExtraInputAttributes: ['class' => 'italic'], )
Readonly title or slug
Lock either field, optionally based on context:
TitleWithSlugInput::make( titleIsReadonly: fn($context) => $context === 'edit', slugIsReadonly: fn($context) => $context === 'edit', )
When slugIsReadonly is true the slug row renders as a static permalink display (no edit link, no action buttons).
Validation rules
TitleWithSlugInput::make( titleRules: [ 'required', 'string', 'min:3', 'max:120', ], )
A unique rule is automatically applied to the slug. To customize it, see Custom unique validation.
Custom unique validation
Pass an array of named arguments that map to Filament's ->unique() method:
TitleWithSlugInput::make( slugRuleUniqueParameters: [ 'modifyRuleUsing' => fn(Unique $rule) => $rule->where('is_published', 1), 'ignorable' => fn(?Model $record) => $record, ], )
Available keys: ignorable, modifyRuleUsing, ignoreRecord, table, column.
Custom error messages
Override validation messages in your resource class:
protected $messages = [ 'data.slug.regex' => 'Invalid slug. Use only a–z, 0–9, and hyphens.', ];
Custom slugifier
Replace the default Str::slug() with your own closure:
TitleWithSlugInput::make( slugSlugifier: fn($string) => preg_replace('/[^a-z]/', '', $string), slugRuleRegex: '/^[a-z]*$/', )
Dark mode
Empty homepage slug
Remove the slug's required rule, then use / as the slug value to represent the homepage. The / bypasses the auto-regeneration that would trigger on an empty value:
TitleWithSlugInput::make(
slugRules: [],
)
Within a repeater
Repeater::make('FAQEntries') ->relationship() ->collapsible() ->schema([ TitleWithSlugInput::make( fieldTitle: 'title', fieldSlug: 'slug', urlPath: '/faq/', urlHostVisible: false, titleLabel: 'Title', titlePlaceholder: 'Insert FAQ title...', ), ]),
URL slug sandwich
Place the slug in the middle of a path, e.g. /books/my-slug/detail/:
TitleWithSlugInput::make( urlPath: '/books/', urlVisitLinkRoute: fn(?Model $record) => $record?->slug ? '/books/' . $record->slug . '/detail' : null, slugLabelPostfix: '/detail/', urlVisitLinkLabel: 'Visit Book Details', ),
Slug as subdomain
TitleWithSlugInput::make( fieldSlug: 'subdomain', urlPath: '', urlHostVisible: false, urlVisitLinkLabel: 'Visit Domain', urlVisitLinkRoute: fn(?Model $record) => $record?->slug ? 'https://' . $record->slug . '.camya.com' : null, slugLabel: 'Domain:', slugLabelPostfix: '.camya.com', ),
Config file defaults
Publish the config to set global defaults:
php artisan vendor:publish --tag="filament-title-with-slug-config"
Published at config/filament-title-with-slug.php:
[
'field_title' => 'title', // Override per-field with fieldTitle:
'field_slug' => 'slug', // Override per-field with fieldSlug:
'url_host' => env('APP_URL'), // Override per-field with urlHost:
]
All available parameters
All parameters are optional and use named argument syntax. Parameters marked (ignored when titleField is set) only apply to the default TextInput.
TitleWithSlugInput::make( // Model fields fieldTitle: 'title', fieldSlug: 'slug', // Custom title field — replaces the default TextInput titleField: SomeField::make('title'), // URL urlPath: '/blog/', urlHost: 'https://www.example.com', urlHostVisible: true, urlVisitLinkLabel: 'View', urlVisitLinkRoute: fn(?Model $record) => $record?->slug ? route('post.show', ['slug' => $record->slug]) : null, urlVisitLinkVisible: true, // Title — ignored when titleField is provided titleLabel: 'The Title', titlePlaceholder: 'Post Title', titleExtraInputAttributes: ['class' => 'italic'], titleRules: ['required', 'string'], titleRuleUniqueParameters: [ 'modifyRuleUsing' => fn(Unique $rule) => $rule->where('is_published', 1), 'ignorable' => fn(?Model $record) => $record, ], titleIsReadonly: fn($context) => $context !== 'create', titleAutofocus: true, // Title callbacks — work with both default TextInput and custom titleField titleAfterStateUpdated: function ($state) {}, titleFieldWrapper: fn($field) => $field, // Slug slugLabel: 'The Slug:', slugRules: ['required', 'string'], slugRuleUniqueParameters: [ 'modifyRuleUsing' => fn(Unique $rule) => $rule->where('is_published', 1), 'ignorable' => fn(?Model $record) => $record, ], slugIsReadonly: fn($context) => $context !== 'create', slugSlugifier: fn($string) => Str::slug($string), slugRuleRegex: '/^[a-z0-9\-\_]*$/', slugAfterStateUpdated: function ($state) {}, slugLabelPostfix: '/suffix', )->columnSpan('full'),
Credits
Originally created by Andreas Scheibel (camya). Inspired by packages from awcodes and the work of spatie. Tests built with Pest.
Please see the release changelog for version history, and contributing for how to get involved. Security vulnerabilities can be reported via our security policy.
Maintained by Blendbyte
This project is maintained by Blendbyte — a team of engineers with 20+ years of experience building cloud infrastructure, web applications, and developer tools. We use these packages in production ourselves and actively contribute to the open source ecosystem we rely on every day. Issues and PRs are always welcome.
🌐 blendbyte.com · 📧 hello@blendbyte.com













