remihin/filament-title-with-slug
A Filament form field that pairs a title input with an auto-generated, editable slug. It keeps the slug in sync as you type, lets you tweak it manually, and helps ensure clean, unique URLs for your Laravel models.
remihin/filament-title-with-slug is a single Filament form component, not a panel plugin. There is no service provider registration, no panel config entry, and no publish step required to get value out of it — it is a drop-in replacement for the title + slug TextInput pair on a resource form.
composer require remihin/filament-title-with-slugfilament/filament version already in your lockfile before you write code — this is the single most common reason a copied README snippet fails.title and slug columns. slug should be indexed (unique where the domain allows), and if you resolve records by slug on the front end, getRouteKeyName() should return 'slug'.use RemiHin\FilamentTitleWithSlug\FilamentTitleWithSlugInput;
public static function form(Form $form): Form
{
return $form->schema([
FilamentTitleWithSlugInput::make(
fieldTitle: 'title',
fieldSlug: 'slug',
urlPath: '/posts/',
urlHost: config('app.url'),
),
]);
}
Where to look first: the README usage block for the exact named arguments on your Filament version, then the package's Blade view in vendor/ the moment you need to change markup or debug why the slug stopped updating. Publish views and translations before customising so your diff stays reviewable across upgrades.
Best first use case: a low-traffic internal resource — a Pages or Categories model with existing, already-populated slugs. You get the live URL preview and auto-derivation with a trivial blast radius if the preview formatting is wrong.
Release 1.0.2 is a patch on top of 1.0.1 with a changelog containing only a compare link — treat it as a safe upgrade for existing usage, but read the diff yourself if slug normalisation matters to you.
Wrap it in a shared factory. You will use this component on more than one resource. Put the conventions in one place — a method on a base resource class or a trait — so the URL preview format, labels, and column span are consistent:
protected static function titleWithSlug(string $path): FilamentTitleWithSlugInput
{
return FilamentTitleWithSlugInput::make(
fieldTitle: 'title',
fieldSlug: 'slug',
urlPath: $path,
urlHost: config('app.url'),
)->columnSpanFull();
}
Then static::titleWithSlug('/posts/') in each form().
It works anywhere a Form schema works. Resource create/edit pages, custom pages, and modal forms on table actions. The only requirement is that the underlying Livewire component has title and slug as form state — on a custom page that means your $data array keys must match fieldTitle / fieldSlug.
Keep the slug column as an ordinary attribute. Because the component is presentation-only, your table columns, filters, search, and route binding all keep working with plain TextColumn::make('slug'). Don't reach for anything custom on the read side.
Own uniqueness explicitly. The component generates a candidate string; it does not enforce domain rules. Pair it with Rule::unique('posts', 'slug')->ignore($record) plus a database unique index. Concurrent admin saves are rare but they are the failure mode that produces a 500 instead of a form error.
Decide regeneration policy per resource. Existing slugs are preserved so published URLs don't silently break — that's the right default for SEO, but it means renaming a post does not change its URL. If a resource genuinely needs the slug to follow the title (draft-only content, internal tools), make that an explicit, documented choice rather than assuming the component does it.
Multi-locale: one component per locale. Don't try to reuse a single field across translations. Instantiate with distinct field names (slug_en, slug_fr) alongside their title counterparts and keep the uniqueness rule scoped to the locale.
Testing. The auto-derivation is browser-side, so fillForm(['title' => 'My Post']) in a Livewire test will not produce a slug. Set slug explicitly in test and factory data, and reserve your component tests for the rendered markup and the manual-override path.
composer.json and check the upgrade notes before bumping Filament — a small package typically lags a Filament major release.->hidden() section, a collapsed repeater, or a conditionally-rendered tab may never fire the auto-fill. If the slug stays empty, that's the first thing to check — not the PHP.fieldTitle / fieldSlug don't match your actual form state keys, nothing throws; the slug simply never populates. Inspect the rendered markup for the Alpine bindings and the wire:model names before digging deeper.required, maxLength, and uniqueness on the underlying fields or in a FormRequest, and test the failing path — not just the happy path.urlHost is not inferred from the panel. On staging, multi-tenant installs, or a separate admin domain, the preview will lie until you pass an explicit host. A wrong preview is cosmetic but it erodes editor trust in the field quickly.How can I help you explore Laravel packages today?