Vue Server-Side Rendering in plentyShop

With the version 5.0.30 of the plentyShop LTS plugin, we introduced Vue Server-Side Rendering (SSR) to plentyShop. This update serves to prepare plentyShop LTS online shops for the changes brought by the Google core update and to generally improve online shop performance.

With SSR, Vue.js components, whose markup was previously generated in the browser, are now already processed on the web server. Therefore, the HTML, that has already been generated, is sent to the browser and can directly be displayed there. Consequently, the page can already be displayed before the necessary JavaScript has been loaded and interpreted. Nevertheless, Vue.js applications that are rendered on the server side are also initialised and rendered again in the browser in order to subsequently equip the static markup delivered by the server with dynamic parts of a web application, such as click listeners, data bindings, etc. The process of adding dynamic application parts to the static markup is called hydration. For this, it is important that the markup provided by the server and the markup generated in the browser do not differ.

Basics

The basis of the implementation of SSR in plentyShop is the official SSR guide of Vue.js.

The section Writing Universal Code defines some basic requirements that must be fulfilled by Vue components so they can be processed on the server side. The implementation in plentyShop LTS also has the following requirements:

  • Currently, only so-called Single-File-Components (SFCs) are rendered on the server side. In plentyShop LTS, this covers all components on static pages, category and item views. At this time, MyAccount, Checkout and associated pages, such as order confirmation, return form etc., can not be rendered on the server side.

  • Individual templates that are referenced via the override-template property on a Vue component cannot be taken into account on the server side and therefore lead to hydration errors (see above). However, it is possible to explicitly exclude individual components from server-side rendering (see below).

The global overwriting of component templates through the data-component attribute on the corresponding script tag is still possible (see Changing the template of a Vue component).

Preparing a template for SSR

During server-side rendering, the browser-specific objects window and document and thus all common functions for browsing or manipulating the DOM are not available. For this reason, the template must be provided with markers (so-called "directives") in some relevant places, on the basis of which the HTML document can be analysed and interpreted. These are specified in the form of HTML comments in order to obtain valid HTML even without further processing. However, during processing by server-side rendering, these comments are removed and not delivered to the browser.

All directives start with an HTML template of the form <!-- SSR:xyz() --> and end with another comment of the form <!-- /SSR -->. Directives cannot be nested.

The following directives are available:

SSR:app() directive

This directive marks the element that should be used as the template for the main component. In plentyShop LTS, for example, this element is marked with the ID “vue-app”.

<!-- SSR:app() -->
<div id="vue-app">
...
</div>
<!-- /SSR -->

This directive should only be used once per page.

SSR:global($identifier) directive

This directive marks a script tag with JSON-formatted data that needs to be globally available during server-side rendering under the specified identifier.

<!-- SSR:global(App) -->
<script id="app-data" type="application/json">
    {
        "config": {{ ceresConfig | json_encode | raw }},
        "urls": {{ urls | json_encode | raw }}
    }
</script>
<!-- /SSR -->

Within server-side rendered components, the example above enables, for instance, the access to App.config.

SSR:entry($path) directive

This directive specifies the path to a JavaScript module that should be loaded during server-side rendering. This directive is not closed by a comment of the form <!-- /SSR -->!

To determine the correct path, the Twig function ssr_entry() should be used here:

<!-- SSR:entry({{ ssr_entry('Ceres', 'path/to/bundle.js') }}) -->

See the section "Providing JavaScript modules for SSR" for more details and requirements for server-side loaded modules.

SSR:on() and SSR:off() directives

The content of the directive is only output when the SSR is active/inactive and removed otherwise.

<!-- SSR:on() -->
<p>This page was rendered on the server-side!</p>
<!-- /SSR -->

<!-- SSR:off() -->
<p>This page was not rendered on the server-side!</p>
<!-- /SSR -->

SSR:template($componentTag) directive

To overwrite the template of a Vue component, the script tag with the corresponding data-component attribute must be marked by the directive SSR:template.

<!-- SSR:template(add-to-basket) -->
<script type="x/template" data-component="add-to-basket">
...
</script>
<!-- /SSR -->

Providing JavaScript modules for SSR

The directive <!-- SSR:entry() -→ can be used to load arbitrary JavaScript modules during server-side rendering, e.g. to register own Vue components or to change the initial state of the VueX state.

Provided modules should be bundled as commonjs2 modules (see [Webpack: output.libraryTarget ](https://webpack.js.org/configuration/output/#librarytarget-commonjs2) for further information). The following fields can be exported per module:

  • beforeCreate: (context) ⇒ void : Function that is executed before server-side creation of the Vue application and is passed the renderer context as an argument.

  • beforeRender: (context) ⇒ void : Function that is executed before server-side rendering and is passed the created instance of the Vue application as an argument.

  • afterRender: (vueApp) ⇒ void : Function that is executed after server-side rendering. It receives the created instance of the Vue application as an argument.

  • globals: { [key: string]: any } : Object whose values are made globally available with the respective key as variable name.

  • createApp: (context) ⇒ Promise<Vue> : Method that creates the actual Vue instance for the respective context. The implementation by previously loaded modules is overwritten.

import Vue from "vue";
const globals = { Vue };

function beforeCreate(context) {
    ...
}

function beforeRender(vueApp) {
    ...
}

function afterRender(vueApp) {
    ...
}

export { globals, beforeCreate, beforeRender, afterRender };

Switching plentyShop LTS to server-side rendering

The necessary changes to plentyShop LTS are currently available on GitHub on the branch "feature/ssr". No adjustments are necessary for IO, meaning that the IO stable branch or the version from the Marketplace can be used. To be able to use the SSR feature, test systems must currently be activated by us or moved to an appropriate test environment. Alternatively, you can order your own test systems with activated server-side rendering for the duration of the hackathon (until 30 April). For more information see this post in the forum.

Adding preloading to your widgets

In the context of Server-Side Rendering, we added the setting Preload image to our image widgets. This setting allows that the images used in the widgets image box, image carousel, background image, and item image are already processed on the server’s side, so that the shop’s performance can be increased. You can find further information on how to best use preloading in our PageSpeed Insights best practice.

If you are creating your own widgets that use images, this section will help you learn how to include preloading in your widgets.

Creating a setting in the widget PHP class

First, you should extend the PHP class of your widget with a setting that the user can activate and deactivate. Our image box widget uses a common checkbox for this:

$settings->createCheckbox('preloadImage')
            ->withName('Widget.preloadImageLabel')
            ->withTooltip('Widget.preloadImageTooltip')
            ->withCondition("!lazyLoading");

Note that the image box widget can only be preloaded if lazy loading is not active. The setting includes all the usual suspects: the interactive element, the title, and the tooltip.

Implementing Twig code

After you’ve added the setting to the PHP class, it is time to add the relevant Twig code to the widget. First, make sure to access the settings you just added:

{% set preloadImage     = widget.settings.preloadImage.mobile %}

In a next step, it is important that you set the URL of the image with Twig.set in the Twig builder because you will need the image URL in the subsequent step.

{{ Twig.set("imageUrl", "" | json_encode) }}

Finally, add a conditional if construction and use Twig.print to hand over the image URL you set before. While most image formats are automatically recognized as the image type (namely PNG, JPG, JPEG, WEBP, and GIF), you should still include 'image' in case another format is used.

{% if preloadImage %}
    {{ Twig.print("add_asset(imageUrl,'image')") }}
{% endif %}

And that’s it. If you would like to take a look at how team plentyShop implemented preloading in their widgets, feel free to check out the open source code of the background image widget, image box widget (which was used as the basis of this guide), or the image carousel widget.

SSR Troubleshooting

In this section, we will look at a number of common problems, which can appear in combination with Server-side Rendering, and how to solve them.

How can I check if a page was successfully rendered on the server?

For this, you should inspect the source code of the page before it is processed by Javascript. To do that, open the source code of the page in the browser or disable the execution of Javascript. Now the structure of the document should look like this:

<html>
  <head>...</head>
  <body>
    <div id="vue-app">
      Serverside rendered markup
    </div>
    <script id="ssr-script-container">
      <div id="vue-app">
        Raw markup before rendering
      </div>
    </script>
  </body>
</html>

Make sure to inspect the markup above the ssr-script-container and not its contents because it contains the markup of your app before it is rendered. This content is used by the browser to render the app again and apply dynamic functions to the server-side rendered markup. This process is called hydration.

Server-side errors

These errors may occur while rendering your Vue.js application on the server. In preview mode they will be forwarded to the browser; in productive mode, they are only written to the log and the frontend will fall back to client-side rendering.

No app factory provided

There is no Javascript that exports a createApp() function. By default, this is done by the ceres-server.js from the plentyShop LTS plugin.

You should check if:

<!-- SSR:entry(…​) -→ is included anywhere in your template (by default, this is placed in PageDesign.twig).

If you provide your own Javascript bundles, make sure it exports a createApp() function in the „commonjs2“ 3 format.

Directive not closed correctly: Found ‚SSR:abc()‘ before closing ‚SSR:xyz()‘.

The SSR directives could not be parsed correctly. The parser detects a directive before the previous one was closed with <!-- /SSR -→.

You should check if:

  • All directives are closed correctly.

  • Directives are not nested. Consider imported Twig templates here as well.

Cannot load module: path/to/script.js

Your Javascript is trying to import an external script that doesn’t exist on the server.

You should check if:

  • The imported file is located in your plugin directory.

  • The importedf file is not excluded for the upload to your plentymarkets system, e.g. in the node_modules directory.

TypeError: Cannot read property ‚globals‘ of undefined

The rendering process cannot read a registered entry module correctly.

You should check if:

  • All modules registered via <!-- SSR:entry() -→ exist and do not contain any syntax errors.

  • All registered modules are using the format commonjs2 1.

Error creating app

An error occured while importing all registered scripts. This happens before the rendering of your components (see „Error compiling template“). See appended error message and the logs for details.

Error compiling template

An error occured while compiling the contents of the vue-app element. See appended error message and the logs for details.

Vue SSR is not available

The required resources are not available on your server. Please contact us in the forum.

Client-side errors

These errors occur in the client after rendering the Vue.js application on the server successfully. They are logged to the Javascript console in the developer tools of your browser.

The client-side rendered virtual DOM tree is not matching server-rendered content.

When providing server-side rendered markup, Vue.js renders the app again in the client/browser and tries to inject interactive parts of the application into the server-side rendered markup. To do this, the markup that is provided by the server needs to match to the rendered markup of the client. Otherwise, Vue has to do a full client-side render so the application is still usable, but there is no benefit in the performance anymore. Normally this error appears together with a warning that includes the list of DOM elements provided by the server and the list of virtual nodes created by Vue.js while rendering the application in the client.

You should check if:

  • You have conditional elements with v-if or v-for that are handled in different ways on the server or on the client.

  • You are injecting asynchronous components into a slot. There is a bug in Vue.js that leads to asynchronous components (not loaded by the main Javascript bundle but in separate chunks) producing hydration errors when the are placed into slots. The recommended workaround is to wrap the component in any HTML tag:

Instead of using this:

<template #before-price>
  <my-async-component></my-async-component>
</template>

you can try wrapping it like this:

<template #before-price>
  <div><my-async-component></my-async-component></div>
</template>

Checklist

If you are a developer implementing SSR in a theme or plugin, you should go through the checklist below and make sure your code checks all the boxes. The most common SSR errors arise if one of the following guidelines is not adhered to:

  • Have you added the necessary SSR directives outlined above?

  • Did you make sure you only included markup and no logic in the created() hook, as specified in the Vue documentation?

  • Are all of your HTML tags closed properly?

  • Did you make sure that your HTML code is valid?

  • Does the code run without any SSR errors in the log?