Transitioning from Plone to Mezzanine CMS

Posted by: thogan 3 years, 11 months ago

Well, that didn't last long. It was just a month ago that I completed migrating my website off of Wordpress and into Plone. Now I have changed it again. The THogan.com site is now powered by Mezzanine, a CMS project for Django written in Python.

Don't get me wrong, I have used Plone for many documentation projects in the past, and that was one of the reason I went straight to it for a website refresh. Plone is a good CMS, with good content management features such as a clusterable database with full-text indexing and a modular workflow engine.

However, using Plone in the past, we basically never themed it. There is nothing wrong with the theme out of the box for an intranet CMS. Plone theming currently revolves around the use of a tool called Diazo, which is based on the concept of running transforms on completely rendered stock Plone pages. Instead of modifying templates, you create a rules file which describes how to merge the DOM from a Plone page into the DOM from a template page you provide.

I felt like it was rather inefficient to render a complete page with one template then transform it. But it worked and it only took a few days to get my template integrated with Plone.

Plone Custom Theming Compromises

After working on the site migration for a week, I felt like I had my theme only 95% of the way I wanted it. The problem was that since theming was based on content transformation, some of the things I wanted to do were not possible with the DOM I was getting from Plone.

For example, in my Blog list section, there was a summary paragraph under each post's heading in the DOM. And in the Plone generated DOM, this paragraph was within a <dd> tag that had no CSS classes specified. Many other page elements in the Plone DOM, such as the navigation menu, were also <dt> and <dd> tag pairs. Without classes to differentiate these, applying styles was difficult.

Slowly my CSS accumulated rules that were based on deep DOM hierarchies instead of simple and re-usable classes. Take this gem for example:

/* This is the ul that makes up the edit bar pop-up's menu items */
#contentActionMenus li dl dd ul {
    list-style: none;
    text-align: left;
    margin: 0;
    padding: 0;
}

Another weakness I encountered in the DOM manipulation strategy for theming was when I had "special" pages. The site maintenance calendar page for example, is special. It displays a Javascript calendar widget and does some AJAX loading of calendar event data. To solve this in Plone, I could have gone down the route of creating a custom content type, say "AT-SiteMaintenanceCalendar", and building its own TAL template that included the short blurb of Javascript needed to setup the calendar widget.

To me, this was an unacceptable amount of work for a task that could be described as "put a calendar on a page". Also building custom content types and templates was something I had not had to do with Plone before. I gave it a shot. I dug into the Plone development docs for a couple days but it soon became apparant that I would need to come up to speed on a lot of Zope and Plone internals to accomplish this.

In the end I capitulated and allowed another embarassing blemish on my template:

<script type="text/javascript">
    $(document).ready(function() {
        var sm = $('#sitemaintenance');
        if (sm) {
            sm.fullCalendar({
                events: '/gendata/sitemaintenance.json'
            });
        }
    });
</script>

EVERY PAGE would run this Javascript, which checked to see if the container div for the Site Maintenance calendar happened to exist on that page. If it did, then that meant the user was actually on the Site Maintenance page, and the script should setup the calendar.

Looking Beyond Plone

I like Plone, I feel good about storing my content in ZODB, and I was mostly done after having only worked on the website refresh for a week. Even through there were some compromises around styling and a few hacks in the template, it was almost everything I wanted in a reasonable amount of time.

However, there was one more thing I wanted to add. I had written a small application to do password resets for THogan.com LDAP accounts, and I wanted it to be a part of the site. It wasn't enough to share a theme with the site, I wanted it to be a page that the CMS was aware of. I wanted the UI of the password reset application to exist within a CMS page.

This wasn't happening with Plone, not easily. This was what pushed my frustration over the edge and sent me in pursuit of another CMS / Portal / Whatever.

At first I wondered if what I wanted was a portal. I looked at building my site out in GateIn and then Liferay. No, I didn't want a portal, that was too much. Did I want a static site generator like Octopress? No, that wasn't enough.

I wanted something that was a CMS but that had very straight-forward template overriding mechanisms. I wanted it to be hard to break the CMS by modifying the templates. And I wanted to be able to have completely custom pages live alongside those dynamically generated for regular CMS content.

Off on a Tangent About AngularJS

In my opinion, complete client-side Javascript application UI is the pinnacle of client/server separation for web based applications. When I started using AngularJS to build application front-ends, it made me feel good in ways only Qt or Swing could make me feel before. With AngularJS, I can build an application just as quickly as I could with JSF and PrimeFaces or Django; only the whole thing felt neater. A pile of REST endpoints on the server side, all the display logic and templating on the client side.

Writing about how much I like AngularJS and that whole model of development could be its own series of blog posts. Also, you are probably starting to wonder why I'm writing about AngularJS in the middle of an article about content management systems. The point is, when I talk about embedding a custom application into a CMS, I am talking about an AngularJS application.

It seems like it would be so easy. There is no server side rendering for an AngularJS application, just a template that has an "ng-view" div somewhere in it. If there was a way to easily convince the CMS to render that div and load the application's Javascript, embedding a complete web-app into a CMS page would be trivial.

The password reset application I wrote about above is an AngularJS application. I built it with no CSS and intended from the beginning to make it an integrated part of the website.

The Transition to Mezzanine CMS

I was in the throes of desparation over finding a CMS that would provide rich functionality while giving me a good degree of freedom. It should also not be so complicated that the name of the product is a job title. I also wanted to avoid creating the solution myself by building the CMS I wanted from scratch. Though, that idea was becoming very tempting by this point.

I had exhausted my Google searching and read dozens of suggestions from StackOverflow posts. Nothing had grabbed my interest yet. I literally started to brute force my problem. I picked a language I felt was strong and that I knew well enough, Python. Then I started clicking every CMS link on the Python wiki's CMS page.

After days of reading and hundreds of "pip install" commands, I got down to Mezzanine. It was absolutely everything I wanted.

First, it is a Django project, and allows me to add in whatever other Django applications I want. This solved a big chunk of my problem regarding custom code and behavior. Now if I want to store some custom data, or do some custom server side processing, I just create a Django app and add some models and views. This is extra great for creating AngularJS applications, as I can build the server side REST endpoints into a Django application and have the URLs share a namespace with the rest of my Mezzanine content. This is exactly the kind of content + code harmony I was looking for.

Second of all, templating is brilliantly executed. All of the included templates are cut into small Django template blocks, and can easily be removed or changed without breaking the Mezzanine pages. I also blew away all references to the included CSS and Javascript in the base template, and still Mezzanine did not break. This was the first CMS where I felt that I could take my HTML template and add in the CMS bits, rather than starting with the CMS theme and beating it until it looked like my template.

Overriding templates and template-per-page were huge draws for me. If I wanted to customize the blog listing page, it was as easy as copying the included template to my site's templates directory and then editing it:

thogan@webstage1:/apps/thoganmez$ locate '*blog*.html'
/usr/local/lib/python2.7/dist-packages/mezzanine/blog/templates/admin/includes/quick_blog.html
/usr/local/lib/python2.7/dist-packages/mezzanine/blog/templates/blog/blog_post_detail.html
/usr/local/lib/python2.7/dist-packages/mezzanine/blog/templates/blog/blog_post_list.html
/usr/local/lib/python2.7/dist-packages/mezzanine/blog/templates/blog/includes/filter_panel.html
/usr/local/lib/python2.7/dist-packages/mezzanine/mobile/templates/mobile/blog/blog_post_detail.html
/usr/local/lib/python2.7/dist-packages/mezzanine/mobile/templates/mobile/blog/blog_post_list.html
thogan@webstage1:/apps/thoganmez$ cp /usr/local/lib/python2.7/dist-packages/mezzanine/blog/templates/blog/blog_post_detail.html templates/blog/

I couldn't have asked for better than that. In addition to easily overriding templates, creating those templates for those "special" pages I talked about was also a piece of cake. To create a unique template for a single page, I only needed to create an HTML file in the templates directory with a name that matches the special page's slug.

When overriding a template, the result is a very concise expression of what is different about the target page. The method is very clean, straightforward, and easy to implement and test. Let's go back to the example of my Site Maintenance page with the Javascript calendar. For this page, I created a new template: "pages/site-maintenance.html", and a Mezzanine page with "site-maintenance" for the URL slug. Magically, the two find each other and that template is invoked when the user goes to /site-maintenance.

Here is the template for that page, as an example of how consice custom templates can be:

{% overextends "pages/richtextpage.html" %}

{% load staticfiles %}

{% block extra_css %}
{{ block.super }}
<link rel="stylesheet" href="{% static "css/fullcalendar.css" %}">
{% endblock %}

{% block extra_js %}
{{ block.super }}
<script src="{% static "js/fullcalendar.min.js" %}"></script>
{% endblock %}

{% block main %}
{{ block.super }}
<div id="sitemaintenance"></div>

<script type="text/javascript">
    $(document).ready(function() {
        $('#sitemaintenance').fullCalendar({
            events: '/static/gendata/sitemaintenance.json'
        });
    });
</script>
{% endblock %}

It hardly takes much of an understanding of Django templates to see how good this is. There is very little boilerplate, just some block overrides. Now the site maintenance page is the only one that includes the fullcalendar Javascript, CSS, and setup code.

Here I had thought that building my site out in Plone in only a week was good. The whole transition to Mezzanine took a day and and afternoon. I spent all of one Sunday building the templates and getting content moved in from the Plone site. The next day after work I built the production VM, installed PostgreSQL, NGINX, and Gunicorn, then flipped DNS and brought the new site live. Granted I already had experience with Django, but I still am amazed at the 1.5 day transition time.

Integrating an AngularJS Application with Mezzanine

The migration was smooth and I loved the templating system. But what about my password reset appliation? The one the pushed me into looking for a new CMS in the first place?

It turned out to be as easy as I had theorized above. Since now I could very easily affect the DOM generated by the CMS, wiring in the AngularJS based password reset application was trivial.

First off, AngularJS requires that I add an "ng-app" attribute to the <html> tag. Since I want to be able io integrate other AngularJS applications in the future, I had to make the <html> tag an overridable part of the template. I modified the beginning of my base template (templates/base.html) to put the <html> tag in its own Django template block:

<!doctype html>
{% block htmltag %}
<html lang="{{ LANGUAGE_CODE }}"{% if LANGUAGE_BIDI %} dir="rtl"{% endif %}>
{% endblock %}

Again, I created a CMS page and a matching template. In this case the CMS page was given the URL "/wspwr-app" and the template was stored in the file "templates/pages/wspwr-app.html". Now this new template will be used to render the "/wspwr-app" CMS page. In the new template I re-define the "htmltag" block defined in the base template as shown above. The redefined block contains an <html> tag with the appropriate "ng-app" attribute.

After that I just include the Javascript for AngularJS and my application, as well as a div with the "ng-view" attribute. This is all it takes to render the AngularJS application inside of a CMS page that Mezzanine is aware of.

Here is the complete "wspwr-app.html" template:

{% overextends "pages/richtextpage.html" %}

{% load staticfiles %}

{% block htmltag %}
<html lang="{{ LANGUAGE_CODE }}"{% if LANGUAGE_BIDI %} dir="rtl"{% endif %} ng-app="wspwr">
{% endblock %}

{% block extra_js %}
{{ block.super }}
<script type="text/javascript" src="{% static "wspwr/js/angular.min.js" %}"></script>
<script type="text/javascript" src="{% static "wspwr/js/wspwr.js" %}"></script>
{% endblock %}

{% block main %}
<div id="overlayConnecting">
    <h3>Connecting to server...</h3>
</div>
<div ng-view></div>
{% endblock %}

And so I was victorious in my quest. I felt like my site's theme was now 100% the way I wanted it. And to top it off I had a cleanly integrated custom application running within one of my Mezzanine pages.

Here is the password reset application running off my hard drive from within its Git directory:

And here is the same code running live within a Mezzanine page:

I will definetly be turning to Mezzanine first for site building from here on out. And if you have a site to build, check it out for yourself!

Posted by: thogan