How we extended django CMS to create a single-page application

Creating a rich user experience is one of the most important thing for us as interaction designers when creating apps and websites. Changing pages on a website usually uses a full page reload. That comes with a very short flickering as the whole DOM will be completely rebuilt and rendered.

Christian Schürmann
dreipol

--

We wanted to improve the user experience of dreipol.ch by replacing full page reloads with animated page transitions. This is usually not a big deal if you have some static pages. Using a content management system like django CMS, page animations are a bit harder to achieve. Especially because template rendering is very deep in the core of it.

Our approach to decouple the rendering was to split the work between storing and rendering data. The server is restricted to render a HTML template that only contains the necessary metadata in the head of the document. The actual content of a CMS page or view is passed as a JSON variable into the HTML document. Contents of other pages are requested asynchronously and delivered as JSON. The rendering of the contents is completely handled by the client. We are using vue.js to render frontend components. The routing is handled by the vue-router, another vue.js component.

The vue-router object

In a classic django CMS project all valid URLs are configured as CMS pages or URL patterns on the server. Single-page applications with vue-router need all URLs as a list of routes on the client side. The easiest way of getting that list is by iterating over the nodes of the CMS menu. All custom URLs like the ones of your apphooks need to be attached to the menu. With the help of a template tag from our package djangocms-spa-vue-js the vue-router object is rendered as JSON into your template.

{
"routes": [
{
"name": "cms-page-1",
"path": "/",
"component": "index",
"api": {
"fetch": "/api/en/pages/"
}
},
{
"name": "cms-page-2",
"path": "/work",
"component": "work",
"api": {
"fetch": "/api/en/pages/work/"
}
}
]
}

The REST-API

We implemented a REST-API that returns all data of CMS pages. A page consists of a title, metadata and placeholders. The serialisation of metadata is straightforward. Parsing placeholders and all its plugins manually seemed like a lot of work. We expected some tricky performance problems. Digging into those core objects showed us that rendering JSON is much faster than a HTML template. To make the contents of your CMS pages available asynchronously you have to include an url pattern that our package provides. You are responsible to implement API data endpoints for your custom views. We help you out with some base classes. Check out the documentation of djangocms-spa-vue-js for some examples. To reduce the amounts of requests we render the data of the current page directly into the router object.

{
"data": {
"containers": {
"main": {
"plugins": [
{
"content": {
"text": "Hello world"
},
"type": "cmp-text"
}
],
"type": "cmp-main"
}
}
}
}

CMS plugins

Probably the biggest difference in the workflow is the rendering of plugins. Templates are no longer needed. The only thing you need to do is implementing a render_spa method that returns a dictionary:

from djangocms_spa.cms_plugins import SPAPluginBaseclass TextPlugin(JsonOnlyPluginBase):
name = _('Text')
model = TextPluginModel
frontend_component_name = 'cmp-text'

def render_spa(self, request, context, instance):
context = super(TextPlugin, self).render_spa(request, context, instance)
context['content']['text']. = instance.text
return context

plugin_pool.register_plugin(TextPlugin)

Conclusion

The combination of the best and strongest parts from all technologies offered us a new way of modern web development. While we lost all of the powerful template rendering features of Django and django CMS, the decoupling of frontend and backend felt immediately right. The role of a CMS plugin is limited to access the data and passing it in a serialized and structured form to the frontend. With some helpers that render the menu and the content directly into the page of the first request, the frontend team can now work independently from our backend.

The first project that we realised with this approach was our very own agency website dreipol.ch. Two weeks before going live vue.js published version 2 and dropped support for previous versions. That made us realise once again that frontend frameworks come and go all the time. We decided to split django CMS features from JS framework related code and we ended up in two packages (djangocms-spa & djangocms-spa-vue-js).

In the next post, we will explain how we handled the frontend editing. Stay tuned.

. . .

Feel free to like this post, share it, follow me or dreipol on Social Media:

medium.com/@christian.schuermann
twitter.com/dreipol

--

--