
Summary
URL input with a live Open Graph / HTML meta preview card. The field uses the FlexTextInput pill shell for the input track and fetches page metadata through a server-side scrape endpoint (cached, rate-limitable). Preview cards support three layouts, optional URL prefix/suffix affixes, and configurable debounce / skeleton timing.| Class | Bjanczak\FilamentFlexFields\Filament\Forms\Components\LinkPreviewField |
| State type | string|null — full URL (including configured prefix) |
| Model cast | 'article_url' => 'string' or leave uncast |
| FieldType | (no dedicated FieldType mapping yet — use the class directly) |
| Playground | link-preview-field slug in Flex Fields playground |
Basic usage
State format
| Value | Description | Example |
|---|---|---|
| Valid URL | Trimmed absolute URL stored on save | https://laravel.com/docs |
| Empty | null after dehydrate | null |
prefix('https://') is set, the stored state is always the full URL (https://example.com/path). The visible input shows only the suffix (example.com/path) for readability. On blur, pasted full URLs are normalized back to suffix-only display.
null.
Preview layouts
Three card layouts viapreviewLayout():
| Layout | Modifier class | Best for |
|---|---|---|
horizontal (default) | fff-link-preview__card--horizontal | Compact rows — square thumb left, title + description + domain right (Twitter / X style) |
vertical | fff-link-preview__card--vertical | Narrow columns — wide thumb on top, title + domain below |
card | fff-link-preview__card--card | Full-width social cards — 16:9 thumb, title + domain |
role="alert" region below the card.
Configuration API
Each fluent method accepts aClosure for dynamic values (e.g. based on $get, $record, or $livewire).
variant(string|Closure $variant)
Visual style shared with FlexTextInput.
| Value | Description |
|---|---|
primary | Default filled pill shell |
secondary | Secondary surface tokens |
soft | Softer background / border |
flat | Minimal chrome |
ghost | Transparent shell |
size(string|ControlSize|Closure $size)
Control height. See Control size. Default: md.
preview(bool|Closure $condition = true)
Enable or disable the preview card entirely. When false, the field behaves as a styled URL input only.
previewDebounce(int|Closure $milliseconds)
Delay after typing before the client calls the scrape endpoint. Default: 500. Pass 0 for immediate fetch (use sparingly).
previewMinUrlLength(int|Closure $length)
Minimum resolved URL character length before scraping starts. Default: 10. Enforced minimum: 4.
previewMinSkeletonMs(int|Closure $milliseconds)
Minimum skeleton display time on initial card reveal (SSR-prefilled URLs or first client fetch). Default: 500. Prevents flicker when metadata resolves instantly from cache.
previewLayout('horizontal'|'vertical'|'card'|Closure $layout)
Card layout. Default: horizontal. Invalid values throw InvalidArgumentException.
resolveInitialPreviewOnServer(bool|Closure $condition = true)
When true (default), the Blade view calls resolveInitialPreview() during SSR for prefilled URLs so the card can render immediately without waiting for Alpine.
When false, initial preview is deferred to the client — useful on heavy forms to avoid blocking page render or duplicate scrapes.
showVisitLink(bool|Closure $condition = true)
When true (default), the domain row is an <a> opening the URL in a new tab (rel="noopener noreferrer").
When false, the domain is plain text with the same icon styling (fff-link-preview__domain--text).
visitLabel(string|Closure $label)
Accessible label for the visit link (aria-label). Default: translated filament-flex-fields::default.link_preview.visit.
visitIcon(string|BackedEnum|Htmlable|Closure|null $icon)
Icon beside the domain row. Default: GravityIcon::Paperclip.
prefix(string|Closure|null $label) / suffix(string|Closure|null $label)
Inline affix labels on the FlexTextInput track. Empty strings are treated as no affix.
placeholder(string|Closure|null $placeholder)
Inherited from Filament HasPlaceholder. Default translation: filament-flex-fields::default.link_preview.placeholder.
readOnly(bool|Closure $condition = true) / disabled(bool|Closure $condition = true)
Inherited from Filament. Read-only still shows preview for the current URL; disabled blocks interaction and scraping triggers.
focusOutline(bool|Closure $condition = true)
Inherited from HasFieldFocusOutline. Default: false. When true, shows the shared --fff-field-focus-* ring on the input shell.
Public helper methods
| Method | Returns | Description |
|---|---|---|
getVariant() | string | Resolved variant |
getSize() | string | Resolved size (sm, md, lg) |
isPreviewEnabled() | bool | Preview card enabled |
getPreviewDebounce() | int | Debounce ms (≥ 0) |
getPreviewMinUrlLength() | int | Min URL length (≥ 4) |
getPreviewMinSkeletonMs() | int | Min skeleton ms (≥ 0) |
getPreviewLayout() | string | horizontal, vertical, or card |
shouldResolveInitialPreviewOnServer() | bool | SSR preview resolution |
shouldShowVisitLink() | bool | Domain row is a link |
getVisitLabel() | string | Visit link aria-label |
getVisitIcon() | string|BackedEnum|Htmlable|null | Resolved visit icon |
getPrefix() / getSuffix() | string|null | Resolved affixes |
getScrapeUrl() | string | Relative scrape route URL for Alpine |
resolveInitialPreview(?string $url) | array|null | Server-side metadata (title, description, image) |
getAlpineConfiguration() | array | Config passed to linkPreviewFieldFormComponent |
getWrapperClasses() | array<string, bool> | Root CSS class map |
resolveInitialPreview() returns null when preview is disabled, URL is empty, URL is not scrapable, or scrape returns no metadata.
Package configuration
Global scrape behaviour inconfig/filament-flex-fields.php:
| Key | Env variable | Default | Description |
|---|---|---|---|
link_preview.cache_ttl_seconds | FLEX_FIELDS_LINK_PREVIEW_CACHE_TTL | 86400 | Server-side scrape cache TTL |
link_preview.rate_limit_per_minute | FLEX_FIELDS_LINK_PREVIEW_RATE_LIMIT | 30 | Per-user scrape rate limit |
link_preview.timeout_seconds | FLEX_FIELDS_LINK_PREVIEW_TIMEOUT | 8 | HTTP timeout for remote pages |
.env:
url-meta-scrape.js) so repeated keystrokes do not spam the server.
Scrape endpoint (named route): filament-flex-fields.url-meta.scrape.
Validation
| Rule | Detail |
|---|---|
| Built-in | nullable, url |
required() | Standard Filament required validation |
| Hydrate / dehydrate | Trims whitespace; empty → null |
Model & database examples
Recipe: CMS external link block
Recipe: prefixed marketing domain
Recipe: read-only audit display
Recipe: heavy admin form — defer SSR scrape
Recipe: reactive live() — drive sibling fields from preview URL
live() when other form fields should react to URL changes. Preview fetching still debounces independently via previewDebounce().
Recipe: affix-only internal paths (preview disabled)
For intranet or relative paths where server scraping is not useful — URL input only, no preview card:Recipe: scrape errors — longer debounce + no visit link
When users paste slow or rate-limited URLs, reduce churn and keep the domain as plain text:fff-link-preview__error with role="alert"; they do not block form submission when nullable / url rules pass.
Accessibility
- Preview card container uses
aria-live="polite"for loading and reveal updates - When revealed,
aria-labelon the card reflects the scraped page title - Visit link uses
visitLabel()asaria-label - Scrape errors use
role="alert" - Input remains a standard Filament field with label / hint / error association
CSS classes
| Class | Role |
|---|---|
fff-link-preview-field | Filament wrapper modifier |
fff-link-preview-field--{sm|md|lg} | Size modifier |
fff-link-preview-field--{variant} | Variant modifier |
fff-link-preview-field--layout-{horizontal|vertical|card} | Layout modifier on wrapper |
fff-link-preview | Alpine root (FlexTextInput shell) |
fff-link-preview__card | Preview card container |
fff-link-preview__card--{horizontal|vertical|card} | Layout modifier |
fff-link-preview__domain--text | Non-link domain row when showVisitLink(false) |
fff-link-preview__error | Scrape error message |
fff-flex-text-input, fff-flex-text-input__shell, variant modifiers).
Assets
Lazy-loaded stylesheets (viaFlexFieldAssets::stylesheetsFor('link-preview-field')):
flex-text-inputlink-preview-field
link-preview-field (loaded with x-load).
Uses wire:ignore on the Alpine root — prefer changing Livewire state or sibling fields over direct DOM manipulation in tests.
Implementation notes
- Metadata is scraped server-side via
UrlMetaScraper(Open Graph + fallback<title>/<meta name="description">/<meta property="og:image">). - Non-scrapable URLs (invalid scheme, localhost, etc.) skip preview quietly.
- Image preload runs client-side before revealing the card to avoid layout pop-in.
- Invalid
variant()orpreviewLayout()values throwInvalidArgumentExceptionat render time.