Wagtail 1.0 (beta) best Django CMS?
Now that the Wagtail CMS is gearing up for its 1.0 release, I wanted to take some time to introduce you to the all around best and most flexible Django CMS currently available. Wagtail has been around for a while, but doesn’t seem to get the attention I believe it deserves.
We’ve used Wagtail recently on a number of projects, and the overall experience has been great. It strikes the right balance of making the easy things easy and the hard things not only possible, but relatively easy as well.
- Non-technical end-user ease. Custom admin with great UI/UX
- Plays nicely alongside any other Django apps on your site
- Easy admin customization and branding
- Flexibility of CMS models for more structured data beyond just having a “Page”
- Built in Form builder system
- Great Image and Document/File support and UI
- StreamField for ultimate flexibility allowing you to define and organize small blocks of content
- Ability to organize admin tabs and field layouts
- Control/Flexibility of what page models can be added below certain URLs
- Hooks into ElasticSearch for searching
- Compatible with Varnish and static site generators to help with performance at scale
Let’s face it, the Django admin leaves a lot to be desired. It’s very CRUD-oriented and confusing for all but the most technical of users. Even giving it a facelift with packages like Django Suit, or swapping it out entirely for something like Grappelli isn’t really what your end users want. Don’t get me wrong: both of these packages are great and you should check out, but they both simply can’t get past all of the hurdles and pain that come with attempting to customize the Django admin beyond a certain point.
Wagtail comes out of the box with it’s own custom admin interface that is specifically geared toward a typical CMS workflow. Check out this great promo video about Wagtail and you’ll see what I mean. No seriously, go watch it. I’ll wait.
Isn’t that great looking? My first thought when seeing the Wagtail video for the first time was “Nice, but I bet customizing it is a huge pain in the…”. Thankfully, I gave it a whirl anyway and came to find that customizing the Wagtail admin is actually pretty simple.
There is a great editor’s guide in the docs that is all most end users need to get started. So far in our use, the only thing that confuses users is the Explorer, Root pages, and the hierarchical nature of pages in general. Even those are small issues as one quick chat with the user and they grok it and are on their way.
Oh and a huge bonus the admin is surprisingly usable on both mobile and tablets!
Ways to customize the Wagtail Admin
There are a few ways you can customize the admin. First off, you can determine what fields are visible to your users and on what tab of the interface with just a bit of configuration. Consider this the bare bones entry level of “customization” you’ll be doing.
Customizing the branding of the admin is also a very frequent need. Techies often don’t see the point, but if you can put on your end user hat for a moment it seems weird and often confusing to come to a login page for www.revsys.com that reads “Welcome to the Wagtail CMS Admin”.
For me, these two customization options are what I expect from a CMS. However, Wagtail goes a step further and gives you hooks to allow for much richer customizations. You can do things like:
- Add items to the Wagtail User Bar that appears for logged in users on the right side of the page much like Django Debug Toolbar
- Add or remove panels from the main Wagtail admin homepage
- Add or remove summary items (Pages, Documents, Images, etc) from the homepage
- Use hooks for taking behind the scenes actions or if you want your own customized Responses after creating, editing, or deleting at Page
- Add your own admin menu items, which can go to any Django views or offsite URLs you desire.
I used that last ability to add admin menu items with great success on TEDxLawrence.com. We needed a way for our Speaker Committee to view the speaker submissions, vote, and make comments. Instead of attempting to shoe horn all of this into a Django Admin or even Wagtail Admin universe, I simply linked off to entirely customized Class Based Views to give me complete end to end control.
Most content management systems operate around the concept of a page that usually has a title, some sort of short description of the page, and then the page content itself. Many give you nice WYSIWYG editing tools to make things like adding headers, lists, bold, and italics relatively easy.
With Wagtail you build your own models that inherit from it’s Page model. This gives you the ability to customize specific fields for specific data and ultimately removes a lot of the usual shenanigans one goes through to fit your data concepts into your CMS’ vision of the world.
This probably works best as an example. Let’s build two different types of pages. A simple blog type page and a more complex Staff Member page one might use for individual staff members.
Our simple page can look like this:
from django.db import models from wagtail.wagtailcore.models import Page from wagtail.wagtailcore.fields import RichTextField from wagtail.wagtailadmin.edit_handlers import FieldPanel class BlogPage(Page): sub_title = models.CharField(max_length=500, blank=True) published = models.DateField() author = models.CharField(max_length=100) summary = models.RichTextField(blank=True) body = models.RichTextField() closing_content = models.RichTextField(blank=True) content_panels = [ FieldPanel(‘title’), FieldPanel(‘sub_title’), FieldPanel(‘published’), FieldPanel(‘author’), FieldPanel(‘summary’), FieldPanel(‘body’), FieldPanel(‘closing_content’) ]
Wagtail automatically sets up some fields for you, like title, the slug of the page, start/end visibility times, and SEO/meta related fields so you just need to define the fields you want beyond those.
Here we’ve defined some additional structured information we want on a blog post. A possible sub_title and summary information, an author, the date the entry was published, and the usual body field. We’ve also added an additional closing_content field we might use for a ending call to action or other content that we want highlighted and shown below the post.
All you need to do is add this to a Django app’s models.py, run makemigrations and migrate and you’re good to go.
Now let’s make a slightly more complicated Staff Page:
DEPARTMENT_CHOICES = ( (‘admin’, ‘Administration’), (‘accounting’, ‘Accounting’), (‘marketing’, ‘Marketing’), (‘sales’, ‘Sales’), (‘engineer’, ‘Engineering’), ) class StaffPage(Page): first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50) active = models.BooleanField(default=True) start_date = models.DateField() end_date = models.DateField(blank=True, null=True) department = models.CharField(max_length=50, choices=DEPARTMENT_CHOICES) email = models.EmailField() twitter = models.CharField(max_length=50) short_bio = models.RichTextField(blank=True) bio = models.RichTextField(blank=True) education = models.RichTextField(blank=True) work_history = models.RichTextField(blank=True) # Panel options left out for brevity
As you can see the StaffPage model has quite a bit more fields, most of them optional, which allows the Staff member to update their information over time and not get strangled into putting ‘Bio coming soon’ into an otherwise required field.
Pretty simple right? You’re probably thinking there is some catch, luckily you’re wrong. It’s really pretty much that simple. Easy things easy right?
Harder things in Wagtail
So what are the hard things in Wagtail? Well it’s mostly just getting familiar with the system in general. A few things that may trip you up are:
- You can’t have a field named url on your derived models as Wagtail uses that field name in the parent Page model. Unfortunately if you do add one, which I’ve done more times than I care to admit, you get a not very useful error “can’t set attribute” and not much else to go on.
- On many listing type pages it’s fine to simply show all of the items, with pagination, in some sort of chronological order. Other times users want to be able to manually curate what shows up on given pages. Wagtail makes this relatively easy as you can define a ForeignKey relationship using a through type model to other pages and use a PageChoosePanel to give the user a nice interface for doing this. The user can also manually order them right in the admin, no additional work necessary.
- Limiting which pages can be created as children (aka beneath) a page can be handled by setting a list named parent_page_types on the child model. Then it can only be added below pages of those defined types. On complex sites with lots of different page types this helps keep the Page Choosing and Creation option choices to a manageable level for the users. And it also obviously helps to keep users from creating the wrong types of pages in the wrong parts of the site.
- Wagtail currently doesn’t have a great story around building navigation menus for you, but there are a dozen reusable Django apps to help handle that. Often a site’s menu remains relatively static and isn’t edited day-to-day anyway.
- Supporting multiple sites with the same CMS. This isn’t technically hard, but more conceptually difficult to grok. Wagtail supports having multiple sites, via it’s wagtailsites app. The way this works is you simply set the Root page for each hostname and it basically takes it from there. However, in most circumstances it’s probably easier and cleaner to just have two different instances of Wagtail and use different databases.
Images and Documents
Documents are files of any type you want to be able to upload into the system. This handles any sort of situations where a user needs to upload a PDF, Excel, or Word document and be able to link to it from any other bit of content.
Images are exactly what you think, however you can define your own base model for this if you choose and attach additional fields for things like copyright, license, attribution, or even something like EXIF data if you wanted.
With both Documents and Images having tagging support via django-taggit and a really nicely designed look and UX for them in the admin interface.
And yes, before you ask, it has built in support for flexible thumbnails in your templates andthe ability for you to mainually define the main focal point in the image to avoid cropping things weirdly.
Form Builder interface
Wagtail also has a nice form builder built into it that can easily suffice for your typical contact form scenarios or more complicated collection needs.
Much like Pages, you simply subclass from Wagtail and define what fields you want to collect. On your model you can also override the process_form_submission method to do more complex validation or in a more common case to email the interested parties that there is a new submission.
One great feature of the form builder that is also built in, is the viewing and downloading interface. Viewing the data that has come in is great, but you just know your users are going to want to pull it out and use it for some other purpose. Wagtail smartly anticipates this and allows the user to download the submitted data, by date range, as a CSV file anytime they want.
Wagtail Snippets are reusable bits of content that aren’t full web pages. Often these are used for things like sidebar content, advertisements, or calls to action.
Unlike with Pages or Forms, you don’t subclass a model but instead define a model and simply register it as a snippet. You’re then free to give your users the option of attaching snippets of the types you want to other pages. Or if you just want to give them the ability to edit things like footer content for example, you can just manually include the snippet data and the Snippet admin UI is really just becomes their editing interface.
Best Feature? Content Streams
While being able to define your own Page types with their own fields goes a long way, it’s quite a stretch from truly free form content. New in version 1.0 is Wagtail’s killer feature the StreamField.
Users want free form content while developers, designers, and even ops want nicely structured data. StreamFields satisfies both camps.
When you define a StreamField on a Page you set what types of blocks are available to be added into that stream. A block can be something as simple as a CharField of text or as complicated as a Staff type record like above usingstructural block types.
The end user can then add say few headers of various types, some rich text content blocks and have it all interspersed with a few images and code blocks. Each of these block types you define can then be styled differently CSS and/or have their markup structured entirely differently if needed.
Prior to this feature being added to 1.0, I had to resort to complicated Page relationships that weren’t actually pages we intended to make visible on the site. We just subverted the Page Choosing features of Wagtail to give the users the flexibility they needed and keep it all in the same admin interface.
Here is what the admin UI looks like for StreamFields. Here we've defined a field named Body that has header, content, and code block types. Each of these lines are different blocks. The top and bottom being headers. As you can see you can simply click the plus icons to add new blocks in between others or use the arrows on the right to move block around. They are currently a bit hard to see due to a CSS bug I anticipate being fixed quickly.
I think Wagtail has a VERY bright future in general and especially inside the Django community. However, like any 1.0 product there are definitely some things I would like to see in future versions. The two main things I hope to see are:
- A community collection of high quality and flexible common Page and Block types to make most sites more of a Lego exercise than a coding one.
- The ability to more easily customize and control the Publish/Moderate/Save as Draft options that appear at the bottom of the screen while editing content. On many smaller sites or those with a flat workflow it should be trivial to make ‘Publish’ or ‘Submit for Moderation’ be the default action presented to the user.