The Route to Visual SPA Editing
Although hosting a Single Page Application (SPA) or Progressive Web App (PWA) directly on Magnolia is possible, many of our partners and customers prefer to host their application on a remote server like Node.js that they either host themselves or run on a platform like Netlify or Vercel.
The new Magnolia feature “External SPA” allows content authors to edit applications in Magnolia’s Visual SPA Editor despite them being hosted remotely. This new feature is available now, opening doors for exciting new headless architectures like Jamstack. If you want to learn how to use it, check out our recent blog post “Visual Editing for Your Next.js App and Other Front Ends”.
For a look behind the scenes at how we implemented the feature and some of the problems that we encountered along the way, keep on reading below.
The Headless Accelerator by Magnolia
Speed up the development of your digital experiences with a unified workflow and a library of UI patterns. Check out the Headless Accelerator by Magnolia.
Challenge 1: Modifying the Content of an iFrame
Magnolia’s Page Editor is written in Java and powered by GWT, pronounced “gwit” and short for Google Web Toolkit. GWT is a framework for creating browser-based Java applications and deploying them as JavaScript, making apps portable across browsers and devices.
In FreeMarker projects, opening a page for editing triggers the FreeMarker rendering engine that processes the source node including any HTML comments for templating annotations, for example:
<!--cms:component content=”website:/react-minimal/main/0” dialog=”spa-lm:components/headline” label=”Headline” activationStatus=”0”-->
It then transforms the content and wraps areas and components, for example:
<div class=mgnlEditorBar mgnlEditor component mgnlEditorBarStatusIndicator” draggable=”true” style=”-webkit-user-drag: element; cursor: move;”>
<div class=”mgnlEditorBarLabelSelection”>...</div>
<div class=”mgnlEditorBarButtins”>...</div>
</div>
After the page is loaded, the Magnolia Page Editor injects itself into the iFrame, checks annotations in DOM, and produces green bars around areas and components for editing, for example:
But there’s a catch. To edit a page, the Magnolia Page Editor opens the page in an iFrame. However, if the SPA is hosted on a different server than the Magnolia server, it has a different origin, and no modern web browser allows a page to access or modify the content of an iFrame if it is not located on the same host and port.
So at this point, we were at a crossroads.
Either we implement the Page Editor from scratch, replicating all of its functionality including personalization and multi-site management, or we find a way to make use of our existing GWT code. We opted for the latter.
So, how could we overcome the iFrame security issue preventing the host from manipulating the SPA’s DOM in the iFrame? Our plan was that the SPA itself loads the necessary JavaScript code to manipulate the DOM to add the green bars.
However, the Page Editor widget is part of the Vaadin widgetset, which also includes the rest of our AdminCentral UI. GWT loads and unloads chunks of this code dynamically, but there is no separate JavaScript file for the Page Editor that the remote server could source.
So, we created a secondary widgetset, which contains only the Page Editor. This widgetset is compiled into a single static JavaScript file and is served by the Magnolia instance. The frontend-helper libraries check if the SPA runs inside an iFrame and if so, embeds the external widgetset. The widgetset communicates with the parent element through the Post Message API.
This abstraction layer leaves the door open for re-implementing the external widgetset’s functionality without GWT.
Challenge 2: Component-Level Data Fetching
In Jamstack architecture, applications are powered by server-side rendering (SSR) and static site generation (SSG). That means that the app server requests all necessary data and renders the page into HTML, either at every request with SSR, or at build time with SSG. This raises several issues.
The one issue that I want to address is component-level data fetching. Next.js only offers fetching mechanisms at the page level. This means that a component cannot fetch its own data and render it on the server. Instead, each component must fetch its data from the page it exists on.
That's not ideal for dynamic pages like the ones defined in a CMS; the page simply doesn't know what components are used until it is rendered for the first time.
The challenges of component-level data fetching are not exclusive to Magnolia, it is a common problem that developers face and there are a few ways to solve it. Developers will have to choose an approach that makes the most sense for their project.
The first “solution” is component-level fetching in the browser. However, this can only be considered a workaround as we lose many of the benefits of SSR, as the server will return only some of the content. This can be acceptable depending on the project requirements and is the solution we chose.
Another solution is to render pages twice before sending them to the browser: once to collect all of the components, then a second time to fetch any data that a component specifies.
This approach might sound expensive, but libraries like use-sse argue that the second render pass only takes a fraction of the time. This is something that we will experiment with in the future.
Making Content-Powered Jamstack Apps
As a developer, I find it exciting to step into these cutting-edge technologies and face the challenges that come with them. And while we’re still working on finding the best solutions, I feel that we are on the right track and that together with our partners and customers we're making content-powered Jamstack apps work as seamlessly as possible.