Widgets are small, configurable blocks of output that administrators drop into areas (blog sidebar, footer columns…) without touching code. They are built on arrilot/laravel-widgets. The same widget class technology also powers dashboard cards.
In the user guide: Widgets.
| Term | What it is | Stored in |
|---|---|---|
| Widget class | PHP class extending App\Widgets\AbstractWidget | Code; registered with Widgets::register($key, $class) |
| Widget area | A named place in a theme (post-sidebar, footer-1…) | widget_areas table (name, title, description, order, soft-deletable) |
| Widget instance | One placement of a class in an area, with its own title and settings | widgets table (name = class, title, settings, order, web, mobile, ajax, status) |
Built-in classes (WidgetsServiceProvider): tru-menu, tru-text, tru-html, tru-advertisement, tru-posts, tru-posts-categories, tru-posts-tags. Seeded areas: sidebar (homepage), categories-sidebar, tags-sidebar, pages-sidebar, post-sidebar, and footer-1…footer-6 (extra footer columns are soft-deleted and restored by the theme option footer_widget_columns).
Modules/Playground/app/Widgets/NoteWidget.php
use App\Widgets\AbstractWidget;
class NoteWidget extends AbstractWidget
{
protected array $config = ['title' => false, 'limit' => 2]; // defaults; must stay typed as array
public const TITLE = 'playground::p.widget_title'; // shown in the admin widget list
public const DESCRIPTION = 'playground::p.widget_description';
public const VIEW = 'playground::widgets.notes'; // public markup
public const ADMIN_VIEW = 'playground::widgets.notes-admin'; // settings form shown in Appearance -> Widgets
// The widget manager passes ['title' => ..., 'settings' => object|array]; flatten it into $config.
public function __construct(array $config = [])
{
$settings = isset($config['settings']) ? (array) $config['settings'] : [];
$settings['title'] = $config['title'] ?? '';
parent::__construct($settings);
}
public function run(): View
{
return view(static::VIEW, [
'title' => $this->config['title'] ?: false,
'notes' => Note::query()->latest('id')->limit(max(1, (int) ($this->config['limit'] ?? 2)))->get(),
]);
}
}AbstractWidget adds to Arrilot's base: translatable get_title()/get_description() from the constants, a build($sidebar, $widget) method that renders ADMIN_VIEW for the admin screen, viewable()/admin_view() existence checks, and a $fields map that decides which standard controls the editor shows: title, web, mobile, ajax, status (all true; turn one off with $this->set_fields('ajax', false) in the constructor, as the HTML widget does).
resources/views/widgets/notes-admin.blade.php
@extends('widgets.template')
@section('widget-form-' . $widget->id)
<div class="mb-3">
<label class="form-label" for="settings-limit-{{ $widget->id }}">{{ __('playground::p.limit') }}</label>
<input type="number" min="1" max="12" class="form-control" id="settings-limit-{{ $widget->id }}"
name="settings[limit]" value="{{ $widget->settings->limit ?? 2 }}">
</div>
@endsectionThe template (resources/themes/admin/widgets/template.blade.php) supplies the card, the title input and the web/mobile/async/status switches, and posts the form to admin.widgets.update. Everything you name settings[...] is stored as the instance's settings and returned to your constructor as $config['settings'].
resources/views/widgets/notes.blade.php
<section class="widget mb-4">
@if ($title)<h3 class="h5">{{ $title }}</h3>@endif
@foreach ($notes as $note)<p>{{ $note->title }}</p>@endforeach
</section>Use Bootstrap utility classes, escape everything, and return nothing (an empty view) when there is nothing to show so empty areas collapse (the Echo wrappers check Widget::group('name')->isEmpty()).
Provider boot()
use App\Facades\Widgets;
Widgets::register('playground-notes', NoteWidget::class); // key must be unique; prefix it with your aliasThat is all: the class now appears in Appearance → Widgets for every area. (Admins need the view widgets permission for the screen; see Known quirks.) The admin page lists Widgets::all() and validates additions against it.
admin.widgets.store creates a row and returns the editor HTML) and edits it (update saves title, settings and flags). Verified end to end.WidgetsServiceProvider waits until every provider has booted (so module widget classes are registered), then loads the active areas and instances for the visitor's device class (web or mobile, via MobileDetect), cached for one hour per device (widget_areas_web, widget_areas_mobile), and calls Widget::group($area)->position($order)->addWidget($class, ['title' => ..., 'settings' => ...]) (or addAsyncWidget when the instance is marked async).@widgetGroup('post-sidebar') (or {!! Widget::group('post-sidebar')->display() !!}) and guard with @if (! Widget::group('post-sidebar')->isEmpty()).widget_areas_web / widget_areas_mobile when instances change (the model's default cache keys are different, and the response cache lives in its own store). A placement edited in the admin can therefore take up to an hour to appear on the public site unless the application cache is cleared. In the test suite the response cache shares the default array store, which hides this. After changing instances in code or tests call Cache::forget('widget_areas_web') and Cache::forget('widget_areas_mobile').Pages can embed an area too
The core shortcode widget-area (<x-shortcode-widget-area widget="post-sidebar">) renders any area inside page content, so editors can reuse your widgets in the middle of a page.
| Property / method | Effect |
|---|---|
public $cacheTime = 30; (minutes) and cacheKey(array $params) | Cache the rendered widget. The default key serialises the params; the base class prefixes artisan.widgets. |
Async instance (ajax = 1) + placeholder() | Loaded after page load from arrilot/load-widget; the placeholder shows meanwhile |
public $reloadTimeout = 60; (seconds) | Async widgets re-fetch themselves periodically |
container(): array | Wrapper element for async/reloadable widgets (default div.widget-container) |
run() dependencies | Type-hinted arguments are resolved from the container (PostsWidget::run(PostRepository $repository)) |
Do not use async widgets for anything above the fold or in the response cache's guest-shared pages unless the output is not per-visitor.
An area is just a row. Seed it in a migration/seeder and print it from your theme or page view:
php (migration or seeder)
WidgetArea::firstOrCreate(
['name' => 'shop-sidebar'],
['title' => 'Shop', 'description' => 'Widgets next to the shop archive.', 'order' => 20],
);<x-front-layout>
...
@if (! Widget::group('shop-sidebar')->isEmpty())
<x-slot:sidebar><div class="sidebar sidebar-lg">@widgetGroup('shop-sidebar')</div></x-slot>
@endif
</x-front-layout>TruCommerce does this for its shop sidebar; the page builder's Sidebar block lists all areas via PageBuilderPresenter::widgetAreasJson().
| Use a… | when… |
|---|---|
| Widget | The same content should repeat on many pages in a fixed region and an admin should manage it without the page builder |
| Shortcode | It belongs inside one page's content and the editor controls where it sits |
| Data-bound section | The editor designs the markup and wants real records inside it |
It is common to ship all three from one component: the Testimonials example has a shortcode and a widget that read the same model.