SSR for Micro Frontend CMS-based Architecture

Fabio Brunori
Valtech Switzerland
6 min readSep 8, 2022

It stumbled upon me during my job as a FE developer / consultant to see situations where a company would outsource many small projects or functionalities that stack up to compose an heterogeneous website.

The ability to exploit the power of multiple libraries and Frameworks and the freedom it gives you don’t come without any drawbacks though: While it’s easier to find resources if they can choose the stack to work with, this scenario poses an integration issue that’s not trivial to deal with (imagine having Vue.js and React mixed and coexisting).

This article presents a real-case problem (details may vary) and the prototype of a solution.

Let’s say we have an existing public website, authored via CMS and full of useful, powerful components written in different frameworks (for simplicity, let’s limit it to Vue 3 and React).

While authors and users do not care what’s written in what and developers are more than ok using their favourite technology, the search engine crawlers have their say on the quality of something that requires javascript to be read thus forcing websites that wants to be well placed on search engines to statically serve their pages.

This article is a follow up to David Lorentz’s, attempting to Server Side Rendering Web Components containing applications written in different web frameworks / libraries. The solution proposed here leverages the relatively new Declarative Shadow DOM Api

The first step is to subdivide the problem in simpler smaller units and address each of them individually.

Note about the terminology:

I know, it’s confusing, please bear with me. In the CMS context, we’re talking about components (the ones that authors drag and drop while editing pages); in Web Frameworks / Libraries context, we are talking about applications (React, Vue..); they are practically the same.

What we want to do:

  • user navigates webpage.
  • the server fetches the content from the CMS
  • the server SSR the components in the CMS page
  • the server returns the rendered, static, very fast (?) webpage the user has requested.
  • in case of normal users (not crawlers or robots) we want them to be able to use the website with all its perks: the various components need to work properly after the page has been loaded!

How we do it:

  • for any component, we want the server to be able to render it, and for the client to hydrate it.
  • we want to be able to nest multiple components
  • we want multiple frameworks and libraries to work together

hence, the first problem we’re gonna tackle is the Hydration. Since we don’t yet care about multiple frameworks, we’re gonna do it with Vue3.

Hydration

As we want to leverage the Declarative Shadow DOM api we want our server output to be something like

<template shadowroot="open"><!--some content --></template>

potentially having <slot></slot> inside to slot content.

Since both <template /> and <slot /> are built-in components of Vue and therefore conflict with its apis, we need a specific way to render them:

For simplicity, this assumes there’s no named slot.

When rendered, SSRShadowButton will result in <template shadowroot="open"><button><slot>Some Text</slot> 1</button></template> . The minimal logic is to test whether or not the hydration worked.

Now onto the client rehydration logic:

However, if we execute this, we’re now running into a problem: Hydration node mismatch.

When Vue tries to hydrate the component it compares the SSR string (the compiled version) to the current one in the DOM. The console in fact logs <button><slot>Some Text</slot> 1</button>.

Where’s the <template> at? From the declarative shadow dom documentation:

A template element with the shadowroot attribute is detected by the HTML parser and immediately applied as the shadow root of its parent element.

That means that the <template> element won’t be serialized by the parent element’s innerHTML nor by the shadow root’s as it doesn’t use the new getInnerHTML({ includeShadowRoots: true }) API (at least at the time of writing).

Solving the matter is easy task: we can simply remove the <shadow-template-factory> from the Vue template and wrap the output of the app with the template tag: function wrapAsShadowDOM(html) { return `<template shadowroot="open">${html}</template>`; }.

The client now expects the target element on which we mount to not to have the template as it’s not part of the app itself. Running this we now see that hydration works.

Composition

Now that we know how to hydrate a Vue app embedding it in a web component let’s take a look at how we pass from the CMS page to whatever we pass back to the user:

The overall call flow goes like this:

  1. Get the html for the desired page
  2. Get some config (can be static and/or retrieved together) about which tags to SSR and how (is it Vue or React or.. ?)
  3. Parse the page to work on the tree(jsdom is being used as it happens on a node server)
  4. Traverse the tree and create a parallel structure to be returned
  5. Serialize the obtained structure and return it as text.

For each node we visit, we either clone it as-is and iterate on its children (if the configuration doesn’t state otherwise) or we ssr it. in this latter case we proceed as follows:

  1. Create a node with the right name
  2. SSR the app with the rendered and app from the configuration
  3. Parse the resulted string into a tree
  4. Attach this onto the node we created
  5. Look for a <slot /> element so we can iterate the children and attach them to it (if there’s no <slot />, we assume the app doesn’t want children and we just cut the branch)
  6. Attach the created node to it’s parent in the parallel tree

Nesting

Step 5 reiterates over children and attaches them to the visited element taking care of nesting for us. Let’s add a new Vue component and change the CMS content to use it:

as before we add some interaction to test the hydration, give it a try and acknowledge that it works.

Parameters

let’s now add some personalization to these components:

  • In the CMS, we do <ssr-shadow-wrapper title-color="blue"> to add a parameter.
  • We pass all the attributes of the node from the CMS to the renderer when parsing the tree (server) and before mounting (client)
  • Pass then the attribute map to the createApp function that will forward them to the actual component data: () => ({ titleColor: props['title-color'] }), template: `<ssr-shadow-wrapper :titleColor="titleColor">...`

Multiple Frameworks Integration: React

React is (in theory) engineered to be used in Micro-Frontend architectures. This holds true if you stick with it for the whole architecture (mounting react pieces in a react orchestrator). In the moment we want to have some other framework nested in a React app or a totally different React app things get complicated.

To prevent React from touching the DOM after mounting, we will return an empty <div /> from the render() method. The <div /> element has no properties or children, so React has no reason to update it, leaving the jQuery plugin free to manage that part of the DOM.

From the official documentation, this is the intended way to have multiple frameworks working with React. Seems easy then? well, we have SSR and apparently hydration expects a certain app to not have anything past its point. This means that any content inside the <slot /> will be considered unexpected and the hydration will fail (and insert the newly created app into the existing HTML).

We can take home some information:

  • We want to have a react component that only renders the <slot /> so it’ll never re-render and thus care about what’s happening below it.
  • To avoid memory leaks, we should unmount all the apps below a certain component when this gets unmounted.
  • We need a way to make sure a react app doesn’t know about anything meant to be slotted in it.

Let’s elaborate the last point first: the implementation will differ a bit from the Vue one.

Everything else about the implementation is pretty trivial.

Conclusions

As headless CMS are becoming more and more a thing this kind of approach make possible to have an heterogeneous structure for a project, blending together old and new pieces of technologies and reaching a level of flexibility and integration that wasn’t possible before. Do you actually want something like this? Probably not. Sticking with 1 solid plan it’s usually better than mixing a multitude of solutions. We all know though that sometimes we’re stuck into doing integrations of this kind (be it budget, be it a strict client demand..)

Regardless of the ideal scenario, I like to add weapons to my arsenal and this is certainly a powerful one.

Final Notes

  • At the time of writing, only Chromium supports Declarative Shadow DOM. SEO should therefore be fine but Safari and Firefox should use a polyfill.
  • If the various apps in the page need to communicate (eg. share state) there’s the need to implement a bridge mechanism (leveraging events from in and out the shadow DOM; that’s why the shadowroot="open" attribute)

--

--