novius/laravel-filament-slug
Adds a Slug form field for Laravel Filament. Generate slugs from a TextInput, optionally conditionally, while keeping TextInput features like validation rules and unique checks. Requires PHP 8.2+, Laravel 11+, Filament 4.
Installation:
composer require novius/laravel-filament-slug
Ensure your project meets the requirements: PHP 8.2+, Laravel 11+, and Filament 4+.
First Use Case:
Add the Slug field to a Filament Resource form. For example, in a blog post resource:
use Novius\FilamentSlug\Slug;
public static function form(Form $form): Form
{
return $form->schema([
TextInput::make('title')->required(),
Slug::make('slug')
->fromField($title) // Auto-generate slug from 'title'
->required()
->unique(BlogPost::class, 'slug', ignoreRecord: true),
]);
}
Key Files to Review:
Basic Slug Generation:
Slug::make('slug')
->fromField(TextInput::make('title'))
->required();
title field using Laravel’s Str::slug().Conditional Slug Generation:
Slug::make('slug')
->fromField($title, fn (Get $get) => ! $get('is_draft'))
->required();
is_draft is true.Validation Rules:
Leverage TextInput methods for validation:
Slug::make('slug')
->fromField($title)
->regex('/^[a-z0-9-]+$/') // Custom regex
->maxLength(100)
->unique(YourModel::class, 'slug', ignoreRecord: true);
Dynamic Source Fields:
$description = TextInput::make('description');
Slug::make('slug')
->fromField($description); // Generate slug from 'description' instead
Integration with Filament Actions:
Use the Slug field in bulk edit or import actions:
public static function table(Table $table): Table
{
return $table
->columns([
// ...
])
->actions([
Tables\Actions\EditAction::make(),
]);
}
New Resource Creation:
title) during form submission."How to Build a SaaS" becomes /how-to-build-a-saas.Existing Record Updates:
ignoreRecord: true in unique() to avoid conflicts during updates.Bulk Operations:
EditInPlace) to update slugs for multiple records at once.Custom Logic:
Slug class to add custom slug generation logic (e.g., multi-field sources):
use Novius\FilamentSlug\Slug as BaseSlug;
class CustomSlug extends BaseSlug
{
protected static string $slugSeparator = '_'; // Custom separator
public static function make(string $name): static
{
return parent::make($name);
}
}
Database Constraints: Add a unique constraint to your database table to enforce slug uniqueness:
Schema::table('blog_posts', function (Blueprint $table) {
$table->string('slug')->unique();
});
SEO Optimization:
Str::of($title)->slug('-') for custom separators (e.g., hyphens).->maxLength(100)) to avoid URL truncation issues.Localization:
For multilingual slugs, handle translations in the source field (e.g., title) or use a package like spatie/laravel-translatable to manage localized slugs.
Performance:
Slug::make('slug')
->fromField($title)
->live(false); // Disable real-time updates
afterStateUpdated to defer slug generation until form submission.Testing: Test slug generation with edge cases (e.g., special characters, empty fields, duplicates):
public function test_slug_generation()
{
$this->create('BlogPost', ['title' => 'Test Title']);
$this->assertDatabaseHas('blog_posts', ['slug' => 'test-title']);
}
Duplicate Slugs:
-2 for duplicates). Handle this with:
->unique(YourModel::class, 'slug', ignoreRecord: true)
->afterStateUpdated(fn (Set $set, ?string $state) => {
if (YourModel::where('slug', $state)->exists()) {
$set('slug', $state . '-2');
}
})
Conditional Logic Quirks:
fromField() must return a boolean. Incorrect logic (e.g., returning a string) will cause errors.->fromField($title, fn (Get $get) => filled($get('title')))
Regex Validation:
->regex('/^[a-z0-9-]+$/i') // Case-insensitive, alphanumeric + hyphen
Filament Version Mismatches:
Live Updates:
live(true)) can trigger unnecessary validation or database queries. Disable for performance:
->live(false)
Source Field Changes:
title) is updated after slug generation, the slug may not reflect the latest value. Use afterStateUpdated to sync them:
$title->afterStateUpdated(fn (Set $set, ?string $state) => {
$this->form->fill('slug', Str::slug($state));
});
Slug Not Updating:
title) is bound to the form and its value is changing.Validation Errors:
required() on title).->dehydrateStateUsing() to customize slug output:
->dehydrateStateUsing(fn (?string $state) => Str::lower($state))
Unique Constraint Conflicts:
ignoreRecord: true doesn’t work, manually check for duplicates in the closure:
->afterStateUpdated(fn (Set $set, ?string $state) => {
if (YourModel::where('slug', $state)->whereNot('id', $this->record?->id)->exists()) {
$set('slug', $state . '-duplicate');
}
})
Reusable Slug Logic: Create a trait or helper for consistent slug generation across resources:
trait UsesSlugs
{
protected function makeSlugField(string $name, string $sourceField): Slug
{
return Slug::make($name)
->fromField($this->getSourceField($sourceField))
->required()
->unique(static::class, $name, ignoreRecord: true);
}
}
Client-Side Previews: Use Alpine.js or Livewire to preview slugs as users type:
<x-filament::input.wire:model="title" />
<x-filament::input wire:model="slug" readonly />
public $title, $slug;
protected function rules(): array
{
return [
'title' => 'required',
'slug' => 'required|unique:blog_posts,slug',
];
}
protected function updatedTitle(): void
{
$this->slug = Str::slug($this->title);
}
**Custom
How can I help you explore Laravel packages today?