Add dynamic Content to Pages

Dynamic content integration is a powerful feature, but it's crucial to understand its impact on performance. Using APIs and JavaScript to load content after the initial page request can negatively affect PageSpeed scores and loading times. Therefore, this approach is best suited for updating specific page sections based on user interactions after the initial page load. A prime example is a blog list that dynamically loads subsequent entries when the user clicks on pagination.

For optimal performance and top scores in Google Lighthouse and PageSpeed, the complete page content should be delivered in the initial server response. To incorporate dynamic content from databases or external sources, PagibleAI CMS provides actions. These are PHP classes designed to supply dynamic data for your Blade templates or JSON API responses.

Note: Pages are fully cached if their GET requests doesn't contain any URL query parameters! Therefore, adding random content isn't possible when using action classes, so use Javascript to fetch and/or display random content in that case.

Understanding Action Classes

Action classes are the backbone of dynamic content generation within PagibleAI CMS. They are simple to implement and offer a flexible way to fetch and format data.

A basic action class requires minimal code and should reside in the ./app/Cms/ directory. If this directory doesn't exist, create it within your application. The class namespace must correspond to the directory structure, and the class name must match the file name. For instance, the App\Cms\MyAction class must be located in ./app/Cms/MyAction.php:

<?php

namespace App\Cms;

use Aimeos\Cms\Models\Page;
use Illuminate\Http\Request;

class MyAction
{
    public function __invoke( Request $request, Page $page, object $item )
    {
        return '';
    }
}

While the example uses the App\Cms namespace, you're free to use any namespace that your Composer autoloader can resolve, such as one from your own package.

The __invoke() Method

Every action class must include an __invoke() method. This method is automatically executed when the action is called and receives the following parameters:

  • $request: The Laravel HTTP request object for the current request. Provides access to request parameters, headers, and other request-related information.
  • $page: The PagibleAI CMS Page object associated with the current domain and path. This allows you to access page-specific data and settings.
  • $item: The content element object that triggered the action call. This object contains data configured within the CMS for this specific content element.

Understanding the Item Object

This is a generic PHP object (stdClass) with public properties depending on the content element defined in the ./config/cms.php file. The base properties that should be available in all content elements are:

  • id: Unique content ID within the page
  • type: Type of the content element the action is used in
  • group: Section the content element is assigned to

Each content element can contain arbitrary settings depending on the fields defined for the content element, e.g.:

'fields' => [
    'title' => [
        'type' => 'string',
    ],
    'action' => [
        'type' => 'hidden',
        'value' => '\App\Cms\MyAction',
    ],
    'limit' => [
        'type' => 'number',
            'min' => 1,
            'max' => 100,
            'default' => 10,
        ],
    ],
],

The values configured for the instance of the content element by the editor in the PagibleAI admin backend are stored in the page content as:

{
    "id": "DLeH0cd",
    "type": "blog",
    "group": "main",
    "data": {
        "limit": 2,
        "title": "Blog",
        "action": "\\App\\Cms\\MyAction",
    },
}

To access these properties in your action class use:

$id = @$item->id;
$type = @$item->type;
$group = @$item->group;

$title = @$item->data?->title;
$action = @$item->data?->action;
$limit = @$item->data?->limit;

The @ operator returns NULL if the property isn't available while the ?-> operator prevents accessing sub-properties if the data property isn't there.

Handling Versions Correctly

If you want to use content from the PagibleAI CMS (pages, shared elements or files), you must care about returning the right data depending on who is viewing the result. For logged in users with page:view (editor) privileges, the latest version should be used if filters are applied to the query builder of the model:

$builder = Page::where( 'parent_id', $pid );

if( \Aimeos\Cms\Permission::can( 'page:view', $request->user() ) ) {
    $builder->whereHas('latest', function( $builder ) {
        $builder->where( 'data->status', 1 );
    } );
} else {
    $builder->where( 'status', 1 );
}

For editors, the latest page versions of all pages with the same parent_id value are filtered by their status in this example. It uses the status property from the data JSON column stored in the cms_versions table in this case, while for non-editor users, the page status column is used directly.

The works the same way for shared elements and files, only the properties you can filter by are different.

Using Pagination

Laravel offers a simple to use pagination for models that can also be returned directly:

return $builder->paginate( 10, ['title', 'content'], 'p' );

The three parameters accepted by the paginate() method are:

  1. Maximum number of items shown per page
  2. List of model properties that should be available
  3. Name of the GET query parameter used for pagination

Important: Ensure each action uses a unique query parameter name for pagination to prevent conflicts. Using the same parameter name across multiple actions will cause unintended simultaneous reactions.

You can also transform the found items before returning them by using the through() method:

return $builder->paginate( ... )
    ->through( function( $item ) {
        // transform item data
        return $item;
    } );

For example, if you want to return only specific content elements of a page and update the files associated to the page item to only use those which are used in the left-over content elements, these lines does the job:

return $builder->paginate( ... )
    ->through( function( $item ) {
        $item->content = collect( $item->content )->filter(
            fn( $item ) => $item->type === 'article'
        );
        $item->setRelation( 'files', \Aimeos\Cms\Utils::files( $item ) );
        return $item;
    } );

Returning Values

You can return any type of data or object from the __invoke() method, but it's essential to keep these requirements in mind:

  • Blade Template Compatibility: Ensure your Blade template is designed to handle the structure and format of the returned data.
  • JSON Serialization: For JSON API responses, the returned value must be JSON serializable. This means it should be a scalar value, an array, or an object that can be converted to JSON.
  • Full-Text Search Indexing: To enable proper indexing for full-text search, the returned value must either be convertible to a string or implement the __toString() magic method. Laravel collections are iterated and its entries casted to strings.

Required configuration

To integrate your action class within a content element, you need to define it in your ./config/cms.php file. Specifically, the content element's fields array needs an action entry pointing to your class:

'myelement' => [
    'group' => 'content',
    'icon' => '<svg>...</svg>',
    'fields' => [
        'action' => [
            'type' => 'hidden',
            'value' => '\App\Cms\MyAction',
        ],
        // additional fields for config settings
    ],
],

This configuration creates a content element called myelement. When the page data is rendered (either in the Blade template or via the JSON API), your action class will be executed. The hidden field type ensures the editor can't see or modify the action field. The value must be the fully qualified class name, including its namespace.

Important: Namespace and class name are case-sensitive. Double-check that they exactly match the values defined in your PHP file!