Skip to content

Modular setup

Gustav Burchardt edited this page Sep 24, 2019 · 11 revisions

This page describes how the app is structured, how it builds, how DatoCMS is set up, how to add content and how to add blocks.

The goal of this page is also to describe the idea behind the setup, so other apps can mirror the setup. This section fulfills this goal.

Modular Blocks

Dato has been set up in such a way that pages and blocks can be created seamlessly from the CMS without altering any code. The app just knows how to render a page, which consists of some blocks, but the code does not know beforehand which blocks and what content will be rendered. Therefore the code is completely generalized and can receive any pages and any layouts, as long as they follow the spec. The code will fail, if new fields are added or removed, without reflecting those changes in the code base.

Because of the way KvalifikDK is designed visually, blocks are easily rendered adjacently, so the blocks can be thought of as an array (and is an array):

page.pageSetup = [
  HeaderBlock,
  CaseGrid,
  ActionBlock
  ...
]

So a Page has the following model:

page {
  title: String
  url: String
  pageSetup: Array of Blocks
}

Naturally, other apps might associate more fields with a page, such as a background color or a display width, or whether to show the footer, but this model would appear to be the bare minimum in order to build the pages programmatically.

Build the pages programmatically

As the previous section described, pages are made of blocks, which can render any content, but needs to be set up in both Dato and in the case base. This section describes the build pipeline for the pages.

Step 1: gatsby-node.js

In gatsby-node.js we have the ability to alter the page building process. This file contains api functions, that Gatsby will call while building. In our case, it is sufficient to provide a createPages function that will be called while creating the routes.

We load all pages with

const data = graphql(`
  {
    allDatoCmsPage {
      edges {
        node {
          url
          title
        }
      }
    }
  }
`)

Similarly, work pages are loaded in the same file.

This gives us a data variable containing each page's url and title; enough to build the routes. Afterwards we use this information to call actions.createPage, a function that Gatsby provides to create a new page, and provide the path, the template.

Importantly, we also provide a context (in this case the url, because it is unique). The context is available in each template, eg. templates/page.js, so the page template knows which page to build.

Step 2: The page template

The page template is responsible for building the page. This can be thought of as the App component in a normal React app, but here we use a Page template for each Route. Remember, Gatsby will render a complete html file for each route, so the site becomes static.

The page template (templates/page.js) loads the actual page content:

query($url: String!) {
  datoCmsPage(url: { eq: $url }) {
    title
    url
    bgColor {
      hex
    }
    pageSetup {
      __typename
      ...HeaderFragment
      ...ActionBlockFragment
      [...]
    }
  }
}

Remember the url parameter that was sent along via context? This is the $url param that this query uses. Firstly, it is declared in the query that this parameter will be available, and is expected to be a String. Secondly, the datoCmsPage (the formal type associated with the Page) is required to have a url parameter that equals $url. This way the page gets exactly the page that we wanted it to build. Take note of ...HeaderFragment and ...ActionBlockFragment - we'll get back to these shortly.

The Page component is what you would expect from a React app. However, take note of pageSetup.map(renderBlockType). This function, renderBlockType, is very important. We'll get back to this in a moment.

Note: The work pages are rendered with their own template, see templates/work.js. However, the setup is similar.

Step 3: Page fragment

We have discovered that the Page template depends on various Fragments. Take a look at src/utils/blockQuery.js.

Here we find a big fat graphql query. Each block's query is defined within this one. You should take a look at Fragments if you haven't used fragments before, but in short each fragment is given a name, and a type:

fragment HeaderFragment on DatoCmsHeader

  • fragment: Keyword declaring a fragment
  • HeaderFragment: Name for future reference
  • on: Keyword indicating that a type will follow
  • DatoCmsHeader: The formal type associated with the Header Block

Each fragment also has a body, which defines which fields are to be loaded, when getting a block of that type.

The final query is assembled in each template file, see templates/.

Step 4: Render block types

When the content for the page is loaded, each block type should be rendered. Take a look at src/utils/renderBlockType.js.

This function is in charge of rendering each block type. It simply switches on __typename to render each block type. This function is allowed to map data to better match the API provided by each block component.

Step 5: Complete

This is how the app renders each page programmatically. Of course there's a lot of code in each component, but that is actually irrelevant for the build pipeline itself. Each component can do whatever it likes, such as declare a ephemeral state (simply a local component state), fetch new content with a StaticQuery, fetch runtime only content from a 3rd party service, or something else entirely.

How to add a new block?

To add a new block, you need to touch DatoCMS, orderQuery.js, renderBlockType.js. This section assumes that you have created a file to contain the component for your new block. It should be placed in src/Components/YourComponentName[.js].

Note: Remember that your changes in Dato might propagate into production, so they mustn't break the live app. This means you should be very careful when altering production pages, removing any fields that are not within you own block, or altering content that is in production.

Step 1: Update Dato page model

Navigate to Dato CMS and login. Under [Settings], find the [Page] or the [Work] model. You will see a field named Page Setup, which contains definitions for all blocks. Select [+ New Block] and define your block type. Choose your fields. If you need to create a list of items, you could create a new multi-instance model and link it to that block. However, try to limit the amount of models you create, as it pollutes the settings panel, and eventually costs money (because Dato Cms allows no more than 16 models on their free tier).

Take note of the Model ID of your new block type. If your Model ID is "awesome_block", the type name will be "DatoCmsAwesomeBlock". The type name is required in the next section.

Step 2: Update the content query

Go to src/utils/blockQuery.js and declare your type below all other types. Follow correct syntax (refer to the previous section) and declare your block type fields.

Remember to include it in Page.propTypes inside templates/page.js and templates/work.js.

Step 3: Update renderBlockType

Open src/utils/renderBlockType.js.

Import your component entry point and add a switch case that matches your block's type name and returns a node (ie. a rendered component). Fields from Dato Cms are found in the block object, that is fed to renderBlockType. So if your block has a field called title, you can find this at block.title.

You're allowed to map/rename/change fields from your block (ex. a Dato color is an object, but you might want to only pass down the color.hex property. However renderBlockType should be a pure function, so no side-effects are allowed. Don't fetch content or edit fields directly on the block object.

Step 4: Implement your block

Now go! Fly! Your block should be working. Try adding a new block instance from [Dato > Content > Pages > {some page}].

Note: Remember that your changes in Dato are in production, so they mustn't break the live app. This means, don't alter production pages, don't remove any fields that are not within you own block, don't alter content that is in production.