Skip to content

Extending Capell

Who is this for? Developers who want to extend Capell with page types, model interceptors, event subscribers, settings, frontend hooks, or add-on packages.

Capell is designed to be extended without modifying its core source code. This document explains the main extension points available to your application and to package authors.


  1. Add-on Packages
  2. Page Types And Component Registration
  3. Model Interceptors
  4. Schema Hook Extenders
  5. Event Registry (Callbacks & Subscribers)
  6. Render Hooks
  7. Settings Schema Registry
  8. Extending core models
  9. Package Metadata And Discovery
  10. Further Reading

Capell offers several extension mechanisms. Pick by what you’re extending:

I want to…UseSection
Add a page subject typeCapellCore::registerPageType(new PageTypeData(...))§2
Register component aliasesCapellCore::registerComponent() / registerComponents()§2
Change seed/install data for pages, layouts, themes, or blueprintsCapellCore::registerModelInterceptor()§3
Add fields to an existing page or site edit formSchema hook extender§4
React to an admin lifecycle event (e.g. after save)Event registry callback / subscriber§5
Block an action based on a conditionValidationSubscriber§5
Subscribe to fine-grained lifecycle eventsSubscriberManager::subscribe()Subscriber Manager
Hook into static site exportStaticSiteExtensionRegistry::register()Static Site Extensions
Inject HTML into a frontend Blade componentRender hook§6
Register selectable theme header/footer componentsThemeChromeRegistry::register*()Theme Chrome Components
Add a custom admin toolbar itemTag with AdminToolItem::TAGAdmin Tool Registry
Add a dashboard widget programmaticallyCapellAdmin::registerDashboardWidget()Dashboard Widget Customization
Add a tab to the admin Settings pageSettings Schema Registry§7
Wire model changes to cache flushesCacheInvalidationRegistry::registerDependency()Cache Invalidation
Register critical/deferred frontend assetsCriticalAssetRegistry::register*()Critical Asset Optimization
Cache an expensive Blade fragment@cache(...) ... @endcache directiveFragment Caching
Enable conditional responses via ETagsETagMiddleware + header handlingETag & Conditional Responses
Lazy-load JavaScript for non-critical routesLazy page hydrationLazy Page Hydration
Register Tailwind sources, imports, or pluginsTailwindAssetsRegistry::register*()Tailwind Assets
Load vendor build assets only for matching pagesVendorAssetConditionRegistry::register()Conditional Vendor Assets
Substitute my own subclass for a core modelContainer binding§8
Register package metadatacapell.json plus manifest registration§9

Rule of thumb: prefer targeted extension points over publishing or copying host package files. Published files stop receiving upstream fixes.


The simplest way to extend Capell is to install one of the first-party, opt-in approved packages — blog, themes, sitemaps, navigation, the Curator media backend, the workspace draft/publish system, and more. Each follows the same install pattern:

Terminal window
composer require capell-app/<package>
php artisan capell:<package>-install

For the full registry, dependency map, and per-package descriptions see Approved packages in the top-level docs. The remainder of this guide covers extension points you reach for when you’re writing your own code.


Page types describe the model subject a Blueprint can target. Core registers the built-in page, site, and theme subjects from BlueprintSubjectEnum.

Register a page type from a provider when a package owns a new public content subject:

use Capell\Core\Data\PageTypeData;
use Capell\Core\Facades\CapellCore;
use Vendor\Example\Models\LandingExperience;
public function boot(): void
{
CapellCore::registerPageType(new PageTypeData(
name: 'landing-experience',
model: LandingExperience::class,
label: __('capell-example::page_types.landing_experience'),
));
}

Use plain strings for labels when the data may cross Livewire boundaries. Closures can dehydrate badly in Livewire state; Core eagerly resolves built-in labels for this reason.

Component aliases are a separate registry. Use them when package code needs to refer to renderable component names by a stable type/key pair:

CapellCore::registerComponent('Page', 'LandingExperience', 'capell-example::pages.landing-experience');

For frontend widgets, use WidgetRegistry; for admin content widgets, use CapellAdmin::registerWidget() or registerDiscoverableWidgets().

Model interceptors let packages change default install/setup data without replacing Core actions. They are used for creation flows where Capell owns the write but packages need to adjust data before persistence or react after creation.

Register an interceptor for the model, optional key/conditions, and interface:

use Capell\Core\Contracts\ModelInterceptors\PageInterceptorInterface;
use Capell\Core\Facades\CapellCore;
use Capell\Core\Models\Page;
public function boot(): void
{
CapellCore::registerModelInterceptor(
Page::class,
ExampleHomePageInterceptor::class,
key: ['slug' => 'home'],
priority: 20,
);
}

The interceptor must implement the matching contract, such as PageInterceptorInterface, BlueprintInterceptorInterface, LayoutInterceptorInterface, or ThemeInterceptorInterface:

use Capell\Core\Contracts\ModelInterceptors\PageInterceptorInterface;
use Capell\Core\Contracts\Pageable;
final class ExampleHomePageInterceptor implements PageInterceptorInterface
{
public function beforeCreate(array $data): array
{
$data['meta']['example_package'] = true;
return $data;
}
public function afterCreated(Pageable $page, array $data): void
{
// Dispatch package-owned setup work here if needed.
}
}

Use interceptors for package defaults, not ordinary user writes. Runtime writes should go through Actions.


Schema hook extenders let you inject form fields into page or site edit form-builder at named positions, without overriding the entire schema.

The PageTranslationSchemaHookEnum enum defines named injection points in the page translation editor:

HookPosition
BeforeTitleBefore the title field
AfterTitleAfter the title field
AfterContentEditorAfter the main content editor
AfterExtraContentAfter the extra content section
BeforeSearchMetaBefore the SEO/meta fields
AfterSearchMetaAfter the SEO/meta fields

Create a class implementing PageSchemaExtender and tag it with PageSchemaExtender::TAG:

<?php
namespace App\Filament\FormBuilder;
use Capell\Admin\Contracts\Extenders\PageSchemaExtender;
use Capell\Admin\Enums\PageTranslationSchemaHookEnum;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;
use Illuminate\Database\Eloquent\Model;
class CustomPageSchemaExtender implements PageSchemaExtender
{
public function extendTranslationComponentsForHook(
Schema $schema,
PageTranslationSchemaHookEnum $hook
): array {
return match ($hook) {
PageTranslationSchemaHookEnum::AfterTitle => [
TextInput::make('subtitle')
->label('Subtitle')
->maxLength(255),
],
default => [],
};
}
public function extendSidebarComponents(Schema $schema): array
{
return [];
}
public function extendRelationManagers(Model $record, array $relationManagers): array
{
return $relationManagers;
}
public function extendTabs(Schema $schema, array $tabs): array
{
return $tabs;
}
}

Register the extender in your service provider:

$this->app->tag([CustomPageSchemaExtender::class], PageSchemaExtender::TAG);

See Schema Hooks reference for full details including Site extenders.


5. Event Registry (Callbacks & Subscribers)

Section titled “5. Event Registry (Callbacks & Subscribers)”

The Admin package provides an event registry that lets packages and applications subscribe to lifecycle events in the admin panel.

use Capell\Admin\Filament\Resources\Pages\EditPage;
use Capell\Admin\Support\AdminEventHandlerInterface;
use Capell\Admin\Support\AdminEventRegistry;
use Livewire\Component;
final class RefreshPreviewHandler implements AdminEventHandlerInterface
{
public function handle(array $payload, Component $component): void
{
if ($component instanceof EditPage) {
$component->dispatch('refresh-preview');
}
}
}
resolve(AdminEventRegistry::class)->register(
EditPage::class,
'refreshPreview',
RefreshPreviewHandler::class,
);

The callback only runs when the event fires from an instance of the specified class.

For more complex event handling, implement EventSubscriber:

use Capell\Core\Facades\CapellCore;
use Capell\Core\Contracts\EventSubscriber;
class MyEventSubscriber implements EventSubscriber
{
public function handle(string $event, object $context): void
{
if ($event === 'afterSave') {
// Handle the event
}
}
}
// Register it
CapellCore::subscriberManager()->subscribe(MyEventSubscriber::class);
// Unregister when done
CapellCore::subscriberManager()->unsubscribe(MyEventSubscriber::class);

For events that need to prevent an action from completing, implement ValidationSubscriber:

use Capell\Admin\Contracts\ValidationSubscriber;
use Capell\Core\Models\Blueprint;
class TypeDeletionValidator implements ValidationSubscriber
{
public function handle(string $event, object $context): void {}
public function validate(string $event, object $context): bool
{
if ($event === 'validateCustomType' && $context instanceof Blueprint) {
// Return false to prevent deletion
return ! $this->hasRelatedRecords($context);
}
return true;
}
}
EventTriggered by
afterSaveAfter a page is saved in EditPage
validateCustomTypeWhen validating whether a type can be deleted

To dispatch a custom event from your code:

use Capell\Core\Support\Subscriber\SubscriberManager;
resolve(SubscriberManager::class)->notifySubscribers('myEvent', $context);

Render hooks let you inject HTML into named locations in frontend Blade components without overwriting any files.

See Render Hooks for the full guide.

Quick example: Add a badge after the title in asset tile components:

use Capell\Frontend\Enums\RenderHookLocation;
use Capell\Frontend\Support\Render\RenderHookRegistry;
app(RenderHookRegistry::class)->register(
RenderHookLocation::AfterTitle,
function ($context) {
return '<span class="badge">New</span>';
},
priority: 10,
scenario: 'asset'
);

Add your package’s settings to the admin Settings page by registering a schema and settings class:

use Capell\Core\Support\Settings\SettingsSchemaRegistry;
private function registerSettingsSchemas(): self
{
$registry = resolve(SettingsSchemaRegistry::class);
$registry->registerSettingsClass('myplugin', MyPluginSettings::class);
$registry->register('myplugin', MyPluginSettingsSchema::class);
return $this;
}

See Settings Schema Registry for the full API reference, including how to extend, replace, or remove schemas from other packages.


To substitute your own subclass for a core Capell model (e.g. Page, Site), bind the replacement in your package service provider’s register() method:

use Capell\Core\Models\Page;
public function register(): void
{
$this->app->bind(Page::class, MyExtendedPage::class);
}

Any Capell code that resolves the model via app(Page::class) will receive an instance of MyExtendedPage. This includes Filament resources, loaders, and actions that explicitly resolve through the container.

Previous versions used CapellCore::registerModel(ModelEnum::Page, MyExtendedPage::class). That API has been removed. Container bindings are the replacement.


Packages should describe themselves through capell.json and Composer metadata. Manifest registration is the source of truth for package name, kind, scopes, providers, commands, requirements, settings ownership, marketplace metadata, and contribution counts.

Provider-side CapellCore::registerPackage(...) remains for trusted first-party bootstrap and compatibility paths. New packages should prefer manifest metadata and keep providers focused on wiring concrete runtime registrations.

Register package models with CapellCore::registerModels([...]) when they should appear in diagnostics, protected-table checks, morph maps, exports, or package metadata. This does not replace a Core model implementation; use a container binding for that.

If a package contribution is missing, start with:

Terminal window
composer dump-autoload
php artisan optimize:clear
php artisan capell:package-cache:clear
php artisan list capell

Then check Extension Troubleshooting for the runtime-specific path.


Theme packages can register public header and footer Blade components for admin selection without hard-coding free-text component names into theme forms.

use Capell\Core\Support\Themes\ThemeChromeRegistry;
app(ThemeChromeRegistry::class)->registerHeader('vendor-theme::header', 'Vendor header');
app(ThemeChromeRegistry::class)->registerFooter('vendor-theme::footer', 'Vendor footer');

The admin theme form validates header_file and footer_file against the registered options when saving. Public frontend rendering still reads the saved component name directly, so registration affects admin validation and editing, not normal page boot.