Developing with the stories app

Published on September 26, 2017 by Christopher Zimmermann

You've heard the buzz, now how do you actually use the new Stories app in your projects? You can get the in-depth details in the docs, but this blog post will give developers a nice practical introduction, including some code samples and working demos.

Stories-app-samples git repository

First things first: the heart of the new feature is a new sub-app for editing content called (funnily enough) the "content editor". What is the fundamental difference between this new sub-app and the "form editor" sub-app used in the content apps such as Contacts or Tours? In addition to a form with fixed set of fields, the content editor also has a new "blocks" section where an author can add as many blocks as they want or need. You can use the form editor or the content editor in any content app. The content editor is just as customizable as the form editor. 

So what is the "Stories app" then? We wanted to provide an app that you could use directly out-of-the-box on your projects. The Stories app uses the new content editor pre-configured with a common set of fields like title, lead visual and author. You can use the Stories app as-is, or you can customize it, or don't use it at all and create your own custom content-editor app from scratch. In this article, I'll just use the name "Stories app", but I mean the content editor as well.


The power of blocks

Blocks will typically store what would have been stored in a single "Body" field via a text field or rich text editor (RTE) in a normal form editor. From an authoring perspective, blocks give an author more comfort and creative freedom when creating content. A block can supply anything (Create your own block). From the technical perspective of  "content storage", a clean set of blocks makes for great flexibility in presentation and in content-reuse in comparison to a single big blob of html from an RTE. For example, a template could display only the first N blocks, or only the blocks of a certain type. A template could easily intersperse advertisements or other content between blocks. Developers can display the blocks with totally different presentation technologies depending on the device or context. 

Magnolia's stock travel demo contains two examples of this. On the Stories page, the "Discover" section displays an "image universe" consisting of all of the image blocks from all of the stories. The story pages themselves demonstrate how blocks can be rendered in creative ways. A custom "date block" is not displayed at all, but when a site visitor scrolls past a date block, the clock on the left-hand side updates to that date.


Now you've got three editors to choose from

In some ways, the Stories app is a hybrid of the Pages app and the form-editor based content apps. With the Stories app, an author has the flexibility of the Pages app in that they can add as many blocks as they like and whichever ones they want. At the same time, an author has the simple and streamlined authoring experience of a content app.


1. Pages app

Author has full control, with the price of increased complexity of the UX. Pages mix content and presentation, making it more difficult to re-use the content.

Use cases: Web pages, Landing pages


2. Stories app (content editor)

Author has a lot of creative control. They also get a great simple UX and fast publishing.

Use cases: Media and news, Landing pages, Rich product pages, Content collections


3. Content app (form editor)

Good for managing content which has a strict set of properties.

Use cases: Product catalog, Store locations, Categories and taxonomies


Templating with the Stories app 

OK, enough blah-blah already, let's get to the code! Content from the Stories app (or other content-editor apps) can be used within your server-side templates within the Pages app - or directly via REST in a "headless CMS" scenario. I'll cover use in the Pages app first.

You'll be right at home writing templates for the Stories app content; all the key principles are the same. In fact, the only unique thing is working with the blocks, which is also easy. I've created some very simple templates specifically for learning. I encourage you to download the demos and follow along. Note that the Stories app is an Enterprise Pro feature, so you will need an enterprise license to try the feature. If you don't have one, you can get a free trial.

There are four page templates:

  • A template which lists the stories (storyList.ftl)
  • A template to display one story (storyDetail.ftl)
  • Another template to display the story in a different way to demonstate some techniques for working with blocks (storyDetailTextOnly.ftl)
  • And a template which delivers the content as JSON and can be used as a REST endpoint (storyEndpoint.ftl)

There's also a simple front-end client which consumes content from the endpoint and displays it. (headless.html)


Listing stories

This demo leverages the content from Magnolia's standard travel demo. In Magnolia, in the Stories app, three stories are included under a "/stories-demos" folder. Have a look at the storyList.ftl template. I can grab a list of the stories with the following freemarker. (/templates/pages/storyList.ftl)


[#assign rootContent = cmsfn.contentByPath("/stories-demo", "stories")]
[#assign stories = cmsfn.children(rootContent, "mgnl:composition")]


Then, I can list the stories as I would any content.


[#list stories as story]
  <div class="story">
    <a href="${storyDetailLink(content, story)!"#"}"><h2>${story.title}</h2></a>


The storyDetailLink function generates a link to the detail page. We could have simply used a hard-coded path to the StoryDetail page, but this function makes the link more robust, because it will continue to work no matter where in the page hierarchy an author places the page with the StoryDetail template. The function puts the name of the story node in the url as a selector (the content after the '~' character in the url). Instead of using a selector, you could simply use a query parameter. (Note: By using Magnolia's url mapping features, you could also use a simple url path as is done for the tours in the travel demo.)


Displaying blocks

The /templates/pages/storyDetail.ftl page template renders a story. It determines which story to display based on the value of the selector in the url.

It displays the content from the outline of the story as you would from any content app. I can easily grab properties like title from my content node.




Blocks can easily be rendered using the new @cms.block directive.


[#assign blocks = cmsfn.children(story, "mgnl:block") /]
[#list blocks as block]
  [@cms.block content=block /]


Each block has its own default template which will be used to render the provided content. 


Block flexibility

What about all those claims I made above about how flexible blocks are? I created another template which demonstrates this: /templates/pages/storyDetailTextOnly.ftl. It only displays text. One could imagine something like this for a news feed or text-only alert service.


[#assign blocks = cmsfn.children(story, "mgnl:block") /]
[#list blocks as block]
  [#if block["mgnl:type"] == "text"]

    [@cms.block content=block /]

  [#elseIf block["mgnl:type"] == "image"]

    <div class="image-caption-solo">



First of all, instead of rendering all of the blocks, it only renders text blocks and image blocks. For text blocks, it just uses the default template. But for the image blocks, notice how it supplies a custom template script which includes only the image caption, not the image itself.


Stories for front-end frameworks and headless CMS scenarios

As the Stories app is so great for authoring content for headless usage with its unique combination of flexible, yet structured content - I wanted to show you a simple example of that as well.


Stories as JSON

There are several ways to expose content as JSON in Magnolia. For this demo, I've decided to use the jsonfn module in a template: /templates/pages/storyEndpoint.ftl. If you request the page with no parameters, it returns an array of stories. If you call it with the path of a story as a querystring parameter, then it will return that story including all its blocks. It's pretty easy. I just did it like this:


[#assign blocks = cmsfn.children(story, "mgnl:block")]

   ${jsonfn.fromChildNodesOf(story).addAll().exclude("jcr.*").expand("image", "dam").print()}


If you want to see what the output looks like, have a glance at a file I created for testing: /webresources/headless-test-data.js


My super-fancy front-end app

In a true headless scenario, your front-end app will run on another server, but for this demo, I just created a single html page in the light module: /webresources/headless.html

It hits the story endpoint mentioned above to load and display a list of the stories. Clicking on one of them then loads the full story (including the blocks) from the endpoint and displays it.

In the front-end app, we work with the pure content. Magnolia does not supply the templates, the app does. How this is done is of course dependent on your front-end technology, but here is the simple example provided in this demo which illustrates the basic approach of a switch, or chained if-blocks


Handlebars.registerHelper('storyBlock', function(block) {
  var type = block["mgnl:type"];
  var result="";

  if (type=="text"){
    result = block.text;

  }else if (type=="externalLink"){
    result = block.url
    result = "<a href='" + block.url + "'>" + block.url + "</a>";

  }else if (type=="image"){
    var path = block.image['@path'];
    result = "<img src='" + IMAGE_BASE + path + IMAGE_SUFFIX_RAW + "'>";
    result += "<div class='caption'>" + block.imageCaption + "</div>"

  return new Handlebars.SafeString(result);


What's your story?

I hope this blog post and sample project gives you a nice little kickstart on your project - or maybe inspires you to try developing with the Stories app. I'm really looking forward to seeing what you and the Magnolia community create with the stories app, and of course to hear your stories.


{{item.userId}}   {{item.timestamp | timestampToDate}}

About the author Christopher Zimmermann

Christopher is a software developer and startup enthusiast with a focus on front-end web technologies. His interest in innovative UI leads to diverse experimentation from phone based GPS services, to Oculus Rift immersive experiences. He organizes the 'basel.js' javascript meetup group. Christopher is a product manager at Magnolia.

See all posts on Christopher Zimmermann