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.
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
:
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.
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.
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.
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.
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:
- Maximum number of items shown per page
- List of model properties that should be available
- 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;
} );
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.
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!