Skip to main content
SlugField ← Back to Table of Contents

Summary

Permalink field for Filament: title + slug in one block, live URL preview, inline editing, Copy/Visit/Regenerate buttons, and uniqueness validation.
Spatie laravel-sluggable is optional. By default, the slug is generated from the title using Str::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, -3 suffixes, preventOverwrite, etc.).

Start here — integration without Spatie (default)

No extra packages required besides filament-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)

The model does not need:
  • use HasSlug ani getSlugOptions()
  • an observer generating the slug
  • mutatora setSlugAttribute
  • composer require spatie/laravel-sluggable
The slug is saved to the record just like title—from the form data.

Step 3 — Filament Resource (minimum)

This creates: a title field + a hidden auto-sync flag + a slug field in one FusedGroup. If you want to see https://your-domain.com/blog/my-post under the slug:
Or only in the Resource, without changing the config:

Parameters of TitleSlugField::make() — what is available?

What happens automatically (without Spatie)

Slug generation without Spatie (technical)

No server requests. No model configuration required.

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, -3 suffixes on database collisions
  • preventOverwrite — never overwrite the slug after publish
  • skipGenerateWhen, extraScope, multiple source fields
  • using the same SlugOptions in form preview and on save()
Do tego: Spatie 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: spatieModel changes 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)

When config('filament-flex-fields.slug.url_host') is set (e.g. APP_URL):
When url_host is null (no URL preview in config):

Table of default visual values

The same layout — three ways to call

Livewire test helper — hidden auto-sync field name:

Installation and assets

The package ships inside janczakb/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 uses HasSlug / getSlugOptions() and you want matching form preview:
Without that package TitleSlugField::make() still works fully — generation uses browser Str::slug() / client slugify.

Package configuration (config/filament-flex-fields.php)

Publish the config:
Slug-related keys:
Example — blog with custom Polish field names:

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)

Do not add 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 Spatie HasSlug may add a suffix (-2) or apply preventOverwrite — 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 7: CMS Homepage (/)

Standalone SlugField (matches playground Homepage slug):
Inside TitleSlugField:

Scenario 8: Repeater — row with title and slug

Nested paths (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 playground slug__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:
Application-side requirements: slug column in migration + slug in model’s $fillable. Nothing more. What happens automatically:

How slug generation works

Form preview vs model save: 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)

On edit, changing the title does not change the slug. Good for published URLs.

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. spatieModel on TitleSlugField is for Spatie Sluggable (HasSlug). For translations use translatableLocales (+ optional spatieTranslatable).

Basic usage

Changing EN/FR titles does not change the slug. Only the slugSourceLocale tab drives permalink generation.
On save, Filament merges title.pl / title.en into a title array. No extra glue code required.

Storage with Spatie laravel-translatable (optional)

On edit: if the record uses 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)

When 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.
FlexField config key: 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 (via config/sluggable.php → actions.generate_slug)
  • Spatie\Sluggable\Support\SluggableAttributeResolver dla modeli z #[Sluggable] bez getSlugOptions()
  • Spatie\Sluggable\Support\Config::getAction() — ten sam resolver akcji co trait HasSlug
Spatie adds a second layer: saving the slug with model options (suffixes, scope, 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())

Multi-field attribute (v4):

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.
Simply having Post as the Resource model does not enable Spatie automatically — the slug configuration must exist on the model.
What changes after adding 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

The form must have the fields used in 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+)

No 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-side generateSlugPreview. For custom slugifiers or other cases:

Custom Spatie action class

If you override config/sluggable.php → actions.generate_slug, the field uses your bound GenerateSlugAction implementation automatically.

skipGenerateWhen — preview without overwriting

In the form preview, when skipGenerateWhen returns true, the field will keep the existing slug instead of generating a new one.

startSlugSuffixFrom and collisions in preview

If my-post already exists in the database, the preview may show my-post-5 (according to Spatie rules).

Closure as slug source

The form must have the title and edition fields filled out — SlugField reads them from the live state of siblings in the schema.

usingSuffixGenerator — custom suffix


The permalink bar shows host (without https://), optional path prefix, slug segment, and optional postfix. HTTPS hosts display a green lock icon.

Subdomain style (host only)

Sandwich URL (prefix + slug + postfix)

Closure visitUrl / visitRoute receives injected: slug (string), routeKey (string — for self-healing models: hello-world-5, otherwise same as slug) and record (?Model).

Action button layout

Buttons sit below the input: Edit / OK / Cancel on the left; Regenerate / Copy / Visit on the right.
Global default: 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:
Only the exact value / 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 — custom field names via config:

Example for each parameter of TitleSlugField::make()


SlugField — configuration API

Each method below is chainable on SlugField::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)

Action toggles below the slug. By default all are true (except Regenerate — visible only after manual slug edit).
Example — Copy only, no Visit:

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').
Defaults: 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

Nested paths are resolved automatically (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: SlugField uses wire:ignore on the Alpine fragment — changing data.slug directly in tests might not reflect UI behavior. Test by updating the title or modifying slug_auto_update_disabled.

Playground (Live Preview)

Enable the playground in .env:
Panel navigation: Settings & Tools → Flex Fields Playground — cluster with left sub-navigation (Filament 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 (or filament-flex-fields.playground.enabled is true).
Spatie is optional for every recipe below. All playground demos work with browser Str::slug() only. To align preview and save with laravel-sluggable, see Optional Spatie upgrade (playground recipes) at the end of this section.
Source of truth: 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_*)
Default state shape: '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).
Preview: 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 require composer 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):
Full integration: Spatie laravel-sluggable integration (install, HasSlug, getSlugOptions(), #[Sluggable], translatable models).

Playground quick reference

Translations

Publish translation files:
Override in lang/vendor/filament-flex-fields/{locale}/default.php. UI Keys (slug.*): Validation (validation.slug.*): Custom labels without publishing — override directly on the field:
Set your application locale (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.