Theme Development
Theme Development Guide
This guide covers how to create a new frontend theme for LaraDashboard. Themes are Laravel modules that provide the public-facing views, layouts, and routing.
What is a Theme?
A theme module provides:
- Livewire page components for rendering frontend pages
- Blade layouts and views for the frontend
- Blade view components (Navbar, Footer, etc.)
- Frontend routes
- Theme-specific settings and hooks
Required: "theme": true in module.json
Theme modules must have "theme": true in their module.json. This is the flag the system uses to identify and list available themes in the admin Theme settings page.
{
"name": "YourTheme",
"alias": "yourtheme",
"title": "Your Theme",
"theme": true,
"category": "theme",
"description": "A custom frontend theme for LaraDashboard.",
"icon": "lucide:palette",
"version": "1.0.0",
"min_laradashboard_required": "1.0.0",
"providers": [
"Modules\\YourTheme\\Providers\\YourThemeServiceProvider"
]
}
Without "theme": true, the module will not appear in the theme selector and cannot be activated as a theme.
Directory Structure
modules/YourTheme/
├── app/
│ ├── Enums/Hooks/
│ │ ├── YourThemeFilterHook.php
│ │ └── YourThemeActionHook.php
│ ├── Livewire/
│ │ ├── Components/
│ │ │ └── PostsListing.php
│ │ └── Pages/
│ │ ├── Home.php
│ │ ├── Page.php
│ │ ├── Posts.php
│ │ ├── SinglePost.php
│ │ ├── Category.php
│ │ ├── Tag.php
│ │ └── Search.php
│ ├── Providers/
│ │ ├── YourThemeServiceProvider.php
│ │ ├── YourThemeLivewireServiceProvider.php
│ │ ├── RouteServiceProvider.php
│ │ └── EventServiceProvider.php
│ └── View/Components/
│ ├── Navbar.php
│ └── Footer.php
├── config/
│ └── config.php
├── resources/
│ ├── assets/
│ │ ├── css/app.css
│ │ └── js/app.js
│ └── views/
│ ├── components/
│ │ ├── navbar.blade.php
│ │ └── footer.blade.php
│ ├── layouts/
│ │ └── app.blade.php
│ └── livewire/
│ ├── components/
│ │ └── posts-listing.blade.php
│ └── pages/
│ ├── home.blade.php
│ ├── page.blade.php
│ ├── posts.blade.php
│ ├── single-post.blade.php
│ ├── category.blade.php
│ ├── tag.blade.php
│ └── search.blade.php
├── routes/
│ ├── web.php
│ └── api.php
└── module.json
Extending Base Components
LaraDashboard provides base Livewire components in app/Livewire/Pages/ that handle all the data loading, query logic, and SEO parameter construction. Your theme only needs to extend these and provide the render() method.
Available Base Classes
| Base Class | Purpose |
|---|---|
BaseHomePage |
Homepage — loads page from settings.homepage_id or slug home |
BaseContentPage |
Generic page — loads by slug |
BaseSinglePost |
Post detail — loads post, related posts, adjacent posts |
BasePostsArchive |
Blog listing — search, filter, sort, paginate |
BaseTaxonomyArchive |
Category/tag listing — loads term and its posts |
BaseSearchPage |
Search — query param search with pagination |
BasePostsListing |
Embeddable listing component for block-based pages |
Example: SinglePost Component
<?php
namespace Modules\YourTheme\Livewire\Pages;
use App\Livewire\Pages\BaseSinglePost;
use Illuminate\View\View;
class SinglePost extends BaseSinglePost
{
public function render(): View
{
return $this->renderWithLayout(
'yourtheme::livewire.pages.single-post',
$this->seo()->forPost($this->post),
[
'relatedPosts' => $this->relatedPosts,
'previousPost' => $this->previousPost,
'nextPost' => $this->nextPost,
]
);
}
}
Example: Category Component
For taxonomy pages, set the $taxonomy property and alias the $term for view compatibility:
<?php
namespace Modules\YourTheme\Livewire\Pages;
use App\Livewire\Pages\BaseTaxonomyArchive;
use App\Models\Term;
use Illuminate\View\View;
class Category extends BaseTaxonomyArchive
{
protected string $taxonomy = 'category';
public ?Term $category = null;
public function mount(string $slug): void
{
parent::mount($slug);
$this->category = $this->term;
}
public function render(): View
{
return $this->renderWithLayout(
'yourtheme::livewire.pages.category',
$this->seo()->forTerm($this->term, 'category')
);
}
}
Core Services
renderWithLayout()
The BaseFrontendPage::renderWithLayout() method:
- Renders your view with given data
- Applies the active theme's layout automatically
- Passes SEO parameters to the layout
FrontendQueryService
Access via $this->query() in any base component:
$this->query()->findHomepage();
$this->query()->findBlogPage();
$this->query()->findPublishedPostBySlug($slug);
$this->query()->findPublishedPageBySlug($slug);
$this->query()->relatedPosts($post, $limit);
$this->query()->adjacentPosts($post);
$this->query()->findTermBySlug($slug, $taxonomy);
$this->query()->publishedPostsQuery();
$this->query()->searchPosts($query);
$this->query()->getCategories();
$this->query()->paginatePosts($query, $perPage);
SeoHelper
Access via $this->seo() in any base component:
$this->seo()->forPost($post);
$this->seo()->forPage($page);
$this->seo()->forTerm($term, 'category');
$this->seo()->forSearch($query);
$this->seo()->forBlogListing($blogPage);
$this->seo()->forHomepage($page);
$this->seo()->forCustom('Title', 'Description', ['ogType' => 'profile']);
View Traits
Use these traits in your Blade view components (Navbar, Footer):
HasSiteIdentity
use App\View\Concerns\HasSiteIdentity;
class Navbar extends Component
{
use HasSiteIdentity;
public function __construct()
{
$this->loadSiteIdentity();
// Provides: $this->siteName, $this->logoLight, $this->logoDark
}
}
HasMenuData
use App\View\Concerns\HasMenuData;
class Navbar extends Component
{
use HasMenuData;
public function __construct()
{
$this->menuItems = $this->loadMenuItems('primary');
$this->footerColumns = $this->loadFooterColumns();
}
}
HasSocialLinks
use App\View\Concerns\HasSocialLinks;
class Footer extends Component
{
use HasSocialLinks;
public function __construct()
{
$this->socialLinks = $this->loadSocialLinks();
}
}
SEO Head Partial
Include the reusable SEO partial in your layout instead of duplicating meta tags:
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
@include('components.seo-head')
{{-- Your theme-specific head content --}}
</head>
Theme Facade
Use Theme facade for theme-aware operations:
use App\Support\Facades\Theme;
Theme::active(); // 'starter26'
Theme::layout(); // 'starter26::layouts.app'
Theme::viewPrefix(); // 'starter26::'
Theme::isActive('starter26'); // true
Theme::setting('posts_per_page'); // 12
Theme::registerDefaults([...]); // Set defaults for null settings
Theme Activation
Themes are activated through the admin Theme settings page (/admin/theme). The active_theme setting determines which theme's routes are loaded and which layout is used.
Theme vs Feature Module
| Aspect | Theme Module (Starter26) | Feature Module (CRM) |
|---|---|---|
| Purpose | Frontend layout/presentation | Admin functionality |
| Category | "theme" in module.json |
"core" or "addon" |
| Routes | Public routes (no /admin/) |
Admin routes (/admin/{module}) |
| Components | Livewire Pages + View Components | Controllers + Livewire Datatables |
| Models | Uses core Post/Term models | Own models (Contact, Deal, etc.) |
| Permissions | None (public pages) | Granular per-action permissions |
| Settings | Theme options (layout, colors) | Feature-specific settings |
| CSS Prefix | Short prefix (e.g., st) |
Short prefix (e.g., crm) |
Theme Configuration
Themes can register default settings via Theme::registerDefaults():
// In your ServiceProvider boot()
use App\Support\Facades\Theme;
Theme::registerDefaults([
'blog_url_prefix' => 'blog',
'posts_per_page' => '12',
'footer_description' => 'Built with LaraDashboard.',
'newsletter_enabled' => true,
]);
Module config file (config/config.php):
return [
'route_prefix' => env('STARTER26_ROUTE_PREFIX', ''),
'theme' => [
'dark_mode' => true,
'posts_per_page' => 12,
'newsletter' => true,
],
'seo' => [
'site_name' => env('APP_NAME', 'My Site'),
'separator' => ' - ',
],
];
Hook Integration
Themes register hooks to modify frontend behavior:
Frontend URL Filter
Control how posts/pages resolve to frontend URLs:
use App\Enums\Hooks\AdminFilterHook;
use App\Support\Facades\Hook;
Hook::addFilter(AdminFilterHook::POST_FRONTEND_URL, function (?string $url, Post $post) {
return match ($post->post_type) {
'page' => url('/' . $post->slug),
'post' => route('starter26.post', $post->slug),
default => $url,
};
}, priority: 10, acceptedArgs: 2);
Frontend Toolbar Items
Add edit buttons to the admin toolbar when viewing the frontend:
Hook::addFilter(AdminFilterHook::FRONTEND_TOOLBAR_ITEMS, function (array $items) {
$primaryMenu = Menu::forLocation('primary');
if ($primaryMenu) {
$items[] = [
'id' => 'edit-navbar',
'label' => __('Edit Navbar'),
'url' => route('admin.menus.builder', $primaryMenu),
'icon' => 'lucide:navigation',
];
}
return $items;
}, priority: 20);
Custom Action Hooks
Define theme-specific hooks for extensibility:
// app/Enums/Hooks/Starter26ActionHook.php
enum Starter26ActionHook: string
{
case POST_VIEWED = 'action.starter26.post_viewed';
case SEARCH_PERFORMED = 'action.starter26.search_performed';
case PAGE_LOADED = 'action.starter26.page_loaded';
}
Tailwind CSS in Themes
Theme modules use CSS prefixing like all modules. See the Tailwind Prefixing Guide for the full setup.
/* modules/starter26/resources/assets/css/app.css */
@import "tailwindcss" prefix(st);
@custom-variant dark (&:is(.dark *));
@source '../../views/**/*.blade.php';
@layer base {
:root, :host {
--st-color-primary: var(--color-primary, #635bff);
}
}
In theme Blade views:
<div class="st:max-w-4xl st:mx-auto st:py-12">
<h1 class="st:text-4xl st:font-bold st:text-gray-900 st:dark:text-white">
{{ $post->title }}
</h1>
</div>
Database Seeders
Themes typically include seeders for initial setup:
database/seeders/
├── Starter26DatabaseSeeder.php ← Master seeder
├── Starter26MenuSeeder.php ← Primary/footer menu items
├── Starter26PageSeeder.php ← Default pages (Home, About, etc.)
└── Starter26SettingsSeeder.php ← Theme default settings
These run during theme activation or installation.
View Components
Theme modules use Blade view components for reusable layout pieces:
// app/View/Components/Navbar.php
namespace Modules\Starter26\View\Components;
use App\View\Concerns\HasSiteIdentity;
use App\View\Concerns\HasMenuData;
use Illuminate\View\Component;
class Navbar extends Component
{
use HasSiteIdentity, HasMenuData;
public function __construct()
{
$this->loadSiteIdentity();
$this->menuItems = $this->loadMenuItems('primary');
}
public function render()
{
return view('starter26::components.navbar');
}
}
Register components in the ServiceProvider:
$this->loadViewComponentsAs('starter26', [
Navbar::class,
Footer::class,
]);
Use in Blade:
<x-starter26-navbar />
<main>{{ $slot }}</main>
<x-starter26-footer />
Reference: Starter26
The starter26 module is the reference implementation. Study its structure for a complete working example of all the patterns described in this guide:
modules/starter26/
├── app/
│ ├── Enums/Hooks/ ← Theme hooks
│ ├── Livewire/Pages/ ← Home, SinglePost, Category, Tag, Page, Search
│ ├── View/Components/ ← Navbar, Footer
│ ├── Http/Controllers/
│ ├── Providers/ ← Service, Livewire, Route, Event
│ └── Services/ModuleService.php ← Hook & menu registration
├── config/config.php ← Theme options
├── database/seeders/ ← Menu, page, settings seeders
├── resources/
│ ├── assets/css/app.css ← Tailwind with prefix(st)
│ ├── assets/js/app.js
│ └── views/
│ ├── index.blade.php ← Main layout wrapper
│ ├── components/navbar.blade.php
│ ├── components/footer.blade.php
│ ├── components/admin-toolbar.blade.php
│ └── livewire/pages/ ← Page views
├── routes/web.php ← Public routes
├── module.json ← "theme": true, category: "theme"
└── vite.config.js