Customize PagibleAI Theme

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.

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

  1. Navigate to the ./public/vendor/cms/theme/ directory.
  2. Open the cms.css (or any other CSS file you want to modify).
  3. Make your desired changes.
  4. 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';
img-src 'self' data: blob:;
media-src 'self' data: blob:;
style-src 'self' https://hcaptcha.com https://*.hcaptcha.com;
script-src 'self' https://hcaptcha.com https://*.hcaptcha.com;
frame-src 'self' https://hcaptcha.com https://*.hcaptcha.com;
connect-src 'self' https://hcaptcha.com https://*.hcaptcha.com

These rules only allow CSS, JS and images from the current domain/port as well as connecting to the HCaptcha server.

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(): Returns true if the node is the root node (has no parent).
  • isLeaf(): Returns true if 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): Retuns true if the $page node is a descendant of $item or 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 generated using the following Blade code:

<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>

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.

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.main from 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. The cmsasset() 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 @yield directive in the parent layout. The name main must be the same name used in the @yield directive 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 type="text/css" rel="stylesheet" href="{{ cmsasset('vendor/cms/theme/hero.css') }}">
@endPushOnce

<h1 class="title">{{ @$data->title }}</h1>

@if(@$data->text)
    <div class="subtitle">
        @markdown($data->text)
    </div>
@endif

@if(@$data->url)
    <a class="btn url" href="{{ $data->url }}">{{ @$data->button }}</a>
@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 when css:hero would be used as parameter, the file would be only added once even if it's included by several different content elements. The cmsasset() helper function generates the correct URL to the asset within the CMS's asset directory.
  • {{ @$data->title }}: This displays the title of the hero element because $data->title retrieves the title value from the content element's data. The @ symbol is used for error suppression and if $data->title is not set, no error will be thrown. Instead, the heading will simply be empty.
  • @if(@$data->text): This conditional block checks if the content element has a text field with a value which isn't an empty string. If it does, the code within the if-block is executed.
  • @markdown($data->text): This displays the text content of the hero element. The @markdown() directive is a crucial part; it parses the text content as Markdown, converting Markdown syntax (e.g., headers, lists, bold text) into corresponding HTML elements.
  • @if(@$data->url): This conditional block checks if the content element has a URL field. If it does, the button using the URL as href is rendered.
  • <a href="{{ $data->url }}">{{ @$data->button }}</a>: This creates a button with a link using the URL provided in $data->url. The link text (the text displayed to the user) is taken from the $data->button field. Error suppression is also utilized in case $data->button is missing.

In summary, this Blade template dynamically renders a hero content element, including its title, an optional subtitle text (parsed as Markdown), and a button linking to a specified URL (also optional). The @pushOnce directive ensures efficient CSS loading, and the conditional @if statements handle cases where certain data fields might be missing. Using the @markdown directive converts the text in Markdown syntax generated by the CKEditor text fields in the backend to HTML.

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 $default if either $item or $prop is 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 the asset() 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 $path is null or empty, it returns an empty string.
  • cmsattr(?string $name): string: This function sanitizes a string for use as an HTML attribute (e.g., id or class).
  • 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 a files object with the file IDs as keys. If the content element is a shared element or an action, it returns their data as plain array.
  • 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 $item is 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 a srcset attribute for responsive images, this function expects the value of the previews file 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 $path starts 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's Storage facade to generate a URL for the file, using the disk configured in cms.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, the cms::invalid template is rendered, which adds a comment to the HTML output that no view was found for the element type.