Unfold CMS — Template Development Guide

Unfold CMS ships with a self-contained, file-based template system built on Laravel Blade. Unlike many CMS platforms that scatter theme logic across plugins, hooks, and database entries, every piece of a template — layouts, views, sections, options, translations, and helpers — lives inside a single directory. Drop it in, activate it, and the CMS reads the rest from your JSON config files.

Why this approach matters:

  • Zero coupling to the core — Templates never modify CMS code. You can swap, update, or delete a template without touching app/, routes/, or the database schema.
  • Admin-configurable without code changesoptions.json generates a full settings UI automatically. Site owners adjust colors, toggle features, and upload logos from the admin panel; no developer needed for day-to-day changes.
  • Safe customization via overrides — Admins can edit any template file through the built-in Template Editor. Edits are stored in a separate _templatename/ overlay directory, so the original files stay untouched and template updates never destroy customizations.
  • Portable — A template is a folder. Zip it, share it, version-control it. Moving a template between sites is a file copy.
  • Shared hosting friendly — Everything is file-based. No build step required in production, no Node.js on the server, no queue workers. The downloaded CMS package includes pre-built assets.

This guide covers everything you need to create, customize, and extend templates.


Table of Contents

  1. Architecture Overview
  2. Template Structure
  3. Configuration Files
  4. Layout System
  5. Pages & Views
  6. Sections System
  7. Components
  8. Blade Directives Reference
  9. Helper Functions
  10. Template Options
  11. Menus & Navigation
  12. Advertising Zones
  13. Localization (i18n)
  14. Dark Mode & Theming
  15. SEO & Structured Data
  16. Shortcodes
  17. Content Blocks
  18. Override System
  19. React Page Overrides
  20. Template Helpers File
  21. Creating a New Template
  22. Best Practices

Architecture Overview

Unfold CMS uses a Blade-based template engine built on top of Laravel. Templates live in resources/views/templates/ and are completely self-contained — each template includes its own layouts, pages, sections, components, configuration, translations, and helpers.

Key concepts:

  • TemplateService — resolves which template to render, manages settings and file overrides
  • SectionService — manages dynamic homepage sections (CRUD, caching, ordering)
  • Override System — admins can customize templates without touching originals via _templatename/ folders
  • Template Options — per-template settings defined in config/options.json, editable from the admin panel
  • Section Config — page sections and page labels defined in config/sections.json

Rendering flow:

  1. A public controller calls template_view('page_name') (e.g., template_view('home'))
  2. TemplateService resolves the full view path: templates.{active_template}.page_name
  3. If an override exists at templates._{active_template}.page_name, that file is used instead
  4. Shared variables ($siteSettings, $templatePath, $currentLocale, $isRtl) are automatically injected
  5. Blade directives, helpers, and settings are available throughout the template

Template Structure

Every template lives in its own directory under resources/views/templates/:

resources/views/templates/
├── default/                    # Template directory
│   ├── template.json           # Template metadata (required)
│   ├── screenshot.png          # Preview image for admin
│   ├── helpers.php             # Template-specific PHP helpers
│   ├── layout.blade.php        # Master layout
│   ├── home.blade.php          # Homepage
│   ├── page.blade.php          # Static pages
│   ├── config/
│   │   ├── options.json        # Template options schema
│   │   └── sections.json       # Section definitions
│   ├── blog/
│   │   ├── index.blade.php     # Blog listing
│   │   ├── show.blade.php      # Single post
│   │   └── category.blade.php  # Category archive
│   ├── components/
│   │   ├── header.blade.php    # Site header
│   │   ├── footer.blade.php    # Site footer
│   │   ├── post-card.blade.php # Blog post card (grid)
│   │   ├── post-card-list.blade.php # Blog post card (list)
│   │   ├── breadcrumbs.blade.php
│   │   ├── pagination.blade.php
│   │   ├── blog-sidebar.blade.php
│   │   ├── share-buttons.blade.php
│   │   ├── comment.blade.php
│   │   ├── comment-form.blade.php
│   │   └── shortcodes/
│   │       ├── contact-form.blade.php
│   │       └── contact-map.blade.php
│   ├── sections/
│   │   ├── about.blade.php
│   │   ├── contact-faq.blade.php
│   │   ├── contact-info.blade.php
│   │   ├── contact-map.blade.php
│   │   ├── cta.blade.php
│   │   ├── faq.blade.php
│   │   ├── features.blade.php
│   │   ├── hero.blade.php
│   │   ├── integrations.blade.php
│   │   ├── stats.blade.php
│   │   ├── tabs.blade.php
│   │   └── testimonials.blade.php
│   └── lang/
│       ├── en.json
│       └── fa.json

Configuration Files

template.json (Required)

The main metadata file. Every template must have this file.

{
    "name": "Default Template",
    "version": "1.0.0",
    "author": "Your Name",
    "description": "A clean, modern template with card-based layouts",
    "screenshot": "screenshot.png",
    "menu_locations": {
        "header": {
            "name": "Header Navigation",
            "description": "Main navigation links in the site header"
        },
        "footer": {
            "name": "Footer Navigation",
            "description": "Footer columns — top-level items become column headers, children become links"
        }
    },
    "zones": {
        "header_leaderboard": {
            "name": "Header Leaderboard",
            "description": "Top of the page leaderboard banner",
            "size": "728x90",
            "type": "banner"
        },
        "sidebar_top": {
            "name": "Sidebar Top",
            "size": "300x250",
            "type": "sidebar"
        }
    }
}

Fields:

Field Required Description
name Yes Human-readable template name
version No Semantic version string
author No Template author name
description No Short description
screenshot No Filename of preview image (relative to template root)
menu_locations No Menu slots the template supports
zones Yes Ad zone definitions (see Advertising Zones)

config/options.json

Defines the settings UI that admins can configure for this template. These settings are rendered automatically in the admin panel under Appearance > Template Settings.

{
    "options": [
        {
            "key": "blog.posts_per_page",
            "type": "integer",
            "label": "Posts Per Page",
            "defaultValue": 12,
            "min": 4,
            "max": 48
        },
        {
            "key": "appearance.default_theme",
            "type": "radio",
            "label": "Default Theme",
            "options": [
                { "value": "system", "label": "System" },
                { "value": "light", "label": "Light" },
                { "value": "dark", "label": "Dark" }
            ],
            "defaultValue": "system",
            "col": 2
        }
    ]
}

Supported field types:

Type Description Extra Properties
text Single-line text input placeholder
textarea Multi-line text input placeholder, rows
integer Number input min, max
boolean Toggle switch children (nested fields shown when true), bordered
select Dropdown options: [{value, label}]
radio Radio buttons options: [{value, label, description}], inline
color Color picker defaultValue (hex)
url URL input placeholder
file File upload file: {accept, maxSize, preview}
slider Range slider slider: {min, max, step, unit}
divider Section heading (no key) label, action: {label, href}

Layout properties:

Property Description
col Number of columns to span (1 or 2)
bordered Whether to show border (default: true)
inline Display radio buttons inline
children Nested fields (shown when parent boolean is true)

config/sections.json

Defines the page sections that admins can populate with content items. The file has two top-level keys: pages (page labels for the admin UI) and sections (the section definitions).

{
    "pages": {
        "homepage": {
            "label": "Homepage",
            "description": "Sections displayed on your site's homepage"
        }
    },
    "sections": {
        "homepage.stats": {
            "name": "Stats",
            "description": "Key numbers and statistics",
            "block_type": "stat",
            "max": 6,
            "page": "homepage",
            "title_label": "Value",
            "body_label": "Label",
            "supports_featured_image": false,
            "fields": []
        },
        "homepage.features": {
            "name": "Features",
            "description": "Feature highlights on the homepage",
            "block_type": "feature",
            "max": 12,
            "page": "homepage",
            "title_label": "Heading",
            "body_label": "Description",
            "supports_featured_image": false,
            "fields": [
                {
                    "key": "icon",
                    "type": "icon_picker",
                    "label": "Icon",
                    "placeholder": "Search icons..."
                },
                {
                    "key": "link_url",
                    "type": "url",
                    "label": "Link URL",
                    "placeholder": "/features/security"
                }
            ]
        },
        "homepage.testimonials": {
            "name": "Testimonials",
            "description": "Customer testimonials",
            "block_type": "testimonial",
            "max": 12,
            "page": "homepage",
            "title_label": "Quote",
            "body_label": "Full Testimonial",
            "supports_featured_image": true,
            "fields": [
                {
                    "key": "author_name",
                    "type": "text",
                    "label": "Author Name",
                    "validation": { "required": true }
                },
                {
                    "key": "author_role",
                    "type": "text",
                    "label": "Author Role"
                },
                {
                    "key": "rating",
                    "type": "select",
                    "label": "Rating",
                    "options": [
                        { "value": "5", "label": "5 Stars" },
                        { "value": "4", "label": "4 Stars" }
                    ]
                }
            ]
        }
    }
}

Page label fields (pages key):

Field Required Description
label Yes Human-readable page name shown in admin
description No Help text shown below the page heading in admin

Page labels live in the template config so templates are fully portable — copy the folder and the admin UI displays correct page headings automatically.

Section config fields (sections key):

Field Required Description
name Yes Display name in admin
description No Help text for admins
block_type Yes Internal block type identifier
max Yes Maximum number of items allowed
page Yes Which page this section belongs to (must match a key in pages)
title_label No Custom label for the title field (default: "Title")
body_label No Custom label for the body field (default: "Body")
supports_featured_image No Whether items can have images (default: false)
fields Yes Array of extra fields (stored in extra_attributes)

Section field types:

Type Description
text Text input
url URL input
color Color picker
icon_picker Lucide icon search/select
select Dropdown with options

Layout System

The master layout (layout.blade.php) provides the HTML skeleton. All pages extend it.

Key structure

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}"
      dir="{{ ($isRtl ?? false) ? 'rtl' : 'ltr' }}"
      x-data="themeToggle"
      data-default-theme="{{ template_option('appearance.default_theme', 'system') }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="csrf-token" content="{{ csrf_token() }}">

    {{-- SEO Meta Tags (auto-generated) --}}
    {!! seo() !!}

    {{-- Additional head tags from child views --}}
    @stack('head')

    {{-- Favicon --}}
    <link rel="icon" href="{{ $siteSettings['favicon'] ?? '/favicon.svg' }}">

    {{-- Fonts (configurable via settings) --}}
    <link href="https://fonts.bunny.net/css?family={{ setting_get('template.font_url', 'Inter:400,500,600,700') }}" rel="stylesheet" />

    {{-- Vite Assets --}}
    @vite(['resources/js/public.ts'])

    {{-- CSS Custom Properties --}}
    <style>
        :root {
            --color-primary: {{ setting_get('template.primary_color', '#3b82f6') }};
            --color-secondary: {{ setting_get('template.secondary_color', '#64748b') }};
            --font-family: '{{ setting_get('template.font_name', 'Inter') }}', system-ui, sans-serif;
        }
    </style>

    @stack('styles')
    @codeSnippets('head')
</head>
<body>
    @codeSnippets('body_start')

    {{-- Maintenance Banner --}}
    @maintenanceBanner

    {{-- Announcements: Top Bar --}}
    {!! \App\Services\AnnouncementService::render('top_bar') !!}

    {{-- Header --}}
    @include(($templatePath ?? 'templates.default') . '.components.header')

    {{-- Header Ad Zone --}}
    @hasAdZone('header_leaderboard')
        <div class="bg-gray-100 py-2 text-center">
            @ad_zone('header_leaderboard')
        </div>
    @endhasAdZone

    {{-- Main Content (filled by child views) --}}
    <main>
        @yield('content')
    </main>

    {{-- Footer --}}
    @include(($templatePath ?? 'templates.default') . '.components.footer')

    {{-- Announcements: Modal & Slide-in --}}
    {!! \App\Services\AnnouncementService::render('modal') !!}
    {!! \App\Services\AnnouncementService::render('slide_in') !!}

    {{-- JSON-LD Schema (pushed from child views) --}}
    @stack('schema')
    @stack('scripts')
    @codeSnippets('body_end')
</body>
</html>

Stacks available in child views

Stack Location Usage
head Inside <head> Pagination rel links, extra meta tags
styles Inside <head> Additional CSS
scripts Before </body> Additional JavaScript
schema Before </body> JSON-LD structured data

Shared variables

These are automatically available in all template views:

Variable Type Description
$siteSettings array All site settings (logo, favicon, name, etc.)
$templatePath string Current template view prefix (e.g., templates.default)
$currentLocale string Current locale (e.g., en, fa)
$isRtl bool Whether the current locale is RTL

Pages & Views

Homepage (home.blade.php)

The homepage extends the layout and renders all enabled sections. The hero section is a settings-only section managed under Appearance > Sections.

@extends(($templatePath ?? 'templates.default') . '.layout')

@section('content')
{{-- Hero Section (settings-only section) --}}
@sectionEnabled('homepage.hero')
    @include('templates.default.sections.hero')
@endsectionEnabled

{{-- Template Sections --}}
@sectionEnabled('homepage.features')
    @include('templates.default.sections.features')
@endsectionEnabled

@sectionEnabled('homepage.testimonials')
    @include('templates.default.sections.testimonials')
@endsectionEnabled

{{-- ... more sections ... --}}

{{-- Latest Posts --}}
@sectionEnabled('homepage.latest_posts')
@if($latestPosts->isNotEmpty())
    <section>
        @foreach($latestPosts as $post)
            <article>
                <h3><a href="{{ $post->url }}">{{ $post->title }}</a></h3>
                <span>{{ $post->formatted_posted_at }}</span>
                <span>{{ $post->reading_time }} {{ __('min read') }}</span>
            </article>
        @endforeach
    </section>
@endif
@endsectionEnabled
@endsection

@push('schema')
@jsonld(schema_organization())
@jsonld(schema_website())
@endpush

Available data in home.blade.php:

Variable Type Description
$latestPosts Collection<Post> Latest 3 published posts
$categories Collection<Category> Published categories (up to 8)
$seoData array SEO metadata

Blog Listing (blog/index.blade.php)

Available data:

Variable Type Description
$posts LengthAwarePaginator Paginated posts
$featuredPosts Collection<Post> Featured/pinned posts
$categories Collection<Category> Category tree
$recentPosts Collection<Post> Latest 5 posts (sidebar)
$paginationMeta array ['prev' => url, 'next' => url] for rel links

Single Post (blog/show.blade.php)

Available data:

Variable Type Description
$post Post The post (with categories, author, media)
$relatedPosts Collection<Post> Posts in same categories
$comments array Threaded comments tree
$commentSettings array Comment config (max_depth, require_approval, etc.)
$isPreview bool Whether this is an admin preview

Static Page (page.blade.php)

Available data:

Variable Type Description
$page Post The page (content_type = page)

Category Archive (blog/category.blade.php)

Available data:

Variable Type Description
$category Category The current category
$posts LengthAwarePaginator Paginated posts in category
$categories Collection<Category> All categories (sidebar)
$recentPosts Collection<Post> Latest 5 posts (sidebar)

Sections System

Sections are dynamic content blocks that admins can populate, reorder, enable/disable, and customize from the admin panel. While the default template uses sections on the homepage, the system supports sections on any page.

How sections work

  1. You define sections in config/sections.json (location key, fields, limits)
  2. You create a Blade file in sections/ that renders the content
  3. Admins manage content through the admin panel (Appearance > Sections)

There are two types of sections:

  • Item-based: Items are stored as Post records with content_type = block. Extra fields use the extra_attributes JSON column.
  • Settings-only: Data is stored as key-value settings. Ideal for single-instance content like Hero, About, or CTA banners.

Writing a section template

Item-based sections

Item-based sections loop through a collection of items:

{{-- sections/features.blade.php --}}
@php $items = section_items('homepage.features'); @endphp
@if($items->isNotEmpty())
<section class="py-20">
    <div class="container mx-auto px-4">
        <h2>{{ __('Features') }}</h2>

        <div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
            @foreach($items as $item)
                <div>
                    {{-- Icon (from extra_attributes) --}}
                    @if($item->extra_attributes['icon'] ?? null)
                        {!! lucide_icon($item->extra_attributes['icon'], 'h-6 w-6') !!}
                    @endif

                    {{-- Title --}}
                    <h3>{{ $item->title }}</h3>

                    {{-- Body --}}
                    @if($item->body)
                        <p>{{ $item->body }}</p>
                    @endif

                    {{-- Custom field: link --}}
                    @if($item->extra_attributes['link_url'] ?? null)
                        <a href="{{ $item->extra_attributes['link_url'] }}">
                            {{ __('Learn more') }}
                        </a>
                    @endif
                </div>
            @endforeach
        </div>
    </div>
</section>
@endif

Settings-only sections

Settings-only sections read key-value settings instead of items:

{{-- sections/about.blade.php --}}
<section class="py-20">
    <div class="container mx-auto px-4">
        <h2>{{ section_setting('homepage.about', 'heading') }}</h2>
        <p>{{ section_setting('homepage.about', 'subtitle') }}</p>
        <p>{{ section_setting('homepage.about', 'description') }}</p>

        @if(section_setting('homepage.about', 'button_text'))
            <a href="{{ section_setting('homepage.about', 'button_url', '#') }}">
                {{ section_setting('homepage.about', 'button_text') }}
            </a>
        @endif
    </div>
</section>

Section item properties

Each $item is a Post model with:

Property Type Description
$item->title string The item's title
$item->body string The item's body/description
$item->short_description string|null Optional short description
$item->extra_attributes array Custom fields defined in sections.json
$item->is_published bool Publication status
$item->getFirstMediaUrl('featured-image') string Featured image URL
$item->getFirstMediaUrl('featured-image', 'thumbnail') string Thumbnail variant
$item->getFirstMediaUrl('featured-image', 'medium') string Medium variant
$item->getFirstMediaUrl('featured-image', 'large') string Large variant

Available sections in default template

Item-Based Sections

Location Key Name Max Items Has Image Custom Fields
homepage.stats Stats 6 No
homepage.features Features 12 No icon, link_url
homepage.testimonials Testimonials 12 Yes author_name, author_role, author_company, rating
homepage.faq FAQ 20 No
homepage.integrations Integrations 16 Yes website_url
homepage.tabs Platform 8 Yes icon
contact.contact_faq Contact FAQ 20 No

Settings-Only Sections

Location Key Name Settings
homepage.hero Hero title, subtitle, buttons, background image, overlay
homepage.about About heading, subtitle, description, image, button_text, button_url
homepage.cta Call to Action heading, subtitle, description, button_text, button_url, background_color
contact.contact_info Contact Info heading, subtitle, description, email, phone, address
contact.contact_map Map heading, theme, latitude, longitude

Components

Components are reusable Blade partials included via @include. Place them in components/.

Including components

{{-- Include with template path for override support --}}
@include(($templatePath ?? 'templates.default') . '.components.header')

{{-- Include with direct path (no override support) --}}
@include('templates.default.components.post-card', ['post' => $post])

{{-- Include with data --}}
@include('templates.default.components.breadcrumbs', [
    'items' => [
        ['label' => __('Blog'), 'url' => route('blog.index')],
        ['label' => $post->title],
    ]
])

Key components in the default template

Component Description Expected Props
header Site header with navigation, search, theme toggle, auth
footer Site footer with menu columns, social links, copyright
post-card Blog post card for grid layout $post
post-card-list Blog post card for list layout $post
breadcrumbs Breadcrumb navigation $items (array of [label, url?])
pagination Pagination links Passed via $posts->links(...)
blog-sidebar Blog sidebar (categories, recent posts, newsletter)
share-buttons Social share buttons on posts $post
comment Single comment with nesting $comment, $depth, $maxDepth
comment-form Comment submission form $post
shortcodes/contact-form Contact form shortcode
shortcodes/contact-map OpenStreetMap embed shortcode $lat, $lng, $zoom, $height

Blade Directives Reference

Unfold CMS provides 30+ custom Blade directives. These are available in all template files.

Site Identity

Directive Output Example
@siteName Site name from settings <span>@siteName</span>
@siteTagline Site tagline <p>@siteTagline</p>
@siteLogo('classes') Logo <img> with classes, or site name text if no logo @siteLogo('h-8 w-auto')
@siteLogoUrl Just the logo URL <img src="@siteLogoUrl" alt="Logo">
@siteFavicon Favicon URL <link rel="icon" href="@siteFavicon">
@supportEmail Support email address <a href="mailto:@supportEmail">Contact</a>
@copyright Full copyright line with year and site name <footer>@copyright</footer>

Content & Blocks

Directive Description Example
@block('slug') Render a content block by slug @block('homepage-hero')
@hasBlock('slug') ... @endhasBlock Conditional: check if block exists @hasBlock('sidebar') <div>@block('sidebar')</div> @endhasBlock

Content blocks support conditional rendering:

  • @auth ... @endauth — Show only to logged-in users
  • @guest ... @endguest — Show only to guests
  • @admin ... @endadmin — Show only to admins
  • @role('role') ... @endrole — Show only to specific roles

Content blocks support user interpolation: {{ user.name }}, {{ user.email }}, {{ user.first_name }}, etc.

Directive Description Example
@menu('slug') Render a menu by slug @menu('main-navigation')
@menu(id) Render a menu by ID @menu(1)

Advertising

Directive Description Example
@ad_zone('key') Render an ad zone @ad_zone('sidebar-banner')
@hasAdZone('key') ... @endhasAdZone Conditional: check if ad zone is active @hasAdZone('header') @ad_zone('header') @endhasAdZone

Settings & Template Options

Directive Description Example
@setting('key', 'default') Output site setting (escaped) @setting('app.tagline', 'Welcome')
@settingRaw('key', 'default') Output site setting (unescaped, for HTML) @settingRaw('seo.meta_scripts')
@hasSetting('key') ... @endhasSetting Conditional: setting exists and is truthy @hasSetting('app.logo') ... @endhasSetting
@option('key') Output template option (escaped) @option('footer.copyright_text')
@optionRaw('key') Output template option (unescaped) @optionRaw('footer.custom_html')
@hasOption('key') ... @endhasOption Conditional: template option is truthy @hasOption('sidebar.enabled') ... @endhasOption
@templateOption('key', 'default') Template option with default (escaped) @templateOption('sidebar.enabled', true)
@templateOptionRaw('key', 'default') Template option with default (unescaped) @templateOptionRaw('custom.css')

Sections

Directive Description Example
@sectionEnabled('location') Check if section is enabled @sectionEnabled('homepage.features')
@hasSectionItems('location') Check if section has published items @hasSectionItems('homepage.faq')

Shortcodes

Directive Description Example
@shortcodes($content) Process shortcodes in content @shortcodes($page->body)

Authentication & Authorization

Directive Description Example
@authenticated ... @endauthenticated Content for logged-in users @authenticated <a href="/dashboard">Dashboard</a> @endauthenticated
@authId Current user ID <input type="hidden" value="@authId">
@admin ... @endadmin Content for admin users @admin <a href="/admin">Admin</a> @endadmin
@superAdmin ... @endsuperAdmin Content for super admins @superAdmin <a href="/admin/settings">Settings</a> @endsuperAdmin
@hasRole('role') ... @endhasRole Content for specific role @hasRole('editor') <button>Edit</button> @endhasRole
@hasAnyRole(['r1', 'r2']) ... @endhasAnyRole Content for any of roles @hasAnyRole(['admin', 'editor']) ... @endhasAnyRole
@hasAllRoles(['r1', 'r2']) ... @endhasAllRoles Content for all roles @hasAllRoles(['admin', 'mod']) ... @endhasAllRoles
@canUser('perm') ... @endcanUser Content for specific permission @canUser('edit-posts') ... @endcanUser

Marketing & Announcements

Directive Description Example
@announcement('position') Render announcement for position @announcement('top_bar')
@announcements Render all active announcements @announcements
@codeSnippets('position') Custom code for position (head, body_start, body_end) @codeSnippets('head')
@maintenanceBanner Maintenance mode banner @maintenanceBanner

Environment

Directive Description Example
@localhost ... @endlocalhost Content only on localhost @localhost <div>Debug</div> @endlocalhost
@production ... @endproduction Content only in production @production <!-- Analytics --> @endproduction

Action Context

Directive Description Example
@editing ... @endediting Content when editing @editing <button>Update</button> @endediting
@creating ... @endcreating Content when creating @creating <button>Create</button> @endcreating

Helper Functions

These PHP helpers are available globally in all template files.

Template & View helpers

// Resolve the full view path for the active template
template_view('home');        // → 'templates.default.home'
template_view('blog.index');  // → 'templates.default.blog.index'

// Get a template option value from the active template's options.json
template_option('blog.posts_per_page', 12);
template_option('sidebar.enabled', true);

Section helpers

// Check if a section is enabled
section_enabled('homepage.features');  // → true/false

// Get section items for a location (cached)
$items = section_items('homepage.features');

// Get a section-specific setting
section_setting('homepage.hero', 'background_color', '#ffffff');
section_setting('homepage.cta', 'button_text', 'Get Started');

Icon helpers

// Render a Lucide icon as inline SVG
{!! lucide_icon('shield', 'h-6 w-6') !!}
{!! lucide_icon('arrow-right', 'h-4 w-4 text-brand') !!}

Settings helpers

// Get a site setting
setting_get('app.name', 'My Site');
setting_get('social.twitter');

// Check if a setting exists
setting_has('comments.enabled');

// Set a setting (use sparingly in templates)
setting_set('key', 'value', 'string');
// Get menu items for a location (cached collection)
$headerItems = menu_items('header');
$footerItems = menu_items('footer');

// Each item has: ->label, ->resolved_url, ->target, ->allChildren
@foreach(menu_items('header') as $item)
    <a href="{{ $item->resolved_url }}" target="{{ $item->target }}">
        {{ $item->label }}
    </a>
    @foreach($item->allChildren as $child)
        <a href="{{ $child->resolved_url }}">{{ $child->label }}</a>
    @endforeach
@endforeach

// Render a full menu by slug or ID (uses the @menu Blade directive)
@menu('footer-navigation')
@menu('main-navigation')

Auth helpers

authCheck();   // Is authenticated?
authUser();    // Get current user
authId();      // Get current user ID
isAdmin();     // Has admin role?
isSuperAdmin(); // Has super_admin role?
hasRole('editor');
canUser('edit-posts');

Formatting helpers

// Locale-aware date formatting (uses Jalali for Persian locale)
format_date($date, 'Y/m/d');
format_datetime($date, 'Y/m/d', 'H:i');
format_duration(3600, short: true); // "1h"

// Format bytes as human-readable
format_bytes(1048576); // "1 MB"

// Truncate text with word boundary
blurb($text, 150); // "First 150 chars..."

JSON-LD Schema helpers

// Organization schema (for homepage)
schema_organization();

// Website schema with SearchAction
schema_website();

// Article schema for blog posts
schema_article($post);

// WebPage schema for static pages
schema_webpage($page);

// Breadcrumb schema
schema_breadcrumb_list([
    ['name' => 'Home', 'url' => url('/')],
    ['name' => 'Blog', 'url' => route('blog.index')],
    ['name' => $post->title, 'url' => url()->current()],
]);

Environment helpers

is_localhost();    // Running on localhost?
is_production();   // Running in production?
is_demo_mode();    // Demo mode enabled?

Template Options

Template options are the primary way for admins to customize your template's appearance and behavior.

How it works

  1. Define options in config/options.json
  2. Values are stored in the database as template.{name}.{key}
  3. Read values in templates with template_option() or the @option directive
  4. Options are automatically shown in Admin > Appearance > Template Settings

Reading options in templates

{{-- Using the @option directive (escaped) --}}
<h1>@option('footer.copyright_text')</h1>

{{-- Using @hasOption for conditional rendering --}}
@hasOption('sidebar.enabled')
    @include('templates.default.components.blog-sidebar')
@endhasOption

{{-- Using the PHP helper (for logic) --}}
@php
    $postsPerPage = template_option('blog.posts_per_page', 12);
    $sidebarEnabled = template_option('sidebar.enabled', true);
@endphp

{{-- Combining conditionals --}}
@if(template_option('sidebar.enabled', true) && template_option('sidebar.position', 'right') === 'left')
    @include('templates.default.components.blog-sidebar')
@endif

Menus are managed by admins and assigned to template "locations" defined in template.json.

Defining menu locations

In template.json:

{
    "menu_locations": {
        "header": {
            "name": "Header Navigation",
            "description": "Main navigation links"
        },
        "footer": {
            "name": "Footer Navigation",
            "description": "Footer link columns"
        }
    }
}

Rendering menus

{{-- Simple menu rendering --}}
@foreach(menu_items('header') as $item)
    @if($item->allChildren->count() > 0)
        {{-- Item with dropdown --}}
        <div x-data="{ open: false }">
            <button @click="open = !open">{{ $item->label }}</button>
            <div x-show="open">
                @foreach($item->allChildren as $child)
                    <a href="{{ $child->resolved_url }}" target="{{ $child->target }}">
                        {{ $child->label }}
                    </a>
                @endforeach
            </div>
        </div>
    @else
        <a href="{{ $item->resolved_url }}" target="{{ $item->target }}">
            {{ $item->label }}
        </a>
    @endif
@endforeach

The default template uses a pattern where top-level menu items become column headings and their children become the links:

@php
    $footerItems = menu_items('footer');
    $columns = $footerItems->filter(fn($item) => $item->allChildren->isNotEmpty());
    $standalone = $footerItems->filter(fn($item) => $item->allChildren->isEmpty());
@endphp

@foreach($columns as $column)
    <div>
        <h3>{{ $column->label }}</h3>
        <ul>
            @foreach($column->allChildren as $link)
                <li><a href="{{ $link->resolved_url }}">{{ $link->label }}</a></li>
            @endforeach
        </ul>
    </div>
@endforeach
Property Type Description
->label string Display text
->resolved_url string The URL (resolved from type: page/post/custom)
->target string Link target (_self, _blank)
->allChildren Collection<MenuItem> Child items
->is_active bool Whether the item is active
->sort_order int Sort position

Advertising Zones

Ad zones are defined in template.json and rendered with Blade directives.

Defining zones

{
    "zones": {
        "header_leaderboard": {
            "name": "Header Leaderboard",
            "description": "Banner at the top of every page",
            "size": "728x90",
            "type": "banner"
        },
        "sidebar_top": {
            "name": "Sidebar Top",
            "size": "300x250",
            "type": "sidebar"
        },
        "article_inline": {
            "name": "Article Inline",
            "description": "Inside blog posts, below content",
            "size": "728x90",
            "type": "banner"
        },
        "popup_overlay": {
            "name": "Popup Overlay",
            "size": "custom",
            "type": "popup"
        }
    }
}

Zone types: banner, sidebar, popup

Rendering zones in templates

Always wrap ad zones in @hasAdZone to avoid rendering empty containers:

@hasAdZone('sidebar_top')
    <div class="my-4">
        @ad_zone('sidebar_top')
    </div>
@endhasAdZone

Zones are automatically synced to the database when the admin interface loads. Admins can then create ads and assign them to zones.


Localization (i18n)

Templates support full localization via Laravel's __() translation helper and per-template JSON language files.

Translation files

Place language files in lang/:

templates/default/lang/
├── en.json
└── fa.json

Format

{
    "Featured Posts": "Featured Posts",
    "Latest Posts": "Latest Posts",
    "min read": "min read",
    "No posts yet": "No posts yet",
    "Search": "Search",
    "Login": "Login",
    "Sign Up": "Sign Up"
}

Using translations

{{-- Simple translation --}}
<h2>{{ __('Latest Posts') }}</h2>

{{-- With parameters --}}
<span>{{ $post->reading_time }} {{ __('min read') }}</span>

{{-- Pluralization --}}
{{ trans_choice(':count Comment|:count Comments', count($comments), ['count' => count($comments)]) }}

RTL support

The layout automatically sets dir="rtl" based on the active locale. Use logical CSS properties for RTL compatibility:

{{-- Use start/end instead of left/right --}}
<div class="ms-2 me-4 ps-3 pe-5 text-start">

{{-- RTL-aware rotations --}}
<span class="rtl:rotate-180">&rarr;</span>

{{-- RTL-aware transforms --}}
<span class="group-hover:translate-x-1 rtl:group-hover:-translate-x-1">&rarr;</span>

Dark Mode & Theming

CSS custom properties

The template system uses CSS custom properties for theming, set in the layout:

:root {
    --color-primary: #3b82f6;   /* Configurable via admin */
    --color-secondary: #64748b; /* Configurable via admin */
    --font-family: 'Inter', system-ui, sans-serif;
}

Brand color utilities

The layout defines utility classes based on --color-primary:

Class Description
.text-brand Primary color text
.bg-brand Primary color background
.bg-brand-light 10% primary on white
.bg-brand-dark 90% primary + 10% black
.border-brand Primary color border
.hover:text-brand Hover primary text
.hover:bg-brand Hover primary background

Dark mode

Uses Tailwind's dark: variant with a class-based strategy. The theme is toggled via Alpine.js:

{{-- Light/dark text --}}
<h1 class="text-gray-900 dark:text-white">Title</h1>

{{-- Light/dark backgrounds --}}
<section class="bg-white dark:bg-gray-800">

{{-- Light/dark borders --}}
<div class="border-gray-200 dark:border-gray-700">

The default theme mode is configurable via appearance.default_theme option (system, light, or dark).


SEO & Structured Data

Meta tags

SEO meta tags are automatically generated via the ralphjsmit/laravel-seo package. The layout includes:

{!! seo() !!}

Controllers set SEO data before rendering:

// In controllers
seo()->for($post);  // Auto-generates title, description, OG tags

JSON-LD schemas

Use the @push('schema') stack to add structured data:

{{-- Homepage --}}
@push('schema')
<script type="application/ld+json">
{!! json_encode(schema_organization(), JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) !!}
</script>
<script type="application/ld+json">
{!! json_encode(schema_website(), JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) !!}
</script>
@endpush

{{-- Blog post --}}
@push('schema')
<script type="application/ld+json">
{!! json_encode(schema_article($post), JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) !!}
</script>
@endpush

{{-- Static page --}}
@push('schema')
<script type="application/ld+json">
{!! json_encode(schema_webpage($page), JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) !!}
</script>
<script type="application/ld+json">
{!! json_encode(schema_breadcrumb_list([
    ['name' => __('Home'), 'url' => setting_get('app.url')],
    ['name' => $page->title, 'url' => url()->current()],
]), JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) !!}
</script>
@endpush

Pagination SEO

Blog listing pages push rel="prev" and rel="next" links:

@push('head')
@if(isset($paginationMeta))
    @if($paginationMeta['prev'])
        <link rel="prev" href="{{ $paginationMeta['prev'] }}">
    @endif
    @if($paginationMeta['next'])
        <link rel="next" href="{{ $paginationMeta['next'] }}">
    @endif
@endif
@endpush

Shortcodes

Shortcodes allow admins to embed interactive components within page content.

Built-in shortcodes

Shortcode Description Parameters
[contact-form] Contact form with file upload title
[contact-map] OpenStreetMap embed lat, lng, zoom, height

Using shortcodes

In page body content (written by admins in the editor):

[contact-form title="Get in Touch"]

[contact-map lat="35.6892" lng="51.3890" zoom="14" height="400"]

Rendering shortcodes in templates

Use the @shortcodes directive to process page content:

<div class="prose">
    @shortcodes($page->body)
</div>

Creating a custom shortcode

Shortcodes are registered in SettingsServiceProvider. Each shortcode has:

  • A regex pattern to match in content
  • A Blade partial to render the output
  • A partial located in components/shortcodes/

Content Blocks

Content blocks are reusable content snippets managed from the admin panel and rendered using the @block directive.

{{-- Render a block by slug --}}
@block('homepage-banner')

{{-- Check if a block exists before rendering --}}
@hasBlock('sidebar-widget')
    <div class="widget">
        @block('sidebar-widget')
    </div>
@endhasBlock

Blocks support conditional rendering and user field interpolation (see Blade Directives).


Override System

The override system lets admins customize template files without modifying the originals. This is crucial for surviving template updates.

How it works

  • Original files: templates/default/file.blade.php
  • Override files: templates/_default/file.blade.php (underscore prefix)
  • When rendering, the override is checked first, then the original

Template editor

Admins can edit template files through the built-in Template Editor at /admin/template-editor. The editor:

  1. Shows a file tree of all editable files
  2. Highlights overridden files with a badge
  3. Saves changes to the override directory (_default/)
  4. Allows reverting overrides to restore the original
  5. Shows a diff view comparing original vs. override
  6. Lists all available Blade directives as a reference sidebar

Warning: Incorrect changes to Blade templates can break your site's frontend. Always verify changes after saving, and use the revert feature if something goes wrong.

Editable file types

blade.php, php, json, css, js, html, md, txt, xml

Override resolution in code

// TemplateService resolves views with override support
$view = template_view('home');
// If _default/home.blade.php exists → 'templates._default.home'
// Otherwise → 'templates.default.home'

Template helpers override

The helpers.php file also supports overrides:

  1. Override helpers at _default/helpers.php load first
  2. Original helpers at default/helpers.php load second
  3. Because functions use function_exists() guards, override functions take precedence

React Page Overrides

Templates can provide custom React/Inertia pages that override core CMS pages (e.g., auth pages). This makes templates fully portable — when you copy a template to a new CMS installation, custom page designs come with it.

How it works

The system has two parts: a backend PageResolver and a frontend page resolver.

BackendPageResolver checks if the active template provides a .tsx file for the requested page. If found, it returns the template's page path; otherwise it falls back to the core page.

// app/Services/PageResolver.php
$pageResolver = app(PageResolver::class);
$pageResolver->resolve('auth/login/index');
// → 'templates/{template}/pages/auth/login/index' (if override exists)
// → 'auth/login/index' (fallback to core)

Frontendapp.tsx and ssr.tsx use import.meta.glob to collect both core and template pages, then the shared resolve-page.ts utility picks the right component:

// app.tsx / ssr.tsx
const corePages = import.meta.glob('./pages/**/*.tsx');
const templatePages = import.meta.glob('./templates/*/pages/**/*.tsx');

resolve: (name) => resolvePageComponent(name, corePages, templatePages),

Detection is automatic

No configuration needed. PageResolver uses File::exists() to check for template page files at runtime. If the file exists, it's used. If not, the core page renders.

Directory structure

Template pages live under resources/js/templates/{name}/pages/, mirroring the core pages/ structure:

resources/js/templates/{template}/
├── layouts/
│   └── auth/
│       ├── auth-layout.tsx         # Layout dispatcher
│       ├── auth-split-layout.tsx   # Split layout variant
│       ├── auth-card-layout.tsx    # Card layout variant
│       └── auth-simple-layout.tsx  # Simple layout variant
└── pages/
    └── auth/
        ├── login/index.tsx
        ├── sign-up/index.tsx
        ├── forgot-password/index.tsx
        ├── reset-password/index.tsx
        ├── verify-email/index.tsx
        ├── confirm-password/index.tsx
        ├── two-factor-challenge/index.tsx
        └── otp/index.tsx

Creating a page override

  1. Create the file at resources/js/templates/{template}/pages/{page-path}.tsx (or {page-path}/index.tsx)
  2. The component receives the same Inertia props as the core page
  3. Import core UI components via @/components/ui/* — they work everywhere
  4. Import template-specific layouts via relative paths (not @/)
// resources/js/templates/{template}/pages/auth/login/index.tsx
import AuthLayout from '../../../layouts/auth/auth-layout';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';

export default function Login({ status, canResetPassword }: { ... }) {
    return (
        <AuthLayout title="Sign in" description="Welcome back">
            {/* Your custom login form */}
        </AuthLayout>
    );
}

Shared Inertia prop

The active template name is shared as an Inertia prop via HandleInertiaRequests:

// Available on every page as usePage().props.activeTemplate
'activeTemplate' => app(TemplateService::class)->getActiveTemplate(),

What can be overridden

Any page that uses PageResolver::resolve() can be overridden. Currently this includes all public auth pages:

  • auth/login/index
  • auth/sign-up/index
  • auth/forgot-password/index
  • auth/reset-password/index
  • auth/verify-email/index
  • auth/confirm-password/index
  • auth/two-factor-challenge/index
  • auth/otp/index

Admin pages (admin/auth/*) are not overridable — they always use core pages.

Extending to other pages

To make any other page overridable, use PageResolver in the controller:

$pageResolver = app(PageResolver::class);
return Inertia::render($pageResolver->resolve('some/page/index'), $data);

Template Helpers File

Each template can include a helpers.php file with custom PHP functions used across its views.

Structure

<?php
// templates/default/helpers.php

if (!function_exists('hero_overlay_style')) {
    function hero_overlay_style(): string
    {
        $color = section_setting('homepage.hero', 'overlay_color', '#000000');
        // ... build CSS style string
        return "background: rgba(...)";
    }
}

if (!function_exists('footer_social_links')) {
    function footer_social_links(): array
    {
        return array_filter([
            'twitter' => template_option('footer.social.twitter', true)
                ? setting_get('social.twitter') : null,
            // ...
        ]);
    }
}

Rules

  • Always wrap functions in if (!function_exists('...')) guards
  • This enables the override system — override helpers define functions first
  • Keep functions focused and template-specific
  • Use template_option() and setting_get() for configuration values

Creating a New Template

Step-by-step

  1. Create the directory structure:
resources/views/templates/my-template/
├── template.json           # Required
├── screenshot.png          # Recommended
├── helpers.php             # Optional
├── layout.blade.php        # Required
├── home.blade.php          # Required
├── page.blade.php          # Required
├── config/
│   ├── options.json        # Recommended
│   └── sections.json       # Optional (if you want page sections)
├── blog/
│   ├── index.blade.php     # Required
│   ├── show.blade.php      # Required
│   └── category.blade.php  # Required
├── components/
│   ├── header.blade.php
│   ├── footer.blade.php
│   └── ...
├── sections/
│   └── ...                 # One file per section in sections.json
└── lang/
    └── en.json             # Required
  1. Create template.json with metadata, menu locations, and ad zones

  2. Create layout.blade.php with the HTML skeleton, including:

    • {!! seo() !!} for SEO meta tags
    • @vite(...) for assets
    • @yield('content') for page content
    • @stack('head'), @stack('styles'), @stack('scripts'), @stack('schema')
    • @codeSnippets('head'), @codeSnippets('body_start'), @codeSnippets('body_end')
  3. Create page views (home.blade.php, page.blade.php, blog/*.blade.php) that extend the layout

  4. Define options in config/options.json for admin customization

  5. Define sections in config/sections.json and create matching section templates

  6. Add translations in lang/en.json (and other languages)

  7. Add seed data in database/seeders/SectionSeeder.php for your sections

  8. Activate the template from Admin > Appearance > Templates

Minimal template

At minimum, a template needs:

my-template/
├── template.json
├── layout.blade.php
├── home.blade.php
├── page.blade.php
├── blog/
│   ├── index.blade.php
│   ├── show.blade.php
│   └── category.blade.php
└── components/
    ├── header.blade.php
    └── footer.blade.php

Best Practices

Performance

  • Use section_items() for section data — it's cached automatically
  • Use menu_items() for menus — it's cached automatically
  • Use template_option() for settings — it's cached per-request
  • Avoid database queries directly in templates; rely on data passed from controllers

Compatibility

  • Use ($templatePath ?? 'templates.default') when including the layout or shared components to support the override system
  • Use {{ __('text') }} for all user-facing strings
  • Use logical CSS properties (ms-, me-, ps-, pe-, start, end) for RTL support
  • Use rtl: variants for RTL-specific transforms

Styling

  • Use Tailwind CSS with dark: variants for dark mode support
  • Use var(--color-primary) and the .bg-brand, .text-brand utilities for brand colors
  • Use color-mix(in srgb, var(--color-primary) 10%, white) for computed brand shades
  • Keep sections self-contained — each section should work independently

Structure

  • Keep blade templates clean — extract complex logic into helpers.php
  • Use @include for reusable components
  • Use @push/@stack for page-specific head/script content
  • Always check for empty data before rendering: @if($items->isNotEmpty())

SEO

  • Every page should push JSON-LD schema to the schema stack
  • Use {!! seo() !!} in the layout (handled automatically)
  • Add rel="prev" / rel="next" for paginated pages
  • Use semantic HTML (<article>, <section>, <nav>, <header>, <footer>)

Security

  • Always escape output with {{ }} (double curly braces)
  • Only use {!! !!} for trusted HTML (from the CMS itself, like post body)
  • Use @csrf in all forms
  • Never output raw user input without sanitization