Skip to content

Frontend Widgets

Frontend widgets are registered public components that Capell can render in normal content, lazy interaction targets, and package-owned experiences. A widget definition answers four questions:

QuestionDefined by
Which widget key can editors choose?WidgetDefinitionData::$key
Which Blade/Livewire component renders it?WidgetDefinitionData::$component and target
Which frontend assets does it need?resourceGroups
How should it present, load, and expose interactions by default?defaultPresentationSettings and defaultInteractionTriggers

Content Sections and Layout Builder use this same surface for editor-managed content. The registry lives in the core/frontend boundary so packages can ship reusable widget targets without inventing their own modal systems, asset loaders, or public routes.

Use WidgetRegistry::registerDefinition() when the widget needs presentation defaults, runtime resource groups, or interaction defaults.

use Capell\Core\Data\Widgets\WidgetDefinitionData;
use Capell\Core\Support\Widgets\WidgetRegistry;
public function boot(WidgetRegistry $widgets): void
{
$widgets->registerDefinition(WidgetDefinitionData::frontendBlade(
key: 'video-player',
component: 'vendor-video::widgets.player',
resourceGroups: ['vendor-video.player'],
defaultPresentationSettings: [
'width_mode' => 'container',
'loading_strategy' => 'interaction',
],
));
}

WidgetRegistry::register($name, WidgetTarget::FrontendBlade, $component) still works for simple widgets. New package code should prefer definitions because the rendering component, resource groups, presentation defaults, and interaction defaults stay in one place.

Widget instance data has two layers:

LayerPurposePublic component receives it?
data.*The widget’s own content propsYes
data.__capell.*Capell runtime, presentation, and interaction metadataNo

For example:

[
'type' => 'video-player',
'data' => [
'title' => 'Product walkthrough',
'video_url' => 'https://example.com/video.mp4',
'__capell' => [
'presentation' => [
'loading_strategy' => 'visible',
],
'interactions' => [
[
'label' => 'Open transcript',
'target_type' => 'widget',
'behavior' => 'modal',
'target_widget' => [
['type' => 'content', 'data' => ['content' => '<p>Transcript...</p>']],
],
],
],
],
],
]

The public renderer strips data.__capell before passing props to the widget component. A video widget can receive title and video_url; it must not receive its presentation settings, nested target widgets, editor metadata, or interaction internals.

Widget definitions can provide type defaults through defaultPresentationSettings. Editors can override those settings on one widget instance under data.__capell.presentation.

Resolution order is:

  1. instance override in data.__capell.presentation;
  2. widget definition default;
  3. presentation preset default;
  4. system default.

The system default keeps existing content server-rendered. Only opt a widget or block into lazy behaviour when it should genuinely wait for visibility, idle time, or visitor interaction.

Widgets can expose interaction triggers through data.__capell.interactions or type defaults in WidgetDefinitionData::$defaultInteractionTriggers.

Supported target types:

TargetUse
widgetRender a registered widget through the lazy widget endpoint.
fragmentRender an encrypted Layout Builder block fragment.
urlLink to a safe URL.
public_actionUse a safe fallback URL unless a package renders the action elsewhere.

Supported behaviours for lazy targets are modal, slide_over, inline_reveal, and replace_region.

Widget targets render through /_capell/widgets/{reference}. The reference is encrypted JSON containing the widget type and data. The public trigger does not expose the widget type, component name, package name, target content, model IDs, field paths, or editor metadata.

Use a widget target when the visitor is opening a separate experience, such as a video player, form, gallery, quote calculator, or comparison panel. Use a Layout Builder fragment target when the visitor is loading a public block fragment from the current layout.

The editor-facing shape for a trigger that opens a video in a modal looks like this:

[
'label' => 'Watch tour',
'icon' => 'heroicon-o-play-circle',
'style' => 'primary',
'target_type' => 'widget',
'behavior' => 'modal',
'modal_size' => 'lg',
'target_widget' => [
[
'type' => 'video-player',
'data' => [
'title' => 'Product tour',
'video_url' => 'https://example.com/product-tour.mp4',
],
],
],
]

The public page renders a safe trigger and an encrypted lazy widget URL. It does not render the target widget content until the visitor clicks.

Use FrontendResourceRegistry when a widget needs CSS or JavaScript that should only load when the widget appears or when an interaction target opens.

use Capell\Core\Enums\PresentationLoadingStrategy;
use Capell\Frontend\Support\Assets\FrontendResourceRegistry;
public function boot(FrontendResourceRegistry $resources): void
{
$resources
->group('vendor-video.player')
->css('resources/css/player.css', buildPath: 'vendor/vendor-video')
->js('resources/js/player.js', buildPath: 'vendor/vendor-video', loading: PresentationLoadingStrategy::Interaction);
}

Public HTML receives generated resource IDs. Resource group keys and package names stay out of the rendered page.

Loading strategyUse when
eagerThe widget is visible and needed for the first render.
visibleThe widget can wait until it enters the viewport.
interactionThe widget is only needed after a click, focus, or similar action.
idleThe widget is useful soon, but not critical to initial rendering.

For interaction targets, prefer interaction resources unless the target also appears server-rendered elsewhere on the page.

Widget HTML and interaction placeholders must not expose:

  • admin/editor controls;
  • model IDs;
  • field paths;
  • block keys;
  • component names;
  • package namespaces;
  • signed URLs;
  • raw target widget data.

Use Public HTML safety, Presentation delivery, and Frontend extensions when changing widget rendering.