Skip to main content

Views & Assets

This guide covers creating Blade templates, working with the Metronic theme, managing static assets via the public/ directory, and how symlinks make your assets web-accessible.

View Structure

Addon views are stored in Resources/views/:

Resources/views/
├── layouts/
│ └── master.blade.php # Optional: addon-specific layout
├── index.blade.php # Main page
├── settings.blade.php # Settings page
├── reports/
│ ├── overview.blade.php
│ └── details.blade.php
└── partials/
├── header.blade.php
└── stats-card.blade.php

Extending the Main Layout

Mumara Campaigns uses the Metronic theme. Extend the main layout:

{{-- Resources/views/index.blade.php --}}
@extends('layouts.master2')

@section('title', 'Email Validator')

@section('content')
<div class="kt-container kt-container--fluid kt-grid__item kt-grid__item--fluid">

{{-- Page Header --}}
<div class="kt-subheader kt-grid__item" id="kt_subheader">
<div class="kt-container kt-container--fluid">
<div class="kt-subheader__main">
<h3 class="kt-subheader__title">Email Validator</h3>
<span class="kt-subheader__separator kt-subheader__separator--v"></span>
<span class="kt-subheader__desc">Validate and verify email addresses</span>
</div>
<div class="kt-subheader__toolbar">
<a href="{{ route('emailvalidator.validate.form') }}" class="btn btn-brand btn-elevate btn-icon-sm">
<i class="la la-plus"></i>
New Validation
</a>
</div>
</div>
</div>

{{-- Main Content --}}
<div class="kt-content kt-grid__item kt-grid__item--fluid">
<div class="kt-portlet">
<div class="kt-portlet__head">
<div class="kt-portlet__head-label">
<h3 class="kt-portlet__head-title">Recent Validations</h3>
</div>
</div>
<div class="kt-portlet__body">
@include('emailvalidator::partials.recent-table', ['results' => $recentResults])
</div>
</div>
</div>

</div>
@endsection

@section('scripts')
<script>
// Page-specific JavaScript
</script>
@endsection

View Namespacing

Access addon views using the module alias as namespace:

// In controllers
return view('emailvalidator::index');
return view('emailvalidator::settings.general');
return view('emailvalidator::partials.table');
{{-- In Blade templates --}}
@include('emailvalidator::partials.header')
@extends('emailvalidator::layouts.master')

Passing Data to Views

// Array syntax
return view('emailvalidator::index', [
'title' => 'Dashboard',
'stats' => $stats,
]);

// Using compact
return view('emailvalidator::index', compact('title', 'stats'));

// Using with()
return view('emailvalidator::index')
->with('title', 'Dashboard')
->with('stats', $stats);

View Composers

Share data across multiple views automatically:

// In service provider boot()
public function boot(): void
{
view()->composer('emailvalidator::*', function ($view) {
$view->with('addonVersion', config('emailvalidator.version'));
});
}

Metronic UI Components

Portlets (Cards)

<div class="kt-portlet">
<div class="kt-portlet__head">
<div class="kt-portlet__head-label">
<span class="kt-portlet__head-icon">
<i class="flaticon-email"></i>
</span>
<h3 class="kt-portlet__head-title">
Card Title
<small>Subtitle text</small>
</h3>
</div>
<div class="kt-portlet__head-toolbar">
<a href="#" class="btn btn-clean btn-sm btn-icon btn-icon-md">
<i class="la la-refresh"></i>
</a>
</div>
</div>
<div class="kt-portlet__body">
{{-- Card content --}}
</div>
<div class="kt-portlet__foot">
<div class="kt-form__actions">
<button type="submit" class="btn btn-primary">Submit</button>
<button type="reset" class="btn btn-secondary">Cancel</button>
</div>
</div>
</div>

Data Tables

<table class="table table-striped table-bordered table-hover" id="myTable">
<thead>
<tr>
<th>Email</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach($results as $result)
<tr>
<td>{{ $result->email }}</td>
<td>
@if($result->status === 'valid')
<span class="kt-badge kt-badge--success kt-badge--inline">Valid</span>
@else
<span class="kt-badge kt-badge--danger kt-badge--inline">Invalid</span>
@endif
</td>
<td>
<a href="{{ route('emailvalidator.result', $result->id) }}"
class="btn btn-sm btn-clean btn-icon btn-icon-md" title="View">
<i class="la la-eye"></i>
</a>
</td>
</tr>
@endforeach
</tbody>
</table>

@push('scripts')
<script>
$('#myTable').DataTable({ responsive: true, order: [[1, 'desc']] });
</script>
@endpush

Alerts

@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<div class="alert-icon"><i class="flaticon-warning"></i></div>
<div class="alert-text">{{ session('success') }}</div>
<div class="alert-close">
<button type="button" class="close" data-dismiss="alert">
<span><i class="la la-close"></i></span>
</button>
</div>
</div>
@endif

Asset Management

The public/ Directory

Every addon has a public/ directory for static assets that need to be served by the web server:

Addons/YourAddonName/
└── public/
├── css/
│ ├── addon.css
│ └── settings.css
├── js/
│ ├── addon.js
│ └── charts.js
├── images/
│ ├── logo.png
│ └── icons/
│ └── feature.svg
├── fonts/
│ └── custom-font.woff2
└── plugins/
└── some-library/
├── lib.min.js
└── lib.min.css

When an addon is enabled, the system creates a symlink so the browser can access the public/ directory:

public/Addons/YourAddonName/ → Addons/YourAddonName/public/

This means:

File on diskURL in browser
Addons/YourAddonName/public/css/addon.css/Addons/YourAddonName/css/addon.css
Addons/YourAddonName/public/js/addon.js/Addons/YourAddonName/js/addon.js
Addons/YourAddonName/public/images/logo.png/Addons/YourAddonName/images/logo.png

The symlink is created automatically when the addon is enabled. On Linux/macOS this creates a standard symbolic link; on Windows it creates a junction (which works without administrator privileges).

To manually create the symlink:

# Linux / macOS
mkdir -p public/Addons
ln -s "$(pwd)/Addons/YourAddonName/public" public/Addons/YourAddonName

# Windows (Command Prompt as admin)
mklink /D public\Addons\YourAddonName Addons\YourAddonName\public

# Windows (Junction, no admin needed)
mklink /J public\Addons\YourAddonName Addons\YourAddonName\public
Why Symlinks?

The symlink approach keeps your addon self-contained -- all code and assets live under Addons/YourAddonName/. Only the public/ subdirectory is exposed to the web, so PHP source code, configuration, routes, and other sensitive files remain inaccessible from the browser.

When an addon is disabled or uninstalled, the symlink is removed:

# Linux / macOS
rm public/Addons/YourAddonName

# Windows
rmdir public\Addons\YourAddonName

Including Assets in Views

Reference your addon's public assets in Blade templates:

@section('styles')
{{-- From your addon's public/ directory --}}
<link href="/Addons/YourAddonName/css/addon.css" rel="stylesheet">
<link href="/Addons/YourAddonName/plugins/some-library/lib.min.css" rel="stylesheet">
@endsection

@section('scripts')
<script src="/Addons/YourAddonName/js/addon.js"></script>
<script src="/Addons/YourAddonName/plugins/some-library/lib.min.js"></script>
@endsection

For assets compiled by Laravel Mix into the main public/ directory:

@section('styles')
<link href="{{ asset('css/youraddonname.css') }}" rel="stylesheet">
@endsection

@section('scripts')
<script src="{{ asset('js/youraddonname.js') }}"></script>
@endsection

CSS url() References Within Assets

When writing CSS files that reference images or fonts, use paths relative to the web root since your assets are served from /Addons/YourAddonName/:

/* public/css/addon.css */

/* Reference images in your addon's public directory */
.logo {
background-image: url(/Addons/YourAddonName/images/logo.png);
}

/* Relative paths also work (relative to the CSS file location) */
.icon {
background-image: url(../images/icon.svg);
}

/* Reference fonts */
@font-face {
font-family: 'CustomFont';
src: url(../fonts/custom-font.woff2) format('woff2');
}
tip

Prefer relative paths (../images/icon.svg) over absolute paths (/Addons/YourAddonName/images/icon.svg) within CSS files. Relative paths continue to work even if the addon is renamed or the URL structure changes.

Source Assets & Compilation

Source files that need compilation (SASS, ES6+, TypeScript) live in Resources/assets/ and are compiled to public/:

Resources/assets/           → (compile) → public/
├── sass/app.scss → ├── css/addon.css
├── js/app.js → ├── js/addon.js
└── (not web-accessible) └── (web-accessible)

Laravel Mix Configuration

// webpack.mix.js
const mix = require('laravel-mix');

// Compile into your addon's public/ directory
mix.js(__dirname + '/Resources/assets/js/app.js', __dirname + '/public/js/addon.js');
mix.sass(__dirname + '/Resources/assets/sass/app.scss', __dirname + '/public/css/addon.css');

// Production versioning
if (mix.inProduction()) {
mix.version();
}

Building Assets

cd Addons/YourAddonName

# Install dependencies
npm install

# Development build
npm run dev

# Watch for changes
npm run watch

# Production build (minified)
npm run production

Static Assets (No Build Step)

If you don't need SASS or ES6 compilation, simply place your CSS and JavaScript files directly in public/:

public/
├── css/
│ └── addon.css # Hand-written CSS
├── js/
│ └── addon.js # Hand-written JavaScript
└── images/
└── logo.png

No build step required -- these files are served directly by the web server through the symlink.

AJAX Requests

@push('scripts')
<script>
$(document).ready(function() {
$('#validateBtn').on('click', function() {
var email = $('#emailInput').val();

$.ajax({
url: '{{ route("emailvalidator.validate.ajax") }}',
method: 'POST',
data: {
email: email,
_token: '{{ csrf_token() }}'
},
beforeSend: function() {
KTApp.blockPage({
overlayColor: '#000000',
opacity: 0.1,
state: 'primary',
message: 'Validating...'
});
},
success: function(response) {
KTApp.unblockPage();
if (response.success) {
toastr.success(response.message);
}
},
error: function(xhr) {
KTApp.unblockPage();
toastr.error('Validation failed. Please try again.');
}
});
});
});
</script>
@endpush

Modals

{{-- Trigger Button --}}
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#confirmModal">
Delete Selected
</button>

{{-- Modal --}}
<div class="modal fade" id="confirmModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm Delete</h5>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete the selected items?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmDelete">Delete</button>
</div>
</div>
</div>
</div>

Publishing Views

Allow users to customize your addon's views by publishing them:

// In service provider
$this->publishes([
module_path($this->moduleName, 'Resources/views')
=> resource_path('views/modules/' . $this->moduleNameLower),
], 'views');

Published views in resources/views/modules/youraddonname/ take precedence over the addon's original views.

php artisan vendor:publish --tag=youraddonname-views