hexadog/laravel-themes-manager
Laravel Themes Manager lets you add multiple themes to a Laravel app, each with its own views, assets, translations, config and routes. Switch themes at runtime, scaffold new themes with Artisan commands, and keep theme files isolated and publishable.
This package gives Laravel a first-class notion of a "theme": a self-contained directory holding views, translations, assets, and optionally config, which the manager can enumerate and activate at runtime.
composer require hexadog/laravel-themes-manager
php artisan vendor:publish --provider="Hexadog\ThemesManager\ThemesManagerServiceProvider"
The release you're on (v1.15.0) adds Laravel 12.x and 13.x support, so verify your framework constraint in composer.json before upgrading; the underlying API surface is unchanged from v1.14.config/themes.php first. That file is the contract for everything else: where themes live on disk (path, typically base_path('themes') or resources/themes), the default/fallback theme, asset handling, and cache behaviour. Read it before writing a single line of code.php artisan theme:list to see what's registered, php artisan theme:make <name> to generate the skeleton). Inspect the generated directory — the folder layout is the real documentation.resources/views/welcome.blade.php into your theme, activate the theme, and confirm the render picks it up. That single loop (create → activate → verify) is the whole package in miniature.Resolve the active theme in one place. Don't scatter ThemesManager::set() calls. Pick a single resolution strategy and put it in a middleware or a small service:
// app/Http/Middleware/ResolveTheme.php
public function handle($request, Closure $next)
{
$theme = $request->user()?->theme
?? config('themes.default');
themes()->set($theme);
return $next($request);
}
Register it globally or on the web group, and keep it near the top of the stack so downstream middleware and controllers see the right theme.
Per-tenant / per-domain theming. A very common production shape: map hostname or tenant model to a theme name, resolve in middleware, and let the package handle the rest. Because activation is just container state, everything downstream — view(), trans(), asset() — automatically follows.
Layered view fallback. Treat the theme as an overlay, not a replacement. Keep shared layouts in resources/views and only theme-specific overrides in the theme directory. Configure the view paths so the theme's views are searched first and Laravel falls back to the base app. This keeps 90% of a theme shared and makes new themes cheap.
Config-driven theme registry. If you need a curated list (paid plans, feature flags), gate themes in your own config/themes.php extension or a dedicated config file rather than checking file_exists at runtime. The manager will happily report a theme as available just because the folder exists — the policy decision belongs to your app.
Assets: symlink, don't copy. Wire the theme's public assets into public/ via the package's publish/symlink mechanism and run it as part of your deploy pipeline. In local dev, re-run after adding files; in CI/CD, run it as a deploy step, not a request-time operation.
Testing. Use Orchestra Testbench for package-level work (v1.15.0 bumps the testbench dependency, so re-run your suite after upgrading). In application tests, either set the theme explicitly in setUp() or assert on the resolved theme in your middleware test. Snapshot-testing rendered output per theme catches override regressions fast.
Composition with translations. Theme-level lang/ directories are the reason not to hand-roll i18n per theme. Keep translation keys namespaced (theme::messages.welcome) so fallback to the app's base translations stays predictable.
php artisan config:cache freezes the theme path and default. Any deploy that caches config must rebuild after theme directories are in place, and php artisan config:clear should be your first debugging step.php artisan view:clear whenever theme structure changes.public/ isn't shared with the build stage. Run the symlink step in the entrypoint, and prefer relative URLs over absolute ones so CDN/domain swaps don't strand theme assets.themes()->set() is request-scoped state, not persistence. It does not write to session or DB. If the user picks a theme, persist that choice yourself and re-resolve it on each request — otherwise it silently resets.theme:list (is it discovered?) → config('themes.path') (right root?) → view:clear + config:clear → middleware trace (did set() actually run?) → php artisan about for cached config state.How can I help you explore Laravel packages today?