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.
Table of Contents
Section titled “Table of Contents”- Add-on Packages
- Page Types And Component Registration
- Model Interceptors
- Schema Hook Extenders
- Event Registry (Callbacks & Subscribers)
- Render Hooks
- Settings Schema Registry
- Extending core models
- Package Metadata And Discovery
- Further Reading
Which extension point do I use?
Section titled “Which extension point do I use?”Capell offers several extension mechanisms. Pick by what you’re extending:
| I want to… | Use | Section |
|---|---|---|
| Add a page subject type | CapellCore::registerPageType(new PageTypeData(...)) | §2 |
| Register component aliases | CapellCore::registerComponent() / registerComponents() | §2 |
| Change seed/install data for pages, layouts, themes, or blueprints | CapellCore::registerModelInterceptor() | §3 |
| Add fields to an existing page or site edit form | Schema hook extender | §4 |
| React to an admin lifecycle event (e.g. after save) | Event registry callback / subscriber | §5 |
| Block an action based on a condition | ValidationSubscriber | §5 |
| Subscribe to fine-grained lifecycle events | SubscriberManager::subscribe() | Subscriber Manager |
| Hook into static site export | StaticSiteExtensionRegistry::register() | Static Site Extensions |
| Inject HTML into a frontend Blade component | Render hook | §6 |
| Register selectable theme header/footer components | ThemeChromeRegistry::register*() | Theme Chrome Components |
| Add a custom admin toolbar item | Tag with AdminToolItem::TAG | Admin Tool Registry |
| Add a dashboard widget programmatically | CapellAdmin::registerDashboardWidget() | Dashboard Widget Customization |
| Add a tab to the admin Settings page | Settings Schema Registry | §7 |
| Wire model changes to cache flushes | CacheInvalidationRegistry::registerDependency() | Cache Invalidation |
| Register critical/deferred frontend assets | CriticalAssetRegistry::register*() | Critical Asset Optimization |
| Cache an expensive Blade fragment | @cache(...) ... @endcache directive | Fragment Caching |
| Enable conditional responses via ETags | ETagMiddleware + header handling | ETag & Conditional Responses |
| Lazy-load JavaScript for non-critical routes | Lazy page hydration | Lazy Page Hydration |
| Register Tailwind sources, imports, or plugins | TailwindAssetsRegistry::register*() | Tailwind Assets |
| Load vendor build assets only for matching pages | VendorAssetConditionRegistry::register() | Conditional Vendor Assets |
| Substitute my own subclass for a core model | Container binding | §8 |
| Register package metadata | capell.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.
1. Capell-Approved Packages
Section titled “1. Capell-Approved Packages”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:
composer require capell-app/<package>php artisan capell:<package>-installFor 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.
2. Page Types And Component Registration
Section titled “2. Page Types And Component Registration”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().
3. Model Interceptors
Section titled “3. Model Interceptors”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.
4. Schema Hook Extenders
Section titled “4. Schema Hook Extenders”Schema hook extenders let you inject form fields into page or site edit form-builder at named positions, without overriding the entire schema.
Available hooks
Section titled “Available hooks”The PageTranslationSchemaHookEnum enum defines named injection points in the page translation editor:
| Hook | Position |
|---|---|
BeforeTitle | Before the title field |
AfterTitle | After the title field |
AfterContentEditor | After the main content editor |
AfterExtraContent | After the extra content section |
BeforeSearchMeta | Before the SEO/meta fields |
AfterSearchMeta | After the SEO/meta fields |
Implementing a page schema extender
Section titled “Implementing a page schema extender”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.
Registering a callback
Section titled “Registering a callback”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.
Creating a subscriber
Section titled “Creating a subscriber”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 itCapellCore::subscriberManager()->subscribe(MyEventSubscriber::class);
// Unregister when doneCapellCore::subscriberManager()->unsubscribe(MyEventSubscriber::class);Validation subscribers
Section titled “Validation subscribers”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; }}Available events
Section titled “Available events”| Event | Triggered by |
|---|---|
afterSave | After a page is saved in EditPage |
validateCustomType | When validating whether a type can be deleted |
Adding new events
Section titled “Adding new events”To dispatch a custom event from your code:
use Capell\Core\Support\Subscriber\SubscriberManager;
resolve(SubscriberManager::class)->notifySubscribers('myEvent', $context);6. Render Hooks
Section titled “6. Render Hooks”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');7. Settings Schema Registry
Section titled “7. Settings Schema Registry”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.
8. Extending core models
Section titled “8. Extending core models”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.
9. Package Metadata And Discovery
Section titled “9. Package Metadata And Discovery”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:
composer dump-autoloadphp artisan optimize:clearphp artisan capell:package-cache:clearphp artisan list capellThen check Extension Troubleshooting for the runtime-specific path.
Theme Chrome Components
Section titled “Theme Chrome Components”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.
10. Further Reading
Section titled “10. Further Reading”Core Documentation
Section titled “Core Documentation”Extension Point Guides
Section titled “Extension Point Guides”- Static Site Extensions — Hook into static site generation
- Subscriber Manager — Subscribe to fine-grained lifecycle events
- Admin Tool Registry — Add custom toolbar items to the admin panel
- Dashboard Widget Customization — Programmatically register dashboard widgets
- Settings Schema Registry — Add package settings to the admin Settings page
- Render Hooks — Inject HTML into frontend Blade components
- Schema Hooks — Add fields to edit form-builder at named positions
- Tailwind Assets — Register Tailwind sources, imports, and plugins
Performance & Optimization
Section titled “Performance & Optimization”- Performance Index — Performance optimization overview
- Cache Invalidation — Wire model changes to cache flushes
- Critical Asset Optimization — Register critical/deferred frontend assets
- Fragment Caching — Cache expensive Blade fragments
- ETag & Conditional Responses — Enable conditional responses via ETags
- Lazy Page Hydration — Lazy-load JavaScript for non-critical routes