Announcing Content-Kit and Mobiledoc
Wow! This takes me back! Please check the date this post was authored, as it may no longer be relevant in a modern context.
Bustle Labs is the tech team behind the editorial staff at Bustle, a fantastic feminist and women’s interest site. Early last year they started work on a WYSIWYG editing surface called Content-Kit. 201 Created has been helping to bring Content-Kit to completion this summer.
Content-Kit is a completely new approach to post and article editing.
- It makes limited use of Content Editable, the siren-song of doomed web editor technologies.
- Content-Kit is designed for rich content. We call these sections of an article “cards,” and implementing a new one doesn’t require an understanding of Content-Kit internals. Adding a new card take an afternoon, not several days.
- Posts are serialized to a JSON payload called Mobiledoc instead of to HTML. Mobiledoc can be rendered for the web, mobile web, or, in theory, on any platform. Mobiledoc is portable and fast.
You can try pre-1.0 Content-Kit right now, or check out the 100% MIT licensed Content-Kit source code. The Mobiledoc DOM renderer and Mobiledoc HTML renderer are available.
Let’s talk about Content-Kit and Mobiledoc, and what remains for them to hit 1.0.
Cards
The implementation details of Content-Kit, or any WYSIWYG editor, are incredibly interesting. But before diving into that discussion, I want to talk about the most important feature of Content-Kit and Mobiledoc.
Modern article or post content has moved beyond text. madhatted.com (this blog) is pretty simple, but a post on Bustle, Verge, or the NYTimes could easily contain several different kinds of sections. For example:
- A full-bleed image and title.
- An introductory paragraph of text.
- A chart providing context for the article.
- Several paragraphs of text.
- A slideshow of a few photographs.
Many web editors render the final version of this content into HTML when persisting. Consequently, adding new kinds of sections means wrestling with complex editor internals. Content-Kit’s cards API is designed to alleviate this burden.
Persisting a rendered version of an article also limits the post from being improved or rendered differently in another environment. For Bustle, there are three main rendering targets to consider. On each, we can support a different level of interactivity.
Desktop Web | Mobile Web | Mobile iOS |
---|---|---|
Full bleed image | Full bleed image | Full bleed image |
Text as HTML | Text as HTML | Text as HTML (rendered in web view) |
Interactive SVG Chart | Static SVG Chart | Image linking to full-screen native interactive |
Inline slideshow | Image with a click opening a full-screen slideshow |
Additionally, the implementation of a slideshow will change over time. Coupling the DOM to any particular version means painful data-migrations are needed for old content.
Content presentation that varies at runtime demands a new persistence format. We’ve created one named Mobiledoc.
Mobiledoc stores post or article content as data, not DOM. For example this Mobiledoc has a headline, chart, and body of text:
{
version: "0.1", ---- Versioning information
sections: [[],[
[1, "h2", [ ----
[[], 0, "Understanding Content-Kit"] | Text section wrapped in an h2
]], ----
[10, "slideshow", [ ----
"pic2.jpg", | Slideshow card, with an array
"pic3.jpg" | payload of images
]], ----
[1, "p", [ ----
[[], 0, "What a nice, short post"] | Text section wrapped in a p
]] ----
]]
}
Importantly, Mobiledoc says nothing about how cards are edited or rendered. Instead, the editor or rendering runtime must be passed a handler conforming to the card API. For example, passing a slideshow display to the DOM renderer:
var SlideshowCard = {
name: 'slideshow',
display: {
setup(element, options, env, payload) {
var img = $('<img style="cursor:pointer">');
var index = 0;
img.
attr('src', payload[index]).
appendTo(element).
click(function(){
index++;
if (!payload[index]) {
index = 0;
}
img.attr('src', payload[index]);
});
}
}
};
var renderer = new MobiledocDOMRenderer();
renderer.render(mobiledoc, $('.post')[0], cards: {
slideshow: slideshow
});
Mobiledoc renderers require cards to expose a setup hook for display cases. Additionally, when a card is passed to an editor instance an edit
hook must be fulfilled. There is an optional teardown hook, and the env
object will
include triggers to moving between edit and preview. One of these, save
, accepts the argument of a data payload to be stored in the Mobiledoc. In our example above, this was the URLs array.
By allowing cards to be implemented at runtime, they can adapt or be swapped out to meet the needs of a given environment. By delegating only the editing and display logic to cards (not core parsing and rendering), Bustle developers can focus on building or enhancing the rich content tools they want to provide to the content team. They are liberated to create rich content experiences with Ember, React, or any other UI library that suits their needs.
We’re excited by the potential of cards and Mobiledoc. Combined with something like Ember or React components, adding new editing and rich display experiences is a snap.
Feel free to follow up by reading some example cards in our demo, the documentation on cards and mobiledoc specs, or continue on to learn about the design decisions behind Content-Kit and Mobiledoc.
Moving away from Content Editable
The pains and limits of using HTML5 Content Editable are well understood by developers building text editing interfaces. The best editing experiences (we love Atom from Github, and Google Docs) avoid Content Editable APIs entirely.
The difficult nature of Content Editable is that it conflates the previewed display of a document with its serialized and persisted form, using DOM for both. Often additional capability (runtime-defined behavior) is awkwardly embedded into DOM, or the natural flexibility of using the DOM to store content (including browser quirks and security issues) must be reined in.
To build a great editing experience, it is clear we need to limit how much Content Editable is used or, ideally, avoid it entirely. By capturing events before they reach the default Content Editable behaviors, Content-Kit currently takes a hybrid approach. In the upcoming weeks, we plan to migrate further away from reliance on Content Editable.
Content-Kit has an internal tree representing a document. The tree looks roughly like:
Posts correspond to an entire article or document. Sections represent top level data. For example, a headline tag would be a section, as would a paragraph tag. A card is also a section. Sections are ordered peers. Content-Kit has additional special section types that could likely become cards, but they also seem common enough to include as built-ins.
In the Content-Kit editing surface, each section has a matching DOM node. Markers do not map to single DOM node, but to a text node with opened and closed tags at its boundaries. Regardless of the depth of DOM nodes being represented by markers, they remain a shallow list.
Content-Kit maintains the mapping of text nodes to markers. When an event happens inside a given DOM node, we can trace which marker is being referenced (if any) by checking that map. With markers and DOM, a simplified graph of Content-Kit’s internal post looks like (white-space added for sanity):
There are three markers in the text section:
- The first marker opens no tags, closes no tags, and has the text content of “A fantastic, ”.
- The second marker opens a bold and italic tag, closes the italic tag, and has the text of “reliable”.
- The third marker opens no tags, closes the bold tag, and has the text “editor.”.
This structuring of markup is wonderfully efficient to render, and takes less space than the corresponding HTML for an article payload. For example, here is the same article as above persisted into Mobiledoc:
{
version: "0.1", ---- Versioning information
sections: [[ ----
["b"], |
["i"] | Markup types. Referenced by markers.
],[ ----
[1, "p", [ ----
[[], 0, "A fantastic, "] | Markup section with a p tag. Markers
[[0,1], 1, "reliable"], | consist of a three markers.
[[], 1, " editor."] |
]] ----
]]
}
Content-Kit stills allows most text editing events to change the Content Editable DOM. During the input
event, targeted subsets of the post content are re-parsed into the abstract post. In the future, keystrokes will be captured and the post can be updated directly, then rerendered. For some kinds of event data, like pressing the enter key, we already do this.
Content-Kit’s rendering system is directly inspired by the architecture of Ember’s Glimmer rendering engine. Render nodes are created during rendering, and manage the relationship of post content to the DOM. They track dirty state, allowing us to quickly render only the part of a post that has changed.
Getting to Content-Kit 1.0
We plan to bring Content-Kit to a 1.0 release in the next few weeks. This will mean the API for cards is stable, the UI is fairly polished and feature complete, and that we’ve integrated the editor into some internal Bustle tools.
The most painful parts of Content Editable will be avoided by 1.0, though we may retain it for very narrow functionality (entering text). Using event capture, making changes directly to the abstract tree and rerendering the post will replace most formatting commands.
Content-Kit is a flexible and powerful editor, and Mobiledoc paired with a generic cards API makes a great publishing tool. If you’re publishing for the web and interested in working with us to advance rich content, please say hello.
Big thanks to Bustle Labs for making Content-Kit and the related libraries open source. We’re having a great summer working with their smart and fun team here in NYC. Bustle is looking for senior software engineers and senior UX designers to join their crew.