Skip to Content
ModulesViews & Components

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
FolderPurpose
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> @endempty

Loops

{{-- 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 @endforeach

The $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') @endpush

Creating 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> ... @endsection

Fallback System

Flute automatically searches for templates in several places:

Current Theme

app/Themes/current_theme/views/

Standard Theme

app/Themes/standard/views/

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]) @endforeach

Yoyo 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/ folder

Usage 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> @endcannot

Icons

{{-- 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> @endpush

Including 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:clear

Organization 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">&times;</button> @endif </div>
{{-- ❌ Bad: hardcoded values --}} <div class="alert alert--info"> Some message </div>

Performance

  • Use @once for 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