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:

  1. Renders your view with given data
  2. Applies the active theme's layout automatically
  3. 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
/