Views and Components
Flute CMS uses Blade — a powerful templating engine from Laravel. Templates allow separating HTML markup from PHP logic and reusing code.
What is Blade
Blade is a templating engine that:
- Allows embedding PHP code in HTML via special syntax
- Supports template inheritance (layouts)
- Automatically escapes data (XSS protection)
- Compiles templates into PHP for high performance
Module Template Structure
- index.blade.php
- show.blade.php
- create.blade.php
| Folder | Purpose |
|---|---|
pages/ | Full pages (article list, product card, etc.) |
components/ | Reusable parts (cards, buttons, forms) |
layouts/ | Base layouts (usually inherit theme layout) |
Registering Templates
In the module provider, templates are registered automatically via bootstrapModule(). But you can register manually with a custom namespace:
// Default namespace = module name in kebab-case
// Module Blog → namespace 'blog'
$this->loadViews('Resources/views', 'blog');
// Now you can use:
// view('blog::pages.index')Blade Basics
Displaying Data
{{-- Output value with escaping (safe) --}}
<h1>{{ $article->title }}</h1>
{{-- WITHOUT escaping (use only for trusted HTML) --}}
<div class="content">{!! $article->content !!}</div>
{{-- Output with default value --}}
<p>Author: {{ $article->author->name ?? 'Anonymous' }}</p>Always use {{ }} for user data. Use {!! !!} syntax only for HTML that you have generated yourself.
Conditions
@if($articles->count() > 0)
<div class="articles">
{{-- Display articles --}}
</div>
@elseif($showEmpty)
<p>No articles yet, but coming soon!</p>
@else
<p>Articles not found</p>
@endif
{{-- Short form --}}
@unless($user->isAdmin())
<p>You are not an administrator</p>
@endunless
{{-- Check for existence --}}
@isset($article)
<h1>{{ $article->title }}</h1>
@endisset
@empty($articles)
<p>List is empty</p>
@endemptyLoops
{{-- Regular foreach --}}
@foreach($articles as $article)
<div class="article">
<h2>{{ $article->title }}</h2>
<p>{{ $article->excerpt }}</p>
</div>
@endforeach
{{-- With empty check --}}
@forelse($articles as $article)
<div class="article">{{ $article->title }}</div>
@empty
<p>No articles</p>
@endforelse
{{-- $loop variable is available inside the loop --}}
@foreach($items as $item)
@if($loop->first)
<p>This is the first element</p>
@endif
<p>{{ $loop->iteration }}. {{ $item->name }}</p>
@if($loop->last)
<p>This is the last element</p>
@endif
@endforeachThe $loop variable contains:
$loop->index— index (starting from 0)$loop->iteration— iteration number (starting from 1)$loop->first— is this the first iteration?$loop->last— is this the last iteration?$loop->count— total number of elements
Template Inheritance
Base Layout
Usually modules use the theme layout:
{{-- Resources/views/pages/index.blade.php --}}
{{-- Inherit theme layout --}}
@extends('flute::layouts.app')
{{-- Fill title section --}}
@section('title', 'Blog')
{{-- Fill content section --}}
@section('content')
<div class="container">
<h1>Latest Articles</h1>
@foreach($articles as $article)
<article class="article-card">
<h2>{{ $article->title }}</h2>
<p>{{ Str::limit($article->excerpt, 200) }}</p>
<a href="{{ route('blog.show', $article->id) }}">Read more</a>
</article>
@endforeach
</div>
@endsection
{{-- Add styles to head section --}}
@push('styles')
@at('Modules/Blog/Resources/assets/scss/blog.scss')
@endpushCreating Your Own Layout
If you need a specific layout for the module:
{{-- Resources/views/layouts/blog.blade.php --}}
@extends('flute::layouts.app')
@section('content')
<div class="blog-wrapper">
<aside class="blog-sidebar">
{{-- Blog sidebar --}}
@include('blog::components.sidebar')
</aside>
<main class="blog-content">
{{-- Child page content goes here --}}
@yield('blog-content')
</main>
</div>
@endsection{{-- Resources/views/pages/index.blade.php --}}
@extends('blog::layouts.blog')
@section('blog-content')
{{-- Content goes into main.blog-content --}}
<h1>Articles</h1>
...
@endsectionFallback System
Flute automatically searches for templates in several places:
This means that @extends('flute::layouts.app') will find the file in the active theme, and if it’s not there — in the standard one.
Components
Blade Components (include)
The simplest way to reuse code:
{{-- Resources/views/components/article-card.blade.php --}}
{{-- @props declares variables accepted by the component --}}
@props([
'article', // Mandatory parameter
'showExcerpt' => true // Optional, default true
])
<article class="article-card">
<h2 class="article-card__title">
<a href="{{ route('blog.show', $article->id) }}">
{{ $article->title }}
</a>
</h2>
@if($showExcerpt && $article->excerpt)
<p class="article-card__excerpt">
{{ Str::limit($article->excerpt, 150) }}
</p>
@endif
<div class="article-card__meta">
<span>{{ $article->author->name }}</span>
<time>{{ $article->created_at->format('d.m.Y') }}</time>
</div>
</article>Usage:
{{-- Pass parameters --}}
@include('blog::components.article-card', [
'article' => $article,
'showExcerpt' => false
])
{{-- Or in a loop --}}
@foreach($articles as $article)
@include('blog::components.article-card', ['article' => $article])
@endforeachYoyo Live Components
Yoyo allows creating interactive components that update without page reload (like React/Vue, but in PHP).
Creating a Yoyo Component
<?php
namespace Flute\Modules\Blog\Components;
use Clickfwd\Yoyo\Component;
/**
* Article filtering component.
* Updates when form fields change.
*/
class ArticleFilter extends Component
{
// Public properties are automatically preserved between requests
public string $search = '';
public string $category = '';
public string $sortBy = 'date';
/**
* Called when component initializes.
*/
public function mount()
{
// Can set initial values
$this->category = request()->get('category', '');
}
/**
* Method called on form submission.
*/
public function filter()
{
// Method filter will be called on yoyo:method="filter"
// Properties are already updated from the form
}
/**
* Reset filters.
*/
public function reset()
{
$this->search = '';
$this->category = '';
$this->sortBy = 'date';
}
/**
* Render component.
*/
public function render()
{
// Load data based on current filters
$query = rep(Article::class)->select()->where('published', true);
if ($this->search) {
$query->where('title', 'LIKE', "%{$this->search}%");
}
if ($this->category) {
$query->where('category_id', $this->category);
}
$query->orderBy($this->sortBy === 'date' ? 'created_at' : 'views', 'DESC');
$articles = $query->fetchAll();
$categories = rep(Category::class)->findAll();
return view('blog::components.article-filter', [
'articles' => $articles,
'categories' => $categories,
]);
}
}Yoyo Component Template
{{-- Resources/views/components/article-filter.blade.php --}}
<div>
{{-- Filtering form --}}
<form class="filter-form">
{{-- yoyo:val binds field to component property --}}
<input type="text"
yoyo:val="search"
placeholder="Search..."
class="filter-input">
<select yoyo:val="category" class="filter-select">
<option value="">All categories</option>
@foreach($categories as $cat)
<option value="{{ $cat->id }}">{{ $cat->name }}</option>
@endforeach
</select>
<select yoyo:val="sortBy" class="filter-select">
<option value="date">By Date</option>
<option value="views">By Popularity</option>
</select>
{{-- Button resets filters --}}
<button type="button" yoyo:on="click" yoyo:method="reset">
Reset
</button>
</form>
{{-- Results update automatically --}}
<div class="articles-grid">
@forelse($articles as $article)
@include('blog::components.article-card', ['article' => $article])
@empty
<p>Nothing found</p>
@endforelse
</div>
</div>Registration and Usage
Components are registered automatically via bootstrapModule() or manually:
$this->loadComponents(); // All components from Components/ folderUsage in template:
{{-- Component name = class name in kebab-case without Component suffix --}}
{{-- ArticleFilter → article-filter --}}
@yoyo('article-filter')
{{-- With parameters --}}
@yoyo('article-filter', ['category' => 'news'])Yoyo components work via AJAX. When yoyo:val fields change, the component automatically re-renders on the server and updates on the page.
Widgets
Widgets are blocks that can be placed on pages via the admin panel. Unlike components, widgets have settings.
Creating a Widget
<?php
namespace Flute\Modules\Blog\Widgets;
use Flute\Core\Widgets\WidgetInterface;
class RecentArticlesWidget implements WidgetInterface
{
/**
* Render widget.
*
* @param array $settings Settings from admin panel
*/
public function render(array $settings = []): string
{
$limit = $settings['limit'] ?? 5;
$showDate = $settings['show_date'] ?? true;
$articles = rep(Article::class)
->select()
->where('published', true)
->orderBy('created_at', 'DESC')
->limit($limit)
->fetchAll();
// Return HTML
return view('blog::widgets.recent-articles', [
'articles' => $articles,
'showDate' => $showDate,
])->render();
}
/**
* Unique widget name.
*/
public function getName(): string
{
return 'recent-articles';
}
/**
* Description for admin panel.
*/
public function getDescription(): string
{
return 'Shows latest blog articles';
}
/**
* Available widget settings.
* Displayed in admin panel when adding widget.
*/
public function getSettings(): array
{
return [
'limit' => [
'type' => 'number',
'label' => 'Number of articles',
'default' => 5,
'min' => 1,
'max' => 20,
],
'show_date' => [
'type' => 'boolean',
'label' => 'Show date',
'default' => true,
],
];
}
}Widget Template
{{-- Resources/views/widgets/recent-articles.blade.php --}}
<div class="widget widget--recent-articles">
<h3 class="widget__title">Recent Articles</h3>
<ul class="widget__list">
@foreach($articles as $article)
<li class="widget__item">
<a href="{{ route('blog.show', $article->id) }}">
{{ $article->title }}
</a>
@if($showDate)
<time class="widget__date">
{{ $article->created_at->format('d.m.Y') }}
</time>
@endif
</li>
@endforeach
</ul>
</div>Using Widget
{{-- In templates --}}
{!! widget('recent-articles', ['limit' => 10]) !!}Built-in Directives
Including Resources
The @at() directive includes CSS, JS, or images:
{{-- SCSS is compiled automatically --}}
@at('Modules/Blog/Resources/assets/scss/blog.scss')
{{-- JS file --}}
@at('Modules/Blog/Resources/assets/js/blog.js')
{{-- File from theme --}}
@at('Themes/standard/assets/sass/app.scss')Translations
{{-- Translation function --}}
<h1>{{ __('blog.title') }}</h1>
{{-- With parameters --}}
<p>{{ __('blog.articles_count', ['count' => $count]) }}</p>
{{-- Directive --}}
@lang('blog.welcome')Authentication and Permissions
{{-- Auth check --}}
@auth
<p>Hello, {{ user()->name }}!</p>
@endauth
@guest
<a href="{{ route('login') }}">Login</a>
@endguest
{{-- Permission check --}}
@can('manage_blog')
<a href="{{ route('admin.blog') }}">Manage Blog</a>
@endcan
@cannot('delete_articles')
<p>You do not have permission to delete</p>
@endcannotIcons
{{-- Built-in icon component --}}
<x-icon name="heart" />
<x-icon name="settings" class="icon-lg" />Styles and Scripts
Push and Stack
Allow adding styles/scripts from child templates to parent:
{{-- In parent template (layout) --}}
<head>
<link rel="stylesheet" href="/css/app.css">
@stack('styles') {{-- Styles from @push will be inserted here --}}
</head>
<body>
@yield('content')
<script src="/js/app.js"></script>
@stack('scripts') {{-- Scripts from @push will be inserted here --}}
</body>{{-- In child template --}}
@extends('flute::layouts.app')
@push('styles')
<link rel="stylesheet" href="/modules/blog/css/blog.css">
@endpush
@section('content')
<h1>Blog</h1>
@endsection
@push('scripts')
<script src="/modules/blog/js/blog.js"></script>
@endpushIncluding SCSS from Provider
public function boot(\DI\Container $container): void
{
// SCSS is compiled automatically
$this->loadScss('Resources/assets/scss/blog.scss');
$this->bootstrapModule();
}SCSS is compiled via scssphp and cached in public/assets/css/cache/. In production, files are minified.
Template Caching
Automatic Caching
- Blade templates are compiled into PHP and saved in
storage/app/views/ - SCSS is compiled and cached in
public/assets/css/cache/ - In development mode, templates are recompiled on change
- In production, cache persists until manual clear
Clearing Cache
// Via code
template()->clearCache();
// Via console
// php flute template:cache:clearOrganization Tips
File Structure
✅ Good:
- Use module namespaces (
blog::pages.index) - Extract repeating code into components
- Separate into folders:
pages/,components/,layouts/
❌ Bad:
- Duplicating HTML in different files
- Huge files with logic and markup
- Hardcoding paths instead of
route()
Components
{{-- ✅ Good: reusable component with parameters --}}
@props(['type' => 'info', 'dismissible' => false])
<div class="alert alert--{{ $type }} @if($dismissible) alert--dismissible @endif">
{{ $slot }}
@if($dismissible)
<button type="button" class="alert__close">×</button>
@endif
</div>{{-- ❌ Bad: hardcoded values --}}
<div class="alert alert--info">
Some message
</div>Performance
- Use
@oncefor code that should run only once - Minimize nesting of
@include(each include = file operation) - For heavy data, use caching in the controller, not in the template