yaap/theme
YAAP Theme is a Laravel package for organizing and managing themes with a simple API. It supports theme activation, view and layout resolution, asset handling, and a flexible structure for separating theme resources from application code.
composer require yaap/theme (5.1.0 added Laravel 11 support; auto-discovery covers Laravel 5.5 through 11). On modern Laravel you don't touch config/app.php — the provider and Theme alias are discovered. For legacy apps, add Yaap\Theme\ThemeServiceProvider and the Theme alias (typically Yaap\Theme\Facades\Theme) manually.php artisan vendor:publish --provider="Yaap\Theme\ThemeServiceProvider" then open config/theme.php. This is the file you will live in — it defines the theme base path, the default theme, and any named themes / inheritance entries.config('theme.path') (usually resources/themes/). Mirror the standard Laravel resources layout inside it: views/ for Blade templates, and an assets/ (or public/) folder for css, js and images.Theme::set('mytheme') and verify with Theme::get().default + admin), where the admin theme extends default and only overrides the files that actually differ.Where to look first: config/theme.php, then the package's Theme class and facade for the actual runtime API.
Per-request theme selection via middleware
class SetTheme
{
public function handle($request, Closure $next)
{
$theme = $request->user()?->theme
?? ($request->is('admin/*') ? 'admin' : config('theme.default'));
Theme::set($theme);
return $next($request);
}
}
Register it globally or on the relevant route group so it runs before any controller renders a view.
Route-group scoped switching
Route::group(['middleware' => SetAdminTheme::class], function () {
// admin routes
});
Useful when the public site stays on the default theme.
Inheritance for shared layouts
// config/theme.php
'themes' => [
'default' => ['extends' => null],
'admin' => ['extends' => 'default'],
],
Keep base layout partials (header, footer, nav, base css) in the parent theme and let the child theme override only the blade files it changes. This is the primary way to avoid duplicating markup between themes.
Theme-relative asset URLs
Use the package helper instead of the plain asset() helper so the theme prefix is applied correctly:
<link rel="stylesheet" href="{{ Theme::asset('css/app.css') }}">
Custom theme class for setup logic
If a theme needs runtime work (a view composer, shared variables, a menu), subclass the base theme and reference it in config rather than stuffing logic into middleware:
class AdminTheme extends \Yaap\Theme\Theme
{
public function register()
{
view()->share('themeName', 'admin');
}
}
Fallback lookups
When a view may not exist in the child theme, either rely on inheritance or probe with view()->exists() using a path built from Theme::get(). Prefer inheritance — it keeps the view finder paths predictable.
Testing
In feature tests call Theme::set('default') in setUp() or override config(['theme.default' => 'default']) so view resolution is deterministic across test cases. On Laravel 11's slim skeleton this usually means a RefreshDatabase/config-reset style base test case and $this->withoutExceptionHandling() when debugging theme resolution failures.
php artisan config:cache freezes config/theme.php. Switching the active theme at runtime is fine; changing the theme map requires config:clear and a deploy.php artisan view:clear. Laravel will happily serve the previous theme's compiled blade otherwise.AppServiceProvider::boot. On Laravel 11, bootstrap/app.php is the natural place to register global middleware — don't reach for Kernel.php, it's gone.php artisan about plus php artisan package:discover if in doubt).extends is mostly view resolution: parent-theme routes, translations and config are not merged automatically. If you expect parent routes to appear under a child theme, you'll need to wire that yourself.public/. Symlink or copy them at deploy time; don't assume the package serves them through a route.view()->addLocation() for plugin views, those paths stack with the theme's. Use view()->getFinder()->getPaths() when debugging why the wrong index.blade.php loads.dd(Theme::get(), config('theme')) plus dd(view()->getFinder()->getPaths()) tells you what actually resolved at runtime.Theme facade and config/theme.php are the supported surface. Subclass Yaap\Theme\Theme or bind your own implementation in the container if you need deeper control — avoid editing vendor files.composer.json and keep your theme folder layout conventional so you can swap in a custom implementation later if a quirk bites.How can I help you explore Laravel packages today?