Server Side Rendering
The rendering server
The rendering server is a NodeJS application that renders the boards/articles.
On the development the rendering server starts when the make dev
command is run.
On the production the rendering server is started with supervisor with the following configuration file:
Find node path
> which node
/usr/bin/node
Config file
[program:project_renderer]
command=__PATH_TO__/node /var/www/__PROJECT_NAME__/node_modules/wfcms-renderer/src/index.js
environment=ASSETS_PATH="/var/www/__PROJECT_NAME__/web/dist/server",PORT="8081"
user=www-data
autostart=true
autorestart=true
startsecs=0
startretries=999
logfile=/var/log/renderer-__PROJECT_NAME__.log
stdout_logfile=/var/log/renderer-__PROJECT_NAME__.log
stderr_logfile=/var/log/renderer-__PROJECT_NAME__.log
Renderer configuration
For changing port or host you can overwrite in project the following params:
wf_cms.templating.external_renderer.host
wf_cms.templating.external_renderer.port
IS_SERVER / IS_CLIENT checks
If there are differences in the code being ran in the client (browser - this is used in admin part) or on the server (NodeJS, this is used in the public part), you can use the IS_SERVER
and IS_CLIENT
constants.
- in javascript:
if (IS_SERVER) {
// run this block only on the SSR
}
if (IS_CLIENT) {
// run this block only in the browser
}
- in templates:
<div data-bind="toggle:IS_SERVER">
<!-- show this block only when rendered on the server -->
</div>
<div data-bind="toggle:IS_CLIENT">
<!-- show this block only when rendered inside the editor (browser) -->
</div>
Note: data-bind="toggle:IS_SERVER"
is used as an example, it can be used with other binding handlers as well.
Debugging issues
There might be cases when the servering server throws an error and/or the pages don't render correctly.
Error rendering a page
The rendering on the admin can be checked at the url: /content/render/{pageID}
. This uses the JSs compiled by webpack for the server to render the page in the browser. Additionally, a curl
command is logged to the console that allows you to check the rendering against the NodeJS server.
If both the /content/render/{pageID}
route and the curl
command copied from there return the expected HTML, you can try to access /view/page-id/{pageID}.html
. This returns the HTML of the given page as returned by the rendering server, but also with all the changes that the Symfony app makes on that HTML
Error getting the template config
The rendering server is also used for reading modules' configuration from the template. This has two main purposes (for now, at least):
- accessing
{{page.supra}}
in Twig (or$page->getSupra()
in PHP) should return the content of the module with rolesupra
, but it should throw an error if no such module is defined. So the Symfony app accesses/template-config/{templateName}
to get a list of all the modules defined. - using the
wf-serializable
module in PHP code
To get the config for all the modules in a template, the rendering server initializes a template with all the modules having wf-new
, so it instantiates all the modules available in the template. This may lead to some bugs that go unnoticed for the developer, as it reaches all the modules defined. An error in one of these modules would result in the template config not being available.
To check the template config, go to /content/template-config/{templateName}
in the browser. Similar to the rendering route above, this runs the template config app in the browser, using the JSs compiled for the server. It also logs the full curl
command to the console to run it against the NodeJS server directly.
Serialization
As mentioned in Internals section, the Symfony app sends to NodeJS all the details that the JS app will need to render the page. This includes a serialized version of the page (edit
serialization group), together with details about the author(s), tags and embedded media.
You can hook into the serialization process in two ways:
Enabling a XalokNext optional serializer
wf_cms_base:
page_serializer:
enabled:
- banners
These serializers are not enabled by default, the developer must enable them in the project.
For now, only the banners
serializer exists. It serializes the Banner
entities linked to the current page in the banners
key
Writing your own serializer
!!! IMPORTANT !!! If working with modules in XalokNext:6.0, use AbstractModulesPageSerializer
Define a service and tag it with wf_cms.page_serializer
.
<service id="wf_cms.serializer.page_serializer.custom_serializer" class="Path\To\Serializer\Class" public="false">
<tag name="wf_cms.page_serializer" />
</service>
Note: In Xalok:6.0 the above is NOT needed - creating a class that implements Wf\Bundle\CmsBaseBundle\Serializer\PageSerializer\PageSerializerInterface
is enough, autowiring takes care of the rest.
This service should implement Wf\Bundle\CmsBaseBundle\Serializer\PageSerializer\PageSerializerInterface
. This interface defines two methods:
getKey():string
- this should return the key under which the data should be saved. In the banners' case it'sbanners
.serializeForRendering(Wf\Bundle\CmsBaseBundle\Entity\Page $page, $previous)
- this method should return the data that will be saved under the key returned bygetKey
.$previous
contains an array with all the details serialized until this point, in case you need something from previous serializations.
This data is accessible on the JS application as an attribute of the page
model. In the banners
example, the ads module reads the data returned by Wf\Bundle\CmsBaseBundle\Serializer\PageSerializer\BannersPageSerializer::serializeForRendering
using this.options.page.get('banners')
.
Extending AbstractModulesPageSerializer
v6+This feature was added in XalokNext:6.0
Working with modules during page serialization is one of the most common use cases for custom serializers. For example: making sure that all articles (or those using a certain template) have the sidebar
module; or adding advertisement positions after every X words (or before the last paragraph).
For this, XalokNext:6.0 introduces a base class that makes working with modules easier. Extend Wf\Bundle\CmsBaseBundle\Serializer\PageSerializer\AbstractModulesPageSerializer
and implement
processModules(
Wf\Bundle\CmsBaseBundle\Entity\Collection\PageEditorMutableModuleCollection $modulesCollection,
Wf\Bundle\CmsBaseBundle\Entity\Page $page,
&$previous
): void
The fist argument received by this function is a mutable form of PageEditorModulesCollection
. This means that you can make changes to the modules returned by it, these changes will be sent to NodeJS:
// single module
// NOTE: The `&` in front of the `getRoledModule` is important!!!
$title = &$modulesCollection->getRoledModule('title');
$title['content'] .= ' edited from the serializer';
// multiple modules
$paragraphs = $modulesCollection->getRoledModules('paragraph');
if (count($paragraphs)) {
$paragraphs[0]['content'] .= ' edited from the serializer';
}
// or, processing them in a loop
// NOTE: The `&` in front of the `$paragraph` is important!!!
foreach ($paragraphs as &$paragraph) {
$paragraph['content'] .= ' edited from the serializer';
}
To make working with the returned results easier, PageEditorMutableModulesCollection
appends some keys to the module:
__role
(string) the role of the returned module__rolePath
(array) the full role path to this module (e.g.:['main_image', 'image_description'])
__masterRole
(string) the master role of the returned module (a module with roleparagraph--1
has master roleparagraph
)__masterRolePath
(array) the master role path of the returned module
PageEditorMutableModulesCollection
also embeds Wf\Bundle\CmsBaseBundle\Templating\ModulesBuilder
, forwarding all add*
methods to it:
if (!$modulesCollection->getRoledModule('sidebar')) {
$modulesCollection->add('sidebar', []);
}
// or
$modulesCollection->addTextModule('paragraph', 'Paragraph automatically added by the page serializer');
v4-6 Passing settings to the SSR
If you need to pass settings from PHP/Twig to the JS application, use the wf_cms_render_page_with_settings
function:
{{ wf_cms_render_page_with_settings(pageSlug, pageVersion, {"option1": "value1"}) }}
In the JS application you can access these settings through page.options.renderSettings
. Inside a module, that is this.options.page.options.renderSettings
v7.1+ Render context
Starting with v7.1+ XalokNext supports passing data from PHP/Twig to the JavaScript code through the renderContext
:
{{ wf_cms_render_board('example', {
template: 'board/example',
title: 'board-title.example'|trans,
renderContext: {
someVariable: someValue
}
}) }}
This can be accessed through the vuex store: store.state.page.renderContext.someVariable
, both when by client-side code (when editing the board) or by server-side code (when rendering the board for the public part)
Note: !!! IMPORTANT all the variables from the renderContext
are passed through the query string. This has the potential of generating many objects in the Varnish cache, or to render it completely useless (e.g.: if one passes a random value, that fragment won't be taken from the cache).
Internals
From the outside world, the rendering server should not be accesible. The Symfony application is the one that serves the requests to the outside world. In an HTML page, the Symfony application renders the main body of the response, from <html>
to </html>
. Where it encounters a wf_cms_render_page
(board or article), the PageController:showAction
(that serves these requests) extracts the data that will be needed for rendering this page (page details, author's details, tags and embedded images/audios/videos), serializes these details and sends them to the NodeJS application. This means that the NodeJS application does not need access to any external sources (MySQL/Redis), nor does it know about security/whether a page is published or not - these checks are still done in the Symfony application.