Register a widget type in register() with a render callback returning HTML. Filter by the viewer's groups inside it — a widget renders on pages you are not thinking about.
$this->app->make('widget_types')->register('example.latest', [
    'label' => 'Latest examples',
    'group' => 'Example',
    'module' => 'example',
    'render' => fn (array $w, ?array $viewer, bool $dark): string
        => $this->latestHtml($w),
]);

And an area, if your pages should accept widgets:

$this->app->make('widget_areas')->register('example.side', [
    'label' => 'Beside the examples',
    'hook' => 'example.index.side',
    'module' => 'example',
]);

Then @hook('example.index.side') in the template.

🚨 Both in register(), never boot(). The widget module wires a listener per area during its boot, so an area registered in boot() only works if your module happens to boot first.

🚨 Filter by the viewer's groups inside your render. A widget renders on pages the operator is not thinking about, which makes it the easiest place in the product to leak something — and filter before the limit, or the widget quietly shows five rows instead of ten.

Return an empty string to draw nothing. The renderer handles the arranging case.

Sign in to say whether this helped.