
Summary
Permalink field for Filament: title + slug in one block, live URL preview, inline editing, Copy/Visit/Regenerate buttons, and uniqueness validation.Spatielaravel-sluggableis optional. By default, the slug is generated from the title usingStr::slug()in the browser and saved to the database like a regular form field. You only need to add the Spatie package if you want the same rules as on model saving (such as-2,-3suffixes,preventOverwrite, etc.).
Start here — integration without Spatie (default)
No extra packages required besidesfilament-flex-fields. The model does not need any traits or slug options—just a database column and $fillable configuration.
Who is responsible for what
Checklist — 4 steps
Step 1 — Migration
Step 2 — Model (minimal, without Spatie)
use HasSluganigetSlugOptions()- an observer generating the slug
- mutatora
setSlugAttribute composer require spatie/laravel-sluggable
title—from the form data.
Step 3 — Filament Resource (minimum)
FusedGroup.
Step 4 — Config (optional, for permalink path)
If you want to seehttps://your-domain.com/blog/my-post under the slug:
Parameters of TitleSlugField::make() — what is available?
What happens automatically (without Spatie)
Slug generation without Spatie (technical)
Four ways to add title + slug
When to switch to Spatie?
Only add Spatie when you need model-level hooks that the form alone cannot handle:- automatic
-2,-3suffixes on database collisions preventOverwrite— never overwrite the slug after publishskipGenerateWhen,extraScope, multiple source fields- using the same
SlugOptionsin form preview and onsave()
laravel-sluggable integration.
Common issues (without Spatie)
Next sections: Default form layout → Installation → Config → Full Example → Spatie Integration
Default form layout (FusedGroup)
TitleSlugField::make() always returns Filament\Schemas\Components\FusedGroup — the same layout with or without parameters:
Important:spatieModelchanges only the slug preview generation logic (server +SlugOptions). It does not change the form layout.
What is inside FusedGroup
The group has CSS class
fff-title-slug-fused-group (without the standard Filament border between fields).
Default appearance (ASCII)
Whenconfig('filament-flex-fields.slug.url_host') is set (e.g. APP_URL):
url_host is null (no URL preview in config):
Table of default visual values
The same layout — three ways to call
Installation and assets
The package ships insidejanczakb/filament-flex-fields. The path without Spatie only needs the package assets — no extra composer require.
Optional: Spatie (only when you need it)
Install only if the model usesHasSlug / getSlugOptions() and you want matching form preview:
TitleSlugField::make() still works fully — generation uses browser Str::slug() / client slugify.
Package configuration (config/filament-flex-fields.php)
Publish the config:
Full Example from scratch (migration -> model -> Resource)
Continuation of the Start here — integration without Spatie section. Steps 1–3 are the minimum; step 4 (Spatie) is optional.
1. Migration
2. Model (without Spatie — enough for production)
HasSlug or getSlugOptions() — unless you move to step 4 below.
3. Filament Resource (without Spatie)
4. (Optional) The same Resource with Spatie
Add this step only when you need suffixes,preventOverwrite, or other SlugOptions rules during save.
Two layers: the form shows a live slug preview; on save SpatieHasSlugmay add a suffix (-2) or applypreventOverwrite— that is expected.
Cookbook — typical scenarios
Scenario 1: Blog — create + edit (default behaviour)
- Create: title → slug live.
- Edit: changing the title does not change the slug.
Scenario 2: Always sync slug with title
Scenario 3: Slug read-only on edit
Scenario 4: Custom title (RichEditor) + slug
Scenario 5: Slug uniqueness within tenant
Scenario 6: No permalink — slug field only
Scenario 7: CMS Homepage (/)
Standalone SlugField (matches playground Homepage slug):
TitleSlugField:
Scenario 8: Repeater — row with title and slug
sections.0.title → sections.0.slug) are resolved automatically.
Scenario 9: Manual slug only (no title, no auto-generate)
Use when there is no title field — user types the slug by hand. This is not the same as playgroundslug__standalone (that name means “slug field alone in the layout”, but it still uses ->source('title')).
Scenario 10: Form read-only (whole field)
Matches playground Form readonly:Scenario 11: Spatie + multiple source fields (optional package)
Quick Start — Filament Resource (create + edit)
Summary of the Start here section — without Spatie:How slug generation works
When Spatie is configured, the field uses the same
SlugOptions as your model so previews match production rules (separator, language, max length, multi-field sources, extraScope, suffix start, Closure sources).
Create vs edit
Default (preserve slug on edit)
Always sync slug from title
Read-only slug on edit only
Read-only title
Universal locale tabs: For any translatable attribute (title, body, metadata, …), use the dedicated TranslatableFields component. The section below covers TitleSlugField only — translatable titles with a single shared slug.
Translatable titles (single slug)
For generic translatable fields (body, excerpt, metadata, …) without slug coupling, use TranslatableFields instead.Optional multi-language title with one shared slug. Locale switching uses package
TranslatableFields (built on SegmentTabs).
What is implemented today
spatieModel≠ Spatie Translatable.spatieModelonTitleSlugFieldis for Spatie Sluggable (HasSlug). For translations usetranslatableLocales(+ optionalspatieTranslatable).
Basic usage
slugSourceLocale tab drives permalink generation.
Storage without Spatie (recommended minimum)
title.pl / title.en into a title array. No extra glue code required.
Storage with Spatie laravel-translatable (optional)
HasTranslations and the package is installed, each tab is hydrated via getTranslation(). Otherwise tabs read the raw JSON attribute.
On save: the nested title array from the form is assigned to the model; Spatie JSON-encodes translatable attributes automatically.
Global defaults (config/filament-flex-fields.php)
translatableLocales is omitted in TitleSlugField::make(), locales are read from config('filament-flex-fields.slug.translatable_locales').
Required title locales
By default only the slug source locale title is required. Optional locales can stay empty on create/edit.required_title_locales (null, 'all', or list of locale codes).
Per-locale title customization
Full TranslatableFields passthrough
When translatableLocales is set, title tabs are built with TranslatableFields internally. By default the factory enables directionByLocale() (RTL/LTR on the title input, not the label — see TranslatableFields / directionByLocale) and emptyBadgeWhenAllFieldsAreEmpty() (warning empty badge on tabs where the title is blank). The active tab stays on slugSourceLocale, not activeTabWithValue().
Use translatableFieldsConfigurator for further tweaks (activeTabWithValue(), bordered panels, custom tab icons, localeFieldUsing(), storageAttributeUsing(), etc.):
getSourceStatePath() resolves to title.{slugSourceLocale} automatically (e.g. title.pl).
SlugField translatable API
FlexField schema config
Slug generation locale
Translatable titles always use server-side slug preview (generateSlugPreview / Str::slug with slugSourceLocale). Alpine receives serverGenerate: true and slugSourceLocale so live preview matches PHP — including Polish diacritics (Łódź → lodz), which generic browser ASCII folding cannot handle reliably.
Spatie laravel-sluggable integration (v4.x)
Not required. If the default path without Spatie is sufficient, you can skip this section entirely.Tested with
spatie/laravel-sluggable ^4.0 (currently 4.0.2). The integration uses official Spatie v4 classes:
Spatie\Sluggable\Actions\GenerateSlugAction(viaconfig/sluggable.php→actions.generate_slug)Spatie\Sluggable\Support\SluggableAttributeResolverdla modeli z#[Sluggable]bezgetSlugOptions()Spatie\Sluggable\Support\Config::getAction()— ten sam resolver akcji co traitHasSlug
preventOverwrite). The form can display the same preview as the model — just pass spatieModel.
Optional dependency isolation (technical)
spatie/laravel-sluggable is in composer.json → suggest, not require. The package does not force installing Spatie.
Optional. Install when you want model-driven slug rules:
Minimal model (trait — klasycznie)
Minimal model (v4 attribute — without getSlugOptions())
Wire the form (zero extra config)
Spatie in the form is enabled only when:- podasz
spatieModel, i - model ma
getSlugOptions()lub atrybut#[Sluggable]z rozpoznawalnymi opcjami.
Post as the Resource model does not enable Spatie automatically — the slug configuration must exist on the model.
spatieModel:
Or on standalone
SlugField:
Explicit Spatie field mapping
When form field names differ from model attributes:Supported Spatie SlugOptions features in preview
Multi-field source example
extraScope / skipGenerateWhen (e.g., tenant_id, status) filled out — SlugField reads them from the live form state (data.*), not just the source fields.
Scoped unique slugs (Spatie extraScope)
Attribute-based model (Spatie v4+)
getSlugOptions() required — SlugField reads the attribute when the method is absent.
Override Spatie for preview only
slugifyUsing() always wins over Spatie.
Force server-side preview
Spatie mode and translatable titles already use server-sidegenerateSlugPreview. For custom slugifiers or other cases:
Custom Spatie action class
If you overrideconfig/sluggable.php → actions.generate_slug, the field uses your bound GenerateSlugAction implementation automatically.
skipGenerateWhen — preview without overwriting
skipGenerateWhen returns true, the field will keep the existing slug instead of generating a new one.
startSlugSuffixFrom and collisions in preview
my-post already exists in the database, the preview may show my-post-5 (according to Spatie rules).
Closure as slug source
title and edition fields filled out — SlugField reads them from the live state of siblings in the schema.
usingSuffixGenerator — custom suffix
Permalink preview & URL actions
The permalink bar shows host (withouthttps://), optional path prefix, slug segment, and optional postfix. HTTPS hosts display a green lock icon.
Basic blog permalink
Subdomain style (host only)
Sandwich URL (prefix + slug + postfix)
Custom visit link (route or absolute URL)
visitUrl / visitRoute receives injected: slug (string), routeKey (string — for self-healing models: hello-world-5, otherwise same as slug) and record (?Model).
Hide permalink or actions
Action button layout
Buttons sit below the input: Edit / OK / Cancel on the left; Regenerate / Copy / Visit on the right.config('filament-flex-fields.slug.action_button_labels').
Uniqueness validation
Separate from Spatie’s DB suffix generation — this is form validation before save.Default (unique in table)
Disable uniqueness check
Scoped uniqueness (tenant, locale, type, …)
Filament-style unique parameters
Homepage slug (/)
For CMS pages that should live at the site root:
/ is allowed as a special case.
TitleSlugField factory parameters
TitleSlugField::make() is a static factory returning a FusedGroup (title + hidden auto-sync flag + slug).
Example of
titleUniqueParameters — unique title within tenant:
$urlHost | ?string | config('filament-flex-fields.slug.url_host') | Permalink host |
| $urlPath | ?string | null | Permalink path prefix |
| $urlHostVisible | bool | true | Show host segment |
| $visitLinkLabel | ?string | translated default | Visit button label |
| $visitUrl | string\|Closure\|null | null | Custom visit URL; closure: slug, routeKey, record |
| $showVisitLink | bool | true | Show visit action |
| $slugLabelPostfix | ?string | null | Trailing URL segment after slug |
| $preserveSlugOnEdit | bool\|Closure | true | Don’t auto-update slug on edit |
| $translatableLocales | array\|Closure\|null | config('…slug.translatable_locales') | Enables TranslatableFields title UI; null = single-language title |
| $slugSourceLocale | string\|Closure\|null | config('…slug.slug_source_locale') or app.locale | Locale whose title drives slug generation |
| $requiredTitleLocales | 'all'\|list<string>\|Closure\|null | config('…slug.required_title_locales') or slug source locale only | Which title tabs are required (null = source locale only) |
| $spatieTranslatable | bool\|Closure | false | Marks Spatie Translatable intent; hydrate auto-detects HasTranslations when package is present |
| $titleLocaleConfigurator | ?Closure | null | fn (FlexTextInput $field, string $locale) => $field |
| $translatableFieldsConfigurator | ?Closure | null | fn (TranslatableFields $fields) => $fields->… — full title tabs config |
| $spatieModel | string\|Closure\|null | null | Spatie Sluggable only (HasSlug) — not Translatable |
| $slugConfigurator | ?Closure | null | fn (SlugField $field) => $field->... |
Example — custom title field + slug configurator:
Example for each parameter of TitleSlugField::make()
SlugField — configuration API
Each method below is chainable onSlugField::make('slug').
source(string|Closure|null $statePath)
State path of the field that drives auto-generation (usually title).
sourceLive(bool|Closure $condition = true)
When false, slug does not react to source changes (manual slug only).
translatableTitle(bool|Closure $condition = true)
Enables translatable title source paths (title.pl, …). Usually set via titleLocales().
titleLocales(array|Closure $locales)
Locale map (['pl' => 'PL', 'en' => 'EN']) or list (['pl', 'en']). Implies translatableTitle(true).
slugSourceLocale(string|Closure $locale)
Which locale title drives slug auto-generation. Default: config slug_source_locale, then app.locale, then first locale.
translatableTitleField(string|Closure $fieldName)
Base title attribute when resolving source path (default: title).
spatieTranslatable(bool|Closure $condition = true)
Configuration flag for Spatie Translatable models. Hydration auto-detects HasTranslations on the record when spatie/laravel-translatable is installed — the flag does not need to be true for detection to work.
titleField(Field $field)
Attach a title field for SlugField::withTitle() / manual fused layouts.
titleFieldWrapper(?Closure $wrapper)
titleAfterStateUpdated(?Closure $callback)
slugAfterStateUpdated(?Closure $callback)
titleReadOnly(bool|Closure $condition = true) / slugReadOnly(bool|Closure $condition = true)
Blocks editing of the respective field. Works with TitleSlugField and manual SlugField::withTitle().
slugifyUsing(?Closure $callback)
Custom slugifier; receives ['source' => string].
spatieModel(string|Closure|null $modelClass)
Enable Spatie integration for preview generation.
spatieSlugField(string|Closure $attribute = 'slug')
Model attribute Spatie writes to / reads from.
spatieSourceField(string|Closure|null $field)
Primary model attribute for the live source string.
serverSideGeneration(bool|Closure $condition = true)
Use Livewire generateSlugPreview instead of client Str.slug. Automatically enabled when Spatie integration is active or when translatable titles are used.
slugSeparator(string|Closure $separator = '-')
Normalization separator (also used by fallback SlugGenerator). Default validation slugPattern is derived from this separator automatically.
maxSlugLength(int|Closure|null $length)
Max length for fallback generator; Spatie uses slugsShouldBeNoLongerThan from model.
urlHost(string|Closure|null $host) / urlPath(string|Closure|null $path)
Permalink segments. Host may include https://; display strips the scheme.
urlHostVisible(bool|Closure) / urlPathVisible(bool|Closure)
Controls which URL segments are visible in the permalink preview.
permalinkPreview(bool|Closure $condition = true)
Show or hide the entire permalink chrome.
permalinkLabel(string|Closure|null $label)
visitUrl(string|Closure|null $url) / visitRoute(string|Closure|null $route)
Target of the Visit button. The Closure receives injected parameters: slug, routeKey (self-healing: {slug}-{id}), and optionally the record.
visitLinkLabel(string|Closure|null $label)
showVisitLink(bool|Closure) / showCopyButton(bool|Closure) / showRegenerateButton(bool|Closure)
Action toggles below the slug. By default all are true (except Regenerate — visible only after manual slug edit).
actionButtonLabels(bool|Closure) / actionButtonsIconOnly(bool|Closure)
Control text on action buttons (Hero UI button-group + Gravity icons).
autoUpdateDisabledField(string|Closure|null $field)
Hidden boolean field path tracking manual slug edits. TitleSlugField sets {slug}_auto_update_disabled automatically.
autoGenerate(bool|Closure $condition = true)
Main toggle for auto-generating slug from the source field.
preserveSlugOnEdit(bool|Closure $condition = true)
On the edit operation, it stops auto-sync from the title (protects published URL). On create, it always syncs.
inlineEditing(bool|Closure $condition = true)
When true (default): permalink preview + Edit/OK/Cancel/Reset buttons. When false: standard TextInput.
allowHomepageSlug(bool|Closure $condition = true)
Allows homepage slug / (CMS homepage). Requires custom validation pattern.
generationDebounce(int|Closure $milliseconds = 400)
Debounce before regenerating slug from title.
slugPattern(string|Closure $pattern) / regex(string|Closure|null $pattern)
Optional override. When omitted, pattern is auto-derived from slugSeparator() (and allowHomepageSlug() when enabled). regex() is an alias.
slugLabelPostfix(string|Closure|null $postfix)
Trailing path after slug in permalink preview.
recordSlug(string|Closure|null $slug)
Initial/stored slug for edit preview and visit link before state hydrates.
slugRules(array|Closure $rules)
Additional validation rules (besides built-in pattern and slugUnique).
slugUnique(bool|Closure $condition = true) / slugUniqueParameters(array $parameters) / slugUniqueScope(?Closure $scope) / slugUniqueModel(string|Closure|null $model)
Uniqueness validation in the form (independent of Spatie suffixes on save).
size(string|Closure $size) / variant(string|Closure $variant)
Size and visual variant of the field wrapper (Hero UI). Defaults from config('filament-flex-fields.ui').
config('filament-flex-fields.ui.slug_size'), slug_variant.
Inherited Filament Field API
SlugField inherits standard Filament methods — they work exactly the same as in other fields:
TitleSlugField configures title and slug separately — supply slug helper text via the slugConfigurator:
Public helper methods (views, tests, extensions)
Public methods used in Blade templates, tests, and custom field extensions:
Example in test:
FlexField schema keys (FieldType::Slug)
When using FlexFieldFormBuilder:
Advanced recipes
Repeater with per-row title + slug
sections.0.title → sections.0.slug).
Standalone slug (no title field)
Regenerate button behaviour
Regenerate only appears when auto-sync has been disabled by a manual slug edit (setting the hidden field{slug}_auto_update_disabled = true). During normal syncing, the button is hidden.
Note:SlugFielduseswire:ignoreon the Alpine fragment — changingdata.slugdirectly in tests might not reflect UI behavior. Test by updating the title or modifyingslug_auto_update_disabled.
Playground (Live Preview)
Enable the playground in.env:
SubNavigationPosition::Start).
- Root URL:
/admin/flex-fields-playground(redirects to first component) - Slug field page:
/admin/flex-fields-playground/slug-field - Routes are registered only when
FLEX_FIELDS_PLAYGROUND=true(orfilament-flex-fields.playground.enabledistrue).
Spatie is optional for every recipe below. All playground demos work with browserSource of truth:Str::slug()only. To align preview and save withlaravel-sluggable, see Optional Spatie upgrade (playground recipes) at the end of this section.
SlugFieldPlayground in the package (src/Support/Playground/SlugFieldPlayground.php). Default form state keys (slug__title, slug__standalone, …) live in SlugFieldPlayground::defaultState().
Playground recipes — 1:1 with SlugFieldPlayground
Full section schema (copy-paste ready):
Recipe 1 — Shared title source + slug field (slug__standalone)
A separate title field drives one or more slug fields on the same form. Playground reuses slug__title for recipes 1, 5, and 6.
Recipe 2 — Title + slug one-liner (TitleSlugField)
Recipe 3 — Translatable title + single slug (slug__i18n_*)
'slug__i18n_title' => ['pl' => '…', 'en' => '…'], 'slug__i18n_slug' => 'przewodnik-po-morzu-srodziemnym'.
Recipe 4 — Title + slug pair (titleField() + recordSlug())
Recipe 5 — Permalink preview + Visit link (slug__permalink)
Uses the shared title field from recipe 1 (slug__title).
Recipe 6 — URL slug sandwich (slug__sandwich)
Uses the shared title field from recipe 1 (slug__title).
wyachts.test/books/my-slug/detail/
Recipe 7 — Read-only variants (grid)
Spatie: optional for all three.
Optional Spatie upgrade (playground recipes)
None of the playground recipes requirecomposer require spatie/laravel-sluggable. Add it only when you need model-level suffixes (-2), preventOverwrite, extraScope, or identical rules on save and in the form preview.
TitleSlugField recipes (2, 3):
SlugField recipes (1, 4–7):
laravel-sluggable integration (install, HasSlug, getSlugOptions(), #[Sluggable], translatable models).
Playground quick reference
Translations
Publish translation files:lang/vendor/filament-flex-fields/{locale}/default.php.
UI Keys (slug.*):
Validation (
validation.slug.*):
Custom labels without publishing — override directly on the field:
app.locale = pl) to load built-in translations if desired.
Troubleshooting
Diagnostics in tinker / test:
Comparison with blendbyte/filament-title-with-slug
Playground
Slug:slug-field
/admin/flex-fields-playground/slug-field — see Playground.