Storybook app from Item Consulting

A brand new application called “Storybook” from our friends at Item Consulting is now available for download from Enonic Market. It enables integration of Storybook and Enonic XP to provide previews of Freemarker and Thymeleaf templates.

Enjoy the release and accept wishes of a happy Easter from all of us at Enonic! :hatching_chick: :rabbit:


Hi developer friends!

I am the main developer of this app, and I’m really excited to have it on Enonic Market now. :smiley:

This app has been almost a year in the making, and it has gone trough a lot of iteration to make it as simple as possible for developers to use.

Why use this?

The value proposition of this app is to reduce the feedback loop – while developing server rendered XP-applications – to a few milliseconds. This is compared to several seconds wait time every time you deploy an xp-app locally.

How does it work?

The app exposes an XP-service (storybook-preview) that Storybook (with Storybook Server rendering) can use to render a preview of Freemarker (*.ftl) or Thymeleaf (*.html) templates.

You write a story where the id is the path of the template you want to test (relative to the “resources” directory), and the args is an object that contains all the variables used in the template (sometimes called the model).


This is an example of a story that loads a template-file, and the related css.


// Importing the {*.ftl, *.html,*.css}-files creates a dependency graph and 
// makes storybook/webpack redraw the preview when the file(s) changes.
import id from "./article-header.ftl";
import "./article-header.css";
import type { Meta, StoryObj } from "@itemconsulting/xp-storybook-utils";
import type { FreemarkerParams } from "./article-header.freemarker";

const meta: Meta<FreemarkerParams> = {
  title: "Part/Article Header",
  parameters: {
    server: { id }, // id = site/parts/article-header/article-header.ftl

export default meta;

export const articleHeader: StoryObj<FreemarkerParams> = {
  name: "Article Header",
  args: {
    displayName: "Dette er en typisk tittel på en bloggartikkel",
    intro: "Ingressfelt kan være relevant noen ganger. Det hender at noen nettredaktører skriver en hel artikkel her.",
    tag: "Bloggartikkel",
    publishedDate: "2023-05-23T10:41:37.212Z",


[#-- @ftlvariable name="tag" type="String" --]
[#-- @ftlvariable name="displayName" type="String" --]
[#-- @ftlvariable name="intro" type="String" --]
[#-- @ftlvariable name="locale" type="String" --]
[#-- @ftlvariable name="publishedDate" type="java.time.ZonedDateTime" --]

[#import "/site/views/partials/date/date.ftl" as Date]

<div class="article-header">
  [#if tag?has_content]
    <div class="text-primary">${tag}</div>


  [#if intro?has_content]
    <div class="intro">
      [@processHtml value=intro /]

  [#if publishedDate?has_content]
    <div class="published">
      [@localize key="articleHeader.published" locale=locale /]
      [@Date.shortDate date=publishedDate locale=locale /]

Resulting storybook running on localhost:6006

What happens technically?

  1. The Storybook-app exposes a service called storybook-preview.
  2. Storybook will take the id from the story and append it to the baseUrl and the args become the query params. For the example above Storybook server creates the following HTTP GET (simplified):
    http://localhost:8080/_/service/no.item.storybook/storybook-preview/site/parts/article-header/article-header.ftl?locale=no&matchers={"zonedDateTime":"/Date$/","region":"/Region$/i"}&displayName=Dette er en typisk tittel på en bloggartikkel&intro=Ingressfelt kan være relevant noen ganger. Det hender at noen nettredaktører skriver en hel artikkel her.&tag=Bloggartikkel&publishedDate=2023-05-23T10:41:37.212Z
  3. The XP Storybook-app is configured with the xpResourcesDirPath which points to the resources directory on your local development machine. It can now use to read the template file (xpResourcesDirPath + id) from disk. So now re-renders of the file only take a few milliseconds.
  4. If your Storybook project has the preset-enonic-xp configured, it will enable doing import id from "./myfile.{ftl,html}", because it will configure Storybook with new custom webpack loaders for Freemarker and Thymeleaf. The loaders will create a dependency graph for imports in those template files, and provide HMR when changing any file in the dependency tree.


Check out the documentation in the xp-storybook-utils repo to learn more about how to work with Storybook and XP.

Have a great day all!
– Tom Arild


:warning: Two warnings

  1. Don’t deploy the Storybook-app on your production server! It will allow attackers to render any content on your domain.
  2. Storybook is a NodeJS-application that runs parallel to your XP-environment. This means that you can not share JS-/TS-code between the Storybook- and XP-environments that depends on that specific environment. E.g Storybook will throw up :face_vomiting: if you try to do an import { ... } from "/lib/xp/content" since it is in the NodeJS-environment.

TIP for TypeScript projects: I usually create a file named my-part.freemarker.d.ts or my-part.thymeleaf.d.ts that defines the shape of the model that the template file expects. I import this shape into both my XP and Storybook/NodeJS-environments.

If I want use TypeScript-shapes from XP core, I do the imports directly from the npm-packages. E.g. import type { Region } from "@enonic-types/core";

The *.freemarker.ts file for my example in the previous post would be something like this:


import type { ZonedDateTime } from "@item-enonic-types/lib-time";

export type FreemarkerParams = {
  displayName: string;
  intro?: string;
  tag?: string;
  publishedDate?: ZonedDateTime;
  locale: string;

Great work Tom! :partying_face:
I notice your security warning. With enonic CLI 3 (launching soon), sandboxes will run in --dev mode by default.

Maybe adding a check for xp running in dev mode would be a nice security improvement - effectively preventing the app from working in a prod environment?

1 Like

Yes! This is an excellent suggestion! I will definitely add this! :star_struck:

1 Like