Skip to content

Frontend Extensions

Frontend packages should register behaviour through render hooks, route/path registries, Blade components, Livewire components, vendor assets, cache dependencies, and explicit package registries.

If the package needs in-page editing, do not put editor metadata in its views. Register editable regions with capell-app/frontend-authoring so the admin-only beacon can add controls after the page has loaded.

Use RenderHookRegistry for injected HTML:

use Capell\Frontend\Enums\RenderHookLocation;
use Capell\Frontend\Support\Render\RenderHookRegistry;
resolve(RenderHookRegistry::class)->register(
RenderHookLocation::HeadClose,
fn (): string => view('capell-example::head.tags')->render(),
);

Use render hooks for SEO tags, consent-aware analytics snippets, public theme fragments, and other output that genuinely belongs in the rendered page.

Do not use render hooks to add authoring buttons, hidden model IDs, field paths, selectors, signed edit URLs, or other editor state to public HTML. Cached HTML must stay safe for anonymous and non-admin visitors.

Register package Tailwind sources or imports through Capell asset registries when package views contain classes that must be compiled.

Use TailwindAssetsRegistry for source files, imports, plugins, and theme colors. Use CriticalAssetRegistry only for assets that must be emitted as critical or deferred public page assets at runtime.

use Capell\Core\Support\Tailwind\TailwindAssetsRegistry;
public function boot(TailwindAssetsRegistry $assets): void
{
$assets->registerSource(__DIR__ . '/../resources/views/**/*.blade.php', 'capell-example');
$assets->registerImport('@source "../../vendor/capell-app/example/resources/views/**/*.blade.php";', 'capell-example');
}

Widget Resources, Presentation, And Interactions

Section titled “Widget Resources, Presentation, And Interactions”

Frontend widgets can register runtime resource groups, type-level presentation defaults, and default interaction triggers through WidgetRegistry::registerDefinition().

Use this split when building package widgets:

DataPurpose
Normal widget dataContent props the widget component needs.
data.__capell.presentationInstance-level delivery and display overrides.
data.__capell.interactionsPublic trigger definitions.
Widget definition defaultsPackage-owned defaults for presentation, resources, and interactions.

Public rendering strips __capell before passing props to the widget component. Keep component data focused on what the widget displays; keep runtime behaviour in Capell metadata.

Use FrontendResourceRegistry for widget CSS and JavaScript that should only load when a widget appears on the page:

use Capell\Core\Data\Widgets\WidgetDefinitionData;
use Capell\Core\Enums\PresentationLoadingStrategy;
use Capell\Core\Support\Widgets\WidgetRegistry;
use Capell\Frontend\Support\Assets\FrontendResourceRegistry;
public function boot(FrontendResourceRegistry $resources, WidgetRegistry $widgets): void
{
$resources
->group('example.carousel')
->css('resources/css/carousel.css', buildPath: 'vendor/example')
->js('resources/js/carousel.js', buildPath: 'vendor/example', loading: PresentationLoadingStrategy::Visible);
$widgets->registerDefinition(WidgetDefinitionData::frontendBlade(
key: 'carousel',
component: 'example::widgets.carousel',
resourceGroups: ['example.carousel'],
defaultPresentationSettings: [
'width_mode' => 'container',
'loading_strategy' => 'visible',
],
defaultInteractionTriggers: [
[
'label' => 'Open gallery',
'target_type' => 'widget',
'behavior' => 'modal',
'target_widget' => [
['type' => 'carousel', 'data' => ['mode' => 'gallery']],
],
],
],
));
}

The public asset manifest exposes generated resource IDs, not package keys or component names. Keep package names, model IDs, field paths, signed URLs, and editor metadata out of widget placeholders and rendered HTML.

Interactions are public triggers that can open lazy widgets, lazy Layout Builder fragments, safe URLs, or public-action fallbacks. They are designed for editor-managed video modals, form slide-overs, galleries, calculators, comparison panels, and optional content.

Every interaction has a trigger, behaviour, and target:

PiecePackage responsibility
TriggerProvide sensible labels/icons/defaults where the widget type has an obvious action.
BehaviourUse the shared runtime behaviours: modal, slide_over, inline_reveal, replace_region.
TargetRegister widgets or integrate with Layout Builder fragments instead of adding bespoke public routes.

Use these storage paths:

SurfacePath
Widget instancedata.__capell.interactions
Widget type defaultWidgetDefinitionData::$defaultInteractionTriggers
Layout Builder block instancelayout block meta.interactions
Layout Builder block type defaulttype meta.interactions

Lazy widget targets render through /_capell/widgets/{reference}. Lazy Layout Builder fragments render through /_capell/fragments/{reference}. Both use encrypted opaque references and generic failures.

Valid interaction behaviours are modal, slide_over, inline_reveal, and replace_region. Package JavaScript should not implement its own modal shell for normal widget interactions; register widget assets and let the frontend runtime fetch and mount the target.

Public triggers may include labels, style hints, generated URLs, analytics keys, and generic runtime attributes. They must not include target widget content, widget keys, component names, package names, model IDs, block keys, field paths, signed URLs, or editor metadata.

Use widget targets when the package owns a reusable experience, such as a video player or calculator. Use fragment targets when the content already exists as a public Layout Builder block and should be fetched later.

Blade components can be registered from the package provider:

Blade::componentNamespace('Capell\\Example\\View\\Components', 'capell-example');

Livewire components should use a package prefix:

Livewire::component('capell-example::preview', PreviewComponent::class);

When a package needs a stable component alias that other Capell code can resolve, register it with FrontendComponentRegistry:

use Capell\Frontend\Contracts\FrontendComponentRegistryInterface;
resolve(FrontendComponentRegistryInterface::class)->register(
key: 'capell-example.preview',
component: 'capell-example::preview',
aliases: ['example-preview'],
props: ['title', 'items'],
);

Use the registry when content or layout state needs a stable component key. Direct Blade/Livewire registration is enough when the component is only referenced by package-owned views.

Register package-owned public routes from the package provider and reserve their paths so the frontend page fallback does not treat them as CMS pages.

use Capell\Frontend\Support\Routing\ReservedFrontendPathRegistry;
resolve(ReservedFrontendPathRegistry::class)->reservePrefix('example-api');
resolve(ReservedFrontendPathRegistry::class)->reserveExact('example-webhook');

reservePrefix('example-api') reserves example-api and every path below it. reserveExact('example-webhook') reserves only that path. Paths are normalized, so leading/trailing slashes do not matter.

Use FrontendRouteMiddlewareRegistry only when the package needs to add middleware to the public page route itself. Most packages should add their own route middleware instead.

use Capell\Frontend\Http\Middleware\RejectReservedFrontendPaths;
use Capell\Frontend\Support\Routing\FrontendRouteMiddlewareRegistry;
resolve(FrontendRouteMiddlewareRegistry::class)->insertAfter(
RejectReservedFrontendPaths::class,
[ExampleFrontendMiddleware::class],
);

The default public page middleware order starts with reserved-path rejection, then web, maintenance, workspace context, frontend resolution, and anonymous cacheability checks. Put package middleware as late as possible so it has the resolved context it needs without bypassing reserved-path and maintenance behavior.

Frontend registers /_capell/widgets/{reference} for lazy widget targets and reserves _capell. Layout Builder registers /_capell/fragments/{reference} for lazy public block fragments. Do not add plain-ID fragment routes such as /fragments/page/12/block/hero. Widget and fragment references must be encrypted and self-contained, and fragment/widget responses must use their own cache headers rather than the normal page HTML cache.

Use FrontendRuleConditionRegistry when package settings or runtime manifests need named boolean conditions:

use Capell\Frontend\Contracts\FrontendRuleCondition;
use Capell\Frontend\Data\FrontendRuleContextData;
use Capell\Frontend\Support\Rules\FrontendRuleConditionRegistry;
final class ExampleCampaignCondition implements FrontendRuleCondition
{
public function key(): string
{
return 'example-campaign';
}
public function evaluate(array $parameters, FrontendRuleContextData $context): bool
{
return (bool) ($parameters['enabled'] ?? false);
}
}
resolve(FrontendRuleConditionRegistry::class)->register(ExampleCampaignCondition::class);

Use FrontendResponseRendererRegistry only when a package owns response rendering for a frontend runtime. The renderer receives the resolved FrontendRenderContextData and returns a Symfony response or Laravel Responsable.

use Capell\Core\Enums\FrontendRuntime;
use Capell\Frontend\Support\Render\FrontendResponseRendererRegistry;
resolve(FrontendResponseRendererRegistry::class)->registerClass(
FrontendRuntime::Blade,
ExampleBladeRenderer::class,
);

Do not replace the default renderer for small markup changes. Use render hooks, components, or theme views first.

If a package model affects public output, register model-to-cache dependencies during provider boot:

use Capell\Frontend\Support\Cache\CacheInvalidationRegistry;
use Vendor\Example\Models\ExampleBanner;
public function boot(CacheInvalidationRegistry $cacheInvalidation): void
{
$cacheInvalidation->registerDependency(
ExampleBanner::class,
['example-banners', 'homepage'],
);
}

Prefer exact keys. A pattern containing * intentionally flushes the whole capell-frontend cache tag because wildcard matching is not portable across cache drivers.

Use Content Sections areas when a frontend package or theme needs editor-managed elements outside the standard page body. Areas keep the normal Content Sections storage model: elements live in containers, and containers set meta.area. Missing meta.area values render as main.

Foundation Theme registers header and renders it from its header view:

<x-capell::layout.area area="header" />

A package can register another area through Capell\ContentSections\Support\LayoutAreas\LayoutAreaRegistry:

use Capell\ContentSections\Support\LayoutAreas\LayoutAreaRegistry;
$this->app->afterResolving(
LayoutAreaRegistry::class,
function (LayoutAreaRegistry $registry): void {
$registry->register(
key: 'announcement',
label: __('capell-example::layout_areas.announcement'),
themeKey: 'client',
);
},
);

Use areas for header, footer, announcement, campaign, or product chrome slots that should contain normal Content Sections elements. Do not fake this with hidden containers in the main loop. Public area Blade must not query the database, lazy-load relationships, or emit editor markers, model IDs, field paths, package metadata, signed admin URLs, or other authoring state.

Theme packages use kind: "theme" in capell.json. A child theme sets extends to the parent theme package name.

Themes own presentation. Shared theme infrastructure belongs in capell-app/foundation-theme, which also provides the default theme key. Premium or client themes can extend Foundation, or they can stand alone if they need a separate base contract.

Use a stable themeKey in the manifest:

{
"name": "vendor/theme-client",
"kind": "theme",
"themeKey": "client",
"extends": "capell-app/foundation-theme",
"requires": ["capell-app/foundation-theme"]
}

At install time, Capell only accepts a theme key that belongs to an installed theme package or a theme package selected for installation. The CLI option is --theme=client; the browser installer uses the same value in its theme selector.

Themes may expose stable presentation selectors that Frontend Authoring can target later, but those selectors should already make sense as normal HTML. Avoid hidden authoring-only attributes.

If frontend output is missing or stale, start with Extension Troubleshooting. The common checks are:

  • php artisan route:list to confirm package routes are registered before the frontend fallback;
  • check ReservedFrontendPathRegistry::exactPaths() and prefixes() in a focused test when package paths fall through to page rendering;
  • inspect FrontendRouteMiddlewareRegistry::all() in a test when middleware order matters;
  • assert FrontendComponentRegistryInterface::has() or hasReference() when component aliases are not resolving;
  • php artisan optimize:clear after changing route, config, or provider registration;
  • php artisan capell:html-cache:clear when static output is stale;
  • php artisan queue:work when invalidation or static generation is queued;
  • the package asset report or TailwindAssetsRegistry::toReport() when CSS is missing;
  • anonymous HTML inspection when cache writes are bypassed by public-output safety checks.