Server Side Rendering

In this document, we describe the flow of control when malibu gets a request for a page.

Server Side Architecture

Before the Request Starts

When a malibu app starts up, the first thing it does is to load a Config object into memory. This config is passed into every subsequent function.


The first step is for the HTTP request to be matched against a set of known routes. The set of isomorphic routes is generated by calling generateRoutes(config), and can be found in app/server/routes.js.

generateRoutes returns an array of routes, with each route looking something like this:

  path: "/my-route/:routeParam",
  pageType: "home-page",
  exact: true,
  params: { foo: "bar" }

The @quintype/framework exports the function generateCommonRoutes which can be used to quickly generate routes for common pages such as the homepage, section pages and story pages, but you are also free to add your own routes.

Each route is matched in order, and then the corresponding pageType and params object is passed to the loadData function. The params object is constructed from the params specified in the route, the url params and the query params.

See Routing Caveats for more details on non-isomorphic routes.


loadData is an asynchronous function that fetches data for a given pageType and params. It is used both in the server side rendering flow, and is also exposed via /route-data.json to the client side flow. loadData can be found at app/server/load-data.js.

It is expected that loadData will use @quintype/backend to make API calls to the server. For example, the crux of the story page might have something like this:

const story = await Story.getStoryBySlug(client, params.slug)

Malibu already implements some basic data loading for various pages, but it’s pretty easy to extend.

See loadData Response Formats for more details on the various formats of the response.


The @quintype/seo library accepts the data loaded from loadData, and returns a set of HTML tags to be placed in the head of the page.

The @quintype/seo package does make some assumptions on how the data returned from loadData is structured. For a “story-page”, it is expected that data.story will contain the story. Similarly for “home-page” and “section-page”, a data.collection must be present.

Please see the @quintype/seo documentation for more details.


pickComponent is the entry point into page rendering. The pickComponent function accepts a pageType, and returns a Component for that page. pickComponent has been made asynchronous in order to allow for splitting your javascript into multiple chunks. pickComponent can be found at app/isomorphic/pick-component.js.

An example of an implementation with chunking is below.

import { PAGE_TYPE } from "./constants";
import { pickComponentHelper } from "@quintype/framework/server/pick-component-helper";

const { pickComponent, getChunkName } = pickComponentHelper(
    [PAGE_TYPE.HOME_PAGE]: { chunk: "list", component: "HomePage" },
    [PAGE_TYPE.SECTION_PAGE]: { chunk: "list", component: "SectionPage" },
    [PAGE_TYPE.STORY_PAGE]: { chunk: "story", component: "StoryPage" },
    default: { chunk: "list", component: "NotFoundPage" }
    list: () => import(/* webpackChunkName: "list" */ "./component-bundles/list.js"),
    story: () => import(/* webpackChunkName: "story" */ "./component-bundles/story.js")

export { pickComponent, getChunkName };

Here we can see two chunks created, list and story. Depending on the pageType, pickComponent will load the correct chunk and render.


Rendering in malibu happens with the help of IsomorphicComponent. IsomorphicComponent recieves the data loaded by loadData, wrapped inside a redux store. This store is then passed to the component that was returned by pickComponent. If the data in the store changes (either within the page or due to a navigation to another page), then IsomorphicComponent ensures that the underlying component updates correctly.


renderLayout renders the page given the pageType and the data loaded in loadData. It can be found in app/server/handlers/render-layout.js, and renders the layout in views/pages/layout.ejs.

renderLayout is called by @quintype/framework with the following params

  • - A redux store containing the data returned by loadData
  • params.content - The html content that has been rendered. See pickComponent and render below for details on how this is built.
  • params.seoTags - The seo tags that have been generated. See @quintype/seo below.

Although the main content of the page has already been rendered, it is possible to render and hydrate additional components, such as a Header, or BreakingNews.

renderLayout will finally return the generated HTML page to browser, via the CDN.

Advanced Topics

While the document above contains most of the information you’d need for the majority of normal development, here are a few more in depth topics.

Routing Caveats

While the routing section in this document deals with the isomorphic routes, a route in malibu can come into one of three categories

  • A route to be forwarded upstream to quintype’s API server. This might be for features which are provided by Quintype’s CMS, such as AMP pages, IA feed, or stories.rss. See upstreamQuintypeRoutes documentation for more information.
  • A route which is handled by express. This is for specific routes such as /manifest.json, /mobile-data.json. See app/server/app.js in your project for more information.
  • An isomorphic route, which is handled as described.

loadData Response Formats

The response from loadData should typically be a promise that resolves to something like the following

  httpStatusCode: 200,
  pageType: 'story-page',
  data: {
    story: {...},
    dataForHeader: {...},
    cacheKeys: ["s/<pub-id>/<story-id>"]
  config: pick(config.asJson(), WHITELIST_CONFIG_KEYS),

However, there are many special cases that can also be handled by this response format.


If the httpStatusCode is set to 301 or 302, then the server will do a redirect, and the browser will navigate with window.location.

  httpStatusCode: 301,
  location: ""

Skipping the Route (404)

In case you want to say that the current route is not found, then just return next().