PagibleAI provides a flexible theming system that allows you to customize the look and feel of your website easily. This document outlines how to modify the views, CSS, and JavaScript files to achieve your desired design.
Customize PagibleAI Theme
Modify CSS and JavaScript
The CSS and JavaScript files for the PagibleAI theme are published to the ./public/vendor/cms/theme/ directory during installation. You can directly modify these files to customize the theme's styling and functionality.
Example: Modifying the main CSS file
- Navigate to the
./public/vendor/cms/theme/directory. - Open the
cms.css(or any other CSS file you want to modify). - Make your desired changes.
- Save the file.
The same process applies to JavaScript files. If content elements need CSS styling or Javascript code, its stored in files specific for this content element, i.e. the contact form includes a contact.css and contact.js file in its Blade view. Therefore, only styles and code from elements used in the page are sent to the browser resulting in high performance scores in Goolge Lighthouse metrics.
Caution: Be aware that the CSS and JavaScript files in the ./public/vendor/cms/theme/ directory are not automatically updated when you install a new version of PagibleAI. This means that any modifications you make to these files will persist across updates, but you may need to manually merge any changes from the new version of PagibleAI into your customized files to ensure compatibility and access to the latest features.
Override Views
To customize the look and feel of PagibleAI, you can override the default views by placing your own versions in the ./resources/views/vendor/cms/ directory of your Laravel application. This allows you to tailor the appearance of various components to match your website's design. This approach leverages Laravel's view customization capabilities which provides a clean and maintainable way to modify package views without directly altering the package's core files.
For instance, to customize the hero.blade.php view, you would first copy the original file from the PagibleAI package into your application's directory:
./resources/views/vendor/cms/hero.blade.php
The page layouts are available at:
./resources/views/vendor/cms/layouts/
For example, the main layout file is in:
./resources/views/vendor/cms/layouts/main.blade.php
After copying the file, you can freely modify it to implement your desired changes. Laravel will automatically load your custom view instead of the default one provided by PagibleAI. This process ensures that your customizations are preserved even when you update the PagibleAI package. For more information on how Laravel handles views, refer to the official Laravel documentation on views.
Adapt main layout
Layouts within PagibleAI are essentially Laravel Blade templates. The default main layout is located in this file:
./vendor/aimeos/pagible/views/layouts/main.blade.php
To customize the layout, you must not modify the file directly in the vendor directory. Instead, copy it to:
./resources/views/cms/layouts/main.blade.php
If you make your changes there, it ensures your customizations are preserved during updates.
Content Security Policy
The main template contains a Content Security Policy (CSP), which is an added layer of security that helps to detect and mitigate certain types of attacks, including Cross Site Scripting (XSS) and data injection attacks. CSP works by restricting the sources from which the browser is allowed to load resources. By defining a strict CSP, you can prevent the browser from loading malicious scripts or other content from unauthorized sources. The CSP rules included in the template are:
base-uri 'self';
default-src 'self';
frame-src 'self' <cms.theme.csp.frame-src>;
connect-src 'self' <cms.theme.csp.connect-src>;
img-src 'self' data: blob: <cms.theme.csp.media-src>;
media-src 'self' data: blob: <cms.theme.csp.media-src>;
style-src 'self' 'nonce-...' <cms.theme.csp.style-src>;
script-src 'self' 'nonce-...' <cms.theme.csp.script-src>;
The CSP directives are configurable via environment variables in your .env file:
CMS_CSP_MEDIA_SRC: Additional sources for images and mediaCMS_CSP_STYLE_SRC: Additional sources for stylesheets (defaults to hCaptcha domains)CMS_CSP_FRAME_SRC: Additional sources for iframes (defaults to hCaptcha domains)CMS_CSP_SCRIPT_SRC: Additional sources for scripts (defaults to hCaptcha domains)CMS_CSP_CONNECT_SRC: Additional sources for AJAX/fetch requests (defaults to hCaptcha domains)
By default, only the current domain and hCaptcha domains are allowed. To add additional trusted sources, set the corresponding environment variable, e.g.:
CMS_CSP_SCRIPT_SRC="https://hcaptcha.com https://*.hcaptcha.com https://cdn.example.com"
Page Head Elements
Meta tags are dynamically generated within the layout using the following Blade code:
@foreach(cms($page, 'meta', []) as $item)
@includeFirst(cmsviews($page, $item), cmsdata($page, $item))
@endforeach
It renders all elements from the meta section of the page added in the admin backend according to their views and content.
In the HTML header, the @stack('css') and @stack('js') directives are used, which are Blade features that allow you to push CSS and JavaScript code from content element templates onto stacks, which are then rendered later in the layout.
<link href="{{ cmsasset('vendor/cms/theme/pico.min.css') }}" rel="stylesheet">
<link href="{{ cmsasset('vendor/cms/theme/pico.nav.min.css') }}" rel="stylesheet">
<link href="{{ cmsasset('vendor/cms/theme/pico.dropdown.min.css') }}" rel="stylesheet">
<link href="{{ cmsasset('vendor/cms/theme/cms.css') }}" rel="stylesheet">
@stack('css')
<script defer src="{{ cmsasset('vendor/cms/theme/cms.js') }}"></script>
@stack('js')
This is useful for including CSS and JS files that are specific to certain sections of a page or individual content elements. By using stacks and @pushOnce, you can ensure that all necessary CSS and JavaScript is only loaded if required and only once, even if a content element is used multiple times on a page.
The page editor in the admin backend requires specific CSS and JavaScript files to function correctly. These files must be included conditionally based on the user's permissions using the following code:
@if(\Aimeos\Cms\Permission::can('page:save', auth()->user()))
<link href="{{ cmsasset('vendor/cms/admin/editor.css') }}" rel="stylesheet">
<script defer src="{{ cmsasset('vendor/cms/admin/editor.js') }}"></script>
@endif
Main Navigation
The page navigation is dynamically generated using the following Blade code:
@foreach($page->nav() as $item)
@if(cms($item, 'status') == 1)
<li>
@if($item->children->count())
<details class="dropdown is-menu">
<summary role>{{ cms($item, 'name') }}</summary>
<ul class="align">
@foreach($item->children as $subItem)
@if(cms($subItem, 'status') == 1)
<li>
<a href="{{ cmsroute($subItem) }}"
class="{{
$page->isSelfOrDescendantOf($subItem)
? 'active'
: ''
}} contrast">
{{ cms($subItem, 'name') }}
</a>
</li>
@endif
@endforeach
</ul>
</details>
@else
<a href="{{ cmsroute($item) }}"
class="{{
$page->isSelfOrDescendantOf($item)
? 'active'
: ''
}} contrast">
{{ cms($item, 'name') }}
</a>
@endif
</li>
@endif
@endforeach
This code snippet creates a two-level navigation menu, rendering sub-menus where necessary. It uses the $page->nav() method, which returns a nested set collection from the kalnoy/nestedset Laravel package representing the hierarchical navigation structure. The depth of this structure is configurable by changing the navdepth setting in ./config/cms.php.. You can also retrieve a specific level of the navigation tree using $page->nav(x), where x is the desired starting level.
While the code primarily uses simple properties like children and methods like count(), here are some useful methods provided by the kalnoy/nestedset Laravel package that you might find helpful when building a more complex navigation:
children: A relationship that returns a collection of the node's direct children. Used directly in the code.parent: A relationship that returns the parent node.ancestors: Returns a collection of all ancestor nodes, up to the root. This could be useful for generating breadcrumbs.descendants: Returns a collection of all descendants of the node (children, grandchildren, etc.).count(): Returns the number of nodes at the current level.depth: Returns the depth of the node in the tree (root node has a depth of 0).isRoot(): Returnstrueif the node is the root node (has no parent).isLeaf(): Returnstrueif the node is a leaf node (has no children).getPath(): Returns a collection of ancestor nodes including the current node. Usefull for creating breadcrumbs.$page->isSelfOrDescendantOf($item): Retunstrueif the$pagenode is a descendant of$itemor the $item itself.
The code also utilizes a few CMS helper functions:
cms(): Retrieves data from CMS items (pages, content elements, etc.).cmsroute(): Generates the URL for a CMS page.
See "Template Helper Functions" for more detailed information.
Breadcrumb Navigation
Breadcrumbs are generated using the following Blade code:
@if($page->ancestors->count() > 1)
<nav aria-label="breadcrumb">
<ul>
@foreach($page->ancestors->skip(1) as $item)
@if(cms($item, 'status') == 1)
<li>
<a href="{{ cmsroute($item) }}">{{ cms($item, 'name') }}</a>
</li>
@else
@break
@endif
@endforeach
<li>{{ cms($page, 'name') }}</li>
</ul>
</nav>
@endif
This code first checks if the current page has more than one ancestor (meaning it's not the homepage). If it does, it renders a breadcrumb navigation. The code iterates through the page's ancestors (parent pages), skipping the first one (the homepage).
For each ancestor, it checks its status to ensure that only active pages are included in the breadcrumb. The cmsroute() helper function generates the URL to the page, and cms($item, 'name') retrieves the name of the page to display in the breadcrumb. The last element in the breadcrumb is the current page's name.
Content from Page Templates
The @yield('main') directive is a placeholder in the main layout where the content of individual pages will be injected. When a page layout extends the main layout using @extends('cms::layouts.main'), the content defined within the @section('main') ... @endsection block in the page layout will be inserted into the main layout at the location of the @yield('main') directive.
You can also inject more sections defined in the different page layouts. If the sections should contain editable content, you need to add the section name to the theme configuration in ./config/cms.php.
Footer
The footer is not part of the main layout directly — instead, main.blade.php contains a @yield('footer') placeholder, and the footer content is defined in page layouts (e.g., page.blade.php) within a @section('footer') block. The footer is generated using the following Blade code:
@section('footer')
<footer class="cms-content" data-section="footer">
@foreach($content['footer'] ?? [] as $item)
@if($el = cmsref($page, $item))
<div id="{{ cmsattr(@$item->id) }}" class="{{ cmsattr(@$el->type) }}">
<div class="container">
@includeFirst(cmsviews($page, $el), cmsdata($page, $el))
</div>
</div>
@endif
@endforeach
</footer>
<footer class="copyright">
© {{ date('Y') }} {{ config('app.name') }}
</footer>
@endsection
This code iterates through the content elements assigned to the "footer" content group. The cmsref() function resolves references if $item is a shared element. The @includeFirst directive dynamically includes the appropriate Blade view for the content element based on its type, passing in the relevant data. A copyright line with the current year and application name is also appended.
The cms-content CSS class enables the page editor to change the element while the data-section attribute tells the page editor to which section new content elements should be added to.
Customize page layouts
Layouts within PagibleAI are essentially Laravel Blade templates. The default layouts are located in the ./vendor/aimeos/pagible/views/layouts/ directory. However, you should never modify files directly within the vendor directory, as changes will be overwritten during updates.
To customize these layouts, you can override them by creating corresponding files in the ./resources/views/cms/layouts/ directory of your Laravel application. This approach allows you to modify the layout structure and design without affecting the core PagibleAI files.
When creating your custom page layout, you'll typically use Blade directives like @extends('cms::layouts.main') and @section('main'). Here's the Blade template of the standard page template:
@extends('cms::layouts.main')
@pushOnce('css')
<link href="{{ cmsasset('vendor/cms/theme/layout-page.css') }}" rel="stylesheet">
@endPushOnce
@section('main')
<div class="cms-content" data-section="main">
@foreach($content['main'] ?? [] as $item)
@if($el = cmsref($page, $item))
<div id="{{ cmsattr(@$item->id) }}" class="{{ cmsattr(@$el->type) }}">
<div class="container">
@includeFirst(cmsviews($page, $el), cmsdata($page, $el))
</div>
</div>
@endif
@endforeach
</div>
@endsection
Let's delve deeper into these directives:
@extends('cms::layouts.main'): This directive is the cornerstone of layout inheritance in Blade. It tells your custom layout to inherit the structure and content from the specified parent layout, in this case,cms::layouts.mainfrom the PagibleAI CMS package. This parent layout provides the basic HTML structure, including the<html>,<head>, and<body>tags, along with common elements like headers, footers, and navigation.@pushOnce('css'): This directive ensures that the CSS stylesheet defined within it (in this case,layout-page.css) is only included once on the page, regardless of how many times the template is included or rendered. This prevents duplicate CSS rules and keeps your page loading efficiently. Thecmsasset()helper function generates the correct URL to the asset within the CMS's asset directory.@section('main')... @endsection: This directive defines a specific section within your custom layout. A section acts as a placeholder where you can inject content that will be displayed in a corresponding@yielddirective in the parent layout. The namemainmust be the same name used in the@yielddirective in the parent layout where you want this content to be inserted.
Inside the main section, the <div class="cms-content" data-section="main"> serves two purposes: The CSS class cms-content enables the editor to change the content element in the page editor view of the admin backend while the data section main defines the content group new elements are assigned to when using the page editor.
Then, the code iterates through the $content['main'] array, which contains the content elements assigned to the "main" content group of your page in the CMS. The available content groups are defined by the theme configuration, e.g. in the ./config/cms.php file. All configured content groups with content elements are available in the $content variable in the template, which is an associative array.
The cmsref($page, $item) function resolves references if $item is a shared element. Otherwise, the content element itself is returned. If the content element exists, it's wrapped in a div with an ID and class based on the element's type. This is important for the page editor to find the corresponding content element the editor wants to modify in the page editor view.
Finally, the @includeFirst(cmsviews($page, $el), cmsdata($page, $el)) dynamically includes the appropriate Blade view for the content element, passing in the relevant data. This allows you to build up the page's content from reusable content elements managed within the PagibleAI CMS.
Customize content elements
This Blade template is responsible for rendering a "hero" content element within the PagibleAI CMS:
@pushOnce('css')
<link href="{{ cmsasset('vendor/cms/theme/hero.css') }}" rel="stylesheet">
@endPushOnce
<div class="first">
@if(@$data->subtitle)
<div class="subtitle">
{{ $data->subtitle }}
</div>
@endif
<h1 class="title">{{ @$data->title }}</h1>
@if(@$data->text)
@markdown($data->text)
@endif
@if(@$data->url)
<a class="btn url" href="{{ $data->url }}">{{ @$data->button }}</a>
@endif
</div>
@if($file = cms($files, @$data->file?->id))
<div class="second">
@include('cms::pic', ['file' => $file, 'main' => true, 'sizes' => '50vw'])
</div>
@endif
Let's break down its functionality:
@pushOnce('css'): This directive ensures that the CSS stylesheet for the hero element (hero.css) is included only once on the page, even if multiple hero elements are present. This prevents redundant CSS loading and whencss:herowould be used as parameter tocmsasset(), a full URL pointing to the CSS file would be returned.- The template is structured into two sections:
<div class="first">for the text content and<div class="second">for an optional image. {{ $data->subtitle }}: Displays an optional subtitle above the main title.{{ @$data->title }}: Displays the hero's main title using the null-safe@operator, which prevents errors if the title is not set.@markdown($data->text): Renders the hero's descriptive text as Markdown.@if(@$data->url): Conditionally renders a call-to-action button linking to the specified URL, with the button text from$data->button.- The image section uses
cms($files, @$data->file?->id)to look up the file object and thecms::picpartial to render a responsive image withsrcset.
In summary, this Blade template dynamically renders a hero content element with two sections: a text area containing an optional subtitle, a title, optional Markdown text, and a call-to-action button, alongside an optional image. The @pushOnce directive ensures efficient CSS loading, and the conditional @if statements handle optional fields gracefully. The cms::pic partial is used to render responsive images with appropriate srcset attributes.
Template Helper Functions
cms(?object $item, ?string $prop, $default = null): This is a central function to retrieve data from CMS items (like pages or elements). It attempts to get a specific property ($prop) from the given$item. It handles null inputs gracefully, returning$defaultif either$itemor$propis null and it supports Laravel Collections too. If the user has 'page:view' permission, it tries to retrieve the property from the latest version, otherwise it returns the value from the published version.cmsasset(?string $path): string: It generates a URL for a static CMS asset (CSS, JavaScript, images, etc.). Internally, it uses theasset()helper to generate the base URL and appends a version string (?v=) based on the file's last modification time. This ensures that browsers load the latest version of the asset.- If
$pathis null or empty, it returns an empty string.
- If
cmsattr(?string $name): string: This function sanitizes a string for use as an HTML attribute (e.g.,idorclass).cmsdata(\Aimeos\Cms\Models\Page $page, object $item): array: To pass data to content element templates, the function returns the content element properties as array and adds afilesobject with the file IDs as keys. If the content element is a shared element or an action, it returns their data as plain array.cmsfile(\Aimeos\Cms\Models\Page $page, string $fileId): ?object: Retrieves a file object from a CMS page by its ID. Returns the file object if found, or null if not found. This is a shortcut forcms(cms($page, 'files'), $fileId).cmsref(\Aimeos\Cms\Models\Page $page, object $item): object: This function resolves references to shared CMS elements. If a referenced element is found, it's returned as an object; otherwise, the original$itemis returned.cmsroute(\Aimeos\Cms\Models\Page $page): string: This function generates the URL to a CMS page or returns the redirect URL of the page. If the user has 'page:view' permission, it uses the latest page version while without, the URL of the published version is returned.cmssrcset($data): string: To generate asrcsetattribute for responsive images, this function expects the value of thepreviewsfile property and returns the correct srcset value.cmsurl(?string $path): string: Generates a full URL for a CMS file stored using Laravel's storage system. If the$pathstarts with 'data:', 'http:', or 'https:', it's assumed to be a data URI or an absolute URL and is returned as is. Otherwise, it uses Laravel'sStoragefacade to generate a URL for the file, using the disk configured incms.disk(defaulting to 'public').cmsviews(\Aimeos\Cms\Models\Page $page, object $item): array: Returns the path of the first matching view for rendering a CMS content element based on the page theme and content element type. If no view is found, thecms::invalidtemplate is rendered, which adds a comment to the HTML output that no view was found for the element type.