devdojo/themes
DevDojo Themes is a lightweight Laravel package for adding switchable themes to your application. Organize theme views, assets, and settings, then activate a theme to change your app’s look without modifying core code.
The package is a Voyager-era theming addon: it gives you a Theme concept backed by database records, lets you register theme directories, and swaps the active theme's views/assets at runtime. Because it is Voyager-first (and small — ~21 stars, last release Aug 2024), treat the source as the source of truth over any blog post.
composer require devdojo/themes
php artisan vendor:publish --provider="DevDojo\Themes\ThemesServiceProvider"
php artisan migrate
/admin and look for the new "Themes" menu item Voyager registers. That admin screen is where you create a theme record, point it at a directory, and mark one active. This is the first place to look — it tells you exactly which fields are expected (name, folder/path, active flag, etc.).resources/views/themes/my-theme/), register it via the admin screen, activate it, and hit a route that resolves a view the theme overrides.First practical use case: create a second theme, put a single welcome.blade.php override in it, and confirm switching the active theme in Voyager changes the rendered page. That proves the view resolution path end-to-end before you commit to a structure.
Directory-per-theme with view overrides. The day-to-day value is letting a theme shadow app views without rewriting controllers. Keep each theme self-contained under a single root (views, assets, optional partials). Controllers keep returning view('welcome'); the theme layer decides which file actually renders. Verify the resolution order in the service provider — most themes packages prepend a theme view path so theme views win, then fall back to resources/views.
Activating a theme at runtime. Rather than only using the Voyager toggle, wire the active theme into a middleware or a service that reads it once per request and calls the package's setter. Pattern:
StartSession and before your view-rendering routes.This is where the package earns its keep versus hand-rolled view()->prependLocation(): you get the admin CRUD for free, but you still own the "which theme for this request" logic.
Asset handling. Theme assets usually live under public/themes/{slug}/ (or are symlinked from the package). Reference them through a helper or asset('themes/'.theme_slug().'/css/app.css') rather than hardcoded paths so switching themes doesn't 404. If the package ships a publish/symlink command, run it as part of deploy.
Voyager integration points. Extend the theme BREAD with custom fields (preview image, author, version) via Voyager's BREAD editor — no code required. If you need theme-scoped settings (colors, logos), add fields to the themes table and read them through the theme model; that keeps config next to the theme instead of in config/.
Compose with layouts. Have each theme supply a layouts/app.blade.php and require app views to @extends('layouts.app') (unqualified). Then a theme only needs to provide the layout plus whatever it wants to override — you don't have to duplicate every view.
Multi-theme testing. In feature tests, set the active theme explicitly (via the model or the package's setter) in setUp() and assert on rendered content, not view paths. Add a thin helper like withTheme('dark') to make tests readable.
composer.json constraints before committing; this is a package to evaluate, not assume.php artisan view:clear (and config:clear after config edits). Add both to your deploy script alongside optimize:clear.View::getFinder()->getPaths() to see the ordering. If the theme path isn't near the front, your app's resources/views wins and the theme looks broken when it's just misordered.@extends/@include names are relative to the finder, not the active theme. Once a theme prepends the finder, an unqualified @extends('layouts.app') resolves in-theme, but an explicit @extends('themes.dark.layouts.app') pins you. Prefer unqualified names so themes stay swappable.migrate.ThemeResolver interface in a service provider and wrap the package's setter, so you can swap the package out later with a one-class change.composer.json and add a short README note on why it's there.How can I help you explore Laravel packages today?