victoire/html-widget
victoire/html-widget is a Laravel/PHP package for defining, managing, and rendering reusable HTML widgets. It helps embed custom markup cleanly while keeping widget content organized, reusable, and easy to integrate into dashboards, CMS blocks, and dynamic page sections.
Reality check before anything else. Despite the "Laravel package" framing in the description, victoire/html-widget is a Symfony/Victoire CMS widget bundle from 2016 — Doctrine entity, Symfony form type, Twig templates, AppKernel registration. composer require victoire/html-widget in a Laravel app gives you no service provider, no facade, no config file, no Blade directive. Spend ten minutes in the repo confirming this (*Bundle.php, Entity/, Form/, Resources/views/) before planning around it.
If you're on legacy Victoire/Symfony: register VictoireWidgetHtmlBundle in AppKernel.php, run doctrine:schema:update --force (missing this is the #1 silent failure), open a page in the editor, drop the HTML widget into a slot, paste an embed, clear cache.
If you're on Laravel (the likely case): don't install it — copy the pattern. You need roughly:
widgets table (key, title, html, is_active, updated_by, timestamps),HtmlWidget model plus a WidgetRenderer contract bound in the container,<x-html-widget key="sidebar-promo" /> that echoes {!! $html !!} inside its own wrapper,Where to look first in the repo — these three files are the whole spec, and are ~30 lines to translate:
Entity/WidgetHtml.php — the persisted field shape.Form/WidgetHtmlType.php — what the editor UI actually exposes.Resources/views/ — the front-end markup contract.First use case: a dashboard or sidebar "notice/embed" block — marketing pastes an iframe, form snippet, or JSON-LD into an admin textarea; the component renders it verbatim. That's the smallest valuable slice and matches the original package's intent.
Funnel every render through one entry point. Never {!! $widget->html !!} inline in page templates. One HtmlWidget::render($key) (or a <x-html-widget> component) is the only place raw output happens — sanitization, caching, kill-switch, and logging all live there.
Key-based storage beats page-based storage. A key column lets the same snippet appear on multiple pages, gives you an audit trail (updated_by), and makes cache-busting trivial. This mirrors how the Victoire bundle attaches one widget instance to a page slot.
Template owns the container, widget owns the inner embed. Your component emits <div class="widget widget--{key}">…</div>; the CMS/admin only stores the fragment. Otherwise editors paste <div> wrappers inconsistently and layout changes require content edits.
Sanitize on write, not on read. Use mews/purifier or symfony/html-sanitizer with an allowlist in a model mutator / saving observer. Or split into html_sanitized vs html_trusted with a boolean gate tied to a permission. Sanitizing on read means N sanitizer calls per request and a moving target for your allowlist.
Cache and bust via observer. Cache::rememberForever("widget:{$key}") in the renderer, with a saved/deleted observer calling Cache::forget. Widget HTML is static; there's no reason to hit the DB per page request.
Seed, don't click. For migrations from Victoire (or any legacy CMS), export rows and map fields into a seeder or a one-off artisan command. Scripting the editor UI is fragile and slow.
Feature-flag the whole thing. config('widgets.enabled') checked in the renderer gives you an incident response — turn off raw HTML rendering without a deploy if something malicious gets through.
Site-wide snippets belong in the layout. Analytics, chat widgets, cookie banners — these go in the base Blade layout, not duplicated per page via the widget mechanism. Same advice the Victoire bundle implicitly requires.
composer.json extra.laravel block, no service provider, no publishable config. Don't burn a sprint trying to make Symfony bundle bootstrapping work in Laravel's container — the adapter costs more than a native implementation.updated_by audit column, and log the diff. A compromised low-privilege editor is a stored-XSS vector.title/sandbox attributes. Add a save-time validation step (sanitizer + tag balance) and consider rendering a preview inside a sandboxed iframe.key plus a content version if you ever A/B.doctrine:schema:update; in Laravel it's the migration you forgot after adding a column. Same class of bug, easier to reproduce locally.app/Resources/VictoireWidgetHtmlBundle/views/ rather than forking the entity — keeps you upgradeable if upstream ever moves. In Laravel, the equivalent is a WidgetRenderer interface bound in AppServiceProvider, so you can swap to a sanitized/sandboxed renderer without touching call sites.How can I help you explore Laravel packages today?