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
Public Asset Symlinks
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 disk | URL 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 |
How the Symlink is Created
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
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.
Removing the Symlink
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');
}
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>×</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