Creating Custom Themes
Create a custom theme as a Capell package when it should be installable, testable, and reusable across apps. Keep one-off project styling in the app only when it depends on private app code.
Package Shape
Section titled “Package Shape”A theme package should include:
composer.jsoncapell.json- a service provider
- a
ThemeDefinitionDataregistration - one
ThemeRenderer - section renderers for the sections the package owns
- public CSS/JS assets registered from PHP
- preset definitions with preview metadata
- package tests for manifest, renderer registration, presets, preview image, and public-output safety
Themes that extend Foundation should declare the parent through package metadata and keep only the changed sections/assets in the child package.
Register The Theme
Section titled “Register The Theme”Register with ThemeRegistry from the package service provider:
use Capell\Core\Enums\FrontendRuntime;use Capell\Core\ThemeStudio\Data\ThemeDefinitionData;use Capell\Core\ThemeStudio\Data\ThemePresetData;use Capell\Core\ThemeStudio\Theme\ThemeRegistry;
public function boot(ThemeRegistry $themes): void{ $themes->register( definition: new ThemeDefinitionData( key: 'agency-launch', name: 'Agency Launch', description: 'Portfolio and lead generation theme for service businesses.', package: 'capell-app/theme-agency-launch', previewImage: '/vendor/capell/theme-agency-launch/preview.jpg', tags: ['portfolio', 'lead-generation'], bestFit: ['agency', 'consulting'], includedSections: ['navigation', 'hero', 'features', 'footer'], presets: [ new ThemePresetData( key: 'editorial-warmth', name: 'Editorial Warmth', description: 'Warm accent palette with editorial spacing.', previewImage: '/vendor/capell/theme-agency-launch/presets/editorial-warmth.jpg', values: [ 'primaryColor' => '#0f766e', 'accentColor' => '#f59e0b', 'radius' => 'sm', ], ), ], assets: [ 'frontend' => '/vendor/capell/theme-agency-launch/theme.css', ], runtime: FrontendRuntime::Livewire, extends: 'foundation', ), themeRenderer: new AgencyLaunchThemeRenderer, sectionRenderers: [ new HeroSectionRenderer, new FeaturesSectionRenderer, ], );}Keep key stable. Installed themes.key, preview tokens, cache keys, and diagnostics all rely on it.
Presets
Section titled “Presets”Presets are explicit choices, not hidden defaults. Each preset needs:
- stable key
- human label
- short description
- preview image path
- token values using the shared vocabulary in Frontend themes
Package-specific values are allowed, but the renderer must handle missing or unknown values safely.
Theme Editor Extension
Section titled “Theme Editor Extension”Package themes can extend the admin Theme Editor by binding a class that implements
Capell\Admin\Contracts\Themes\ThemeEditorExtension and tagging it with
ThemeEditorExtension::TAG.
Use an extension when a package needs:
- extra editor sections or fields
- package-specific preview sample content
- a custom preview Blade component
- extra CSS variables or data attributes derived from editor state
The extension receives a ThemeEditorContextData instance so it can decide whether
it supports the current theme. Keep editor-only values under the clean editor state
shape:
meta.editor.preset.activemeta.editor.brand.*meta.editor.header.*meta.editor.surface.*meta.editor.footer.*meta.editor.assets.*meta.editor.advanced.*admin.editor.*
Do not read old flat meta or admin editor fields from new extension code.
Legacy fields remain compatibility data only.
use Capell\Admin\Contracts\Themes\ThemeEditorExtension;use Capell\Admin\Data\Themes\ThemeEditorContextData;use Capell\Admin\Data\Themes\ThemeEditorStateData;
final class AgencyLaunchThemeEditorExtension implements ThemeEditorExtension{ public function supports(ThemeEditorContextData $context): bool { return $context->themeKey === 'agency-launch'; }
public function editorSections(ThemeEditorContextData $context): array { return []; }
public function samplePreviewContent(ThemeEditorContextData $context): array { return [ 'headline' => 'Launch campaigns with reusable sections.', 'body' => 'Preview copy should show the package sections without querying public models.', ]; }
public function previewComponent(ThemeEditorContextData $context): ?string { return null; }
public function cssVariables(ThemeEditorStateData $state, ThemeEditorContextData $context): array { return [ '--agency-launch-accent' => $state->brand['accentColor'] ?? '#f59e0b', ]; }
public function dataAttributes(ThemeEditorStateData $state, ThemeEditorContextData $context): array { return [ 'theme-package' => $context->themeKey, ]; }}Register it from the package service provider:
$this->app->tag(AgencyLaunchThemeEditorExtension::class, ThemeEditorExtension::TAG);Renderer Rules
Section titled “Renderer Rules”Theme renderers receive prepared ThemePageData. They should render from that data and registered section renderers.
Do not query models from public Blade views. Load public render data before the view receives it, or add an explicit view component/action that prepares the data.
Do not print authoring metadata. Public HTML must be safe for anonymous visitors, signed-in users, admins, crawlers, cache files, and static exports.
Assets
Section titled “Assets”Register package asset sources from PHP:
- Tailwind source paths through
TailwindAssetsRegistry - CSS/JS dependencies through the package provider or the package’s frontend asset registration code
- token-compatible CSS through
ThemeTokenStoreoutput, not hard-coded critical CSS paths
The current direction is to use the optional frontend optimizer package for generated critical CSS. Do not add a theme-level Beasties/Critters fallback.
Diagnostics And Tests
Section titled “Diagnostics And Tests”Run the authoring validation command in the app:
php artisan capell:themes:validate agency-launchPackage tests should cover:
capell.jsonand Composer metadata are valid- provider registers the theme definition and renderer
- all advertised presets resolve
- preview images are present in the package/public asset path
- section renderers cover the declared sections or inherit safely from Foundation
- public rendered output contains expected frontend HTML
- public rendered output does not expose authoring metadata
Use a route-backed frontend test for at least one seeded page. A renderer-only unit test is useful, but it will not catch package boot, asset, cache, or public safety regressions.