components([ Section::make(__('panel.pages.sections.content')) ->description(__('panel.pages.sections.content_hint')) ->schema([ TranslatableTabs::make(fn (string $locale, bool $isDefault) => [ TextInput::make("title.{$locale}") ->label(__('panel.pages.fields.title')) ->required($isDefault) ->maxLength(255) ->columnSpanFull() ->live(onBlur: true) ->afterStateUpdated(function (?string $state, ?Model $record, callable $get, callable $set) use ($locale, $isDefault) { if (blank($get("slug.{$locale}"))) { $set("slug.{$locale}", Str::slug((string) $state)); } // Only the language content is actually // written in matters here: a near-duplicate // in a language nobody is looking at right // now would be a warning about the wrong // thing. if ($isDefault) { DuplicateTitles::warnIfSimilar( Page::class, PageResource::class, $state, $locale, $record?->getKey(), ); } }), TextInput::make("slug.{$locale}") ->label(__('panel.pages.fields.slug')) ->required($isDefault) ->maxLength(255) ->columnSpanFull() ->rules(fn ($record) => [new UniqueTranslatedSlug(Page::class, $record?->id)]), Textarea::make("excerpt.{$locale}") ->label(__('panel.pages.fields.excerpt')) ->rows(2) ->maxLength(500) ->columnSpanFull(), RichEditor::make("body.{$locale}") ->label(__('panel.pages.fields.body')) // Images in the text go where every other upload // does, public: the HTML travels to the frontend // as it is, with each image's URL written in, and // a private file would be a broken image there. // Media::referencedPaths() finds them in the body, // so pruning never takes one a page still shows. ->fileAttachmentsDisk(config('media.disk')) ->fileAttachmentsDirectory('pages/content') ->fileAttachmentsVisibility('public') ->columnSpanFull(), ]), ]), Section::make(__('panel.pages.sections.publishing')) ->columns(2) ->schema([ Select::make('status') ->label(__('panel.pages.fields.status')) ->options(PageStatus::class) ->default(PageStatus::Draft) ->required() ->live() // Editing content and pushing it live are separate // permissions, so the status field itself is locked // for users who may only write. ->disabled(fn () => Auth::user()?->cannot('pages.publish')) ->dehydrated(), DateTimePicker::make('published_at') ->label(__('panel.pages.fields.published_at')) ->seconds(false) ->helperText(__('panel.pages.fields.published_at_hint')), // The date nobody remembers on their own. A notice about // something that already happened stays up until somebody // trips over it. DateTimePicker::make('unpublish_at') ->label(__('panel.pages.fields.unpublish_at')) ->helperText(__('panel.pages.fields.unpublish_at_hint')) ->seconds(false) ->after('published_at') ->disabled(fn () => Auth::user()?->cannot('pages.publish')) ->dehydrated(), Select::make('category_id') ->label(__('panel.pages.fields.category')) ->relationship('category', 'name->'.config('locales.default')) ->getOptionLabelFromRecordUsing(TranslatedOptionLabel::using('name')) ->searchable() ->preload(), Select::make('author_id') ->label(__('panel.pages.fields.author')) ->relationship('author', 'name') ->searchable() ->preload() ->default(fn () => Auth::id()) ->visible(fn () => Auth::user()?->can('users.view_any')), FileUpload::make('featured_image') ->label(__('panel.pages.fields.featured_image')) ->image() // Explicit: the default disk is private, and the // frontend runs on another host and has to be able to // fetch what you upload. ->disk(config('media.disk')) ->directory('pages') ->imageEditor() // So the description field appears as soon as there is // something to describe. ->live() ->columnSpanFull(), // Alt text lives with the record rather than with the // file, because the same image can need a different // description depending on where it is used. TranslatableTabs::make(fn (string $locale) => [ TextInput::make("featured_image_alt.{$locale}") ->label(__('panel.pages.fields.featured_image_alt')) ->helperText(__('panel.pages.fields.featured_image_alt_hint')) ->maxLength(255) ->columnSpanFull(), ]) ->visible(fn (callable $get) => filled($get('featured_image'))) ->columnSpanFull(), Toggle::make('is_featured') ->label(__('panel.pages.fields.is_featured')), ]), Section::make(__('panel.pages.sections.seo')) ->description(__('panel.pages.sections.seo_hint')) ->collapsed() ->schema([ TranslatableTabs::make(fn (string $locale) => [ TextInput::make("meta_title.{$locale}") ->label(__('panel.pages.fields.meta_title')) ->maxLength(60) ->columnSpanFull(), Textarea::make("meta_description.{$locale}") ->label(__('panel.pages.fields.meta_description')) ->rows(2) ->maxLength(160) ->columnSpanFull(), ], label: __('panel.pages.sections.seo')), ]), ]); } }