Static CMS Concept

jamstack

#1

The Static CMS Concept was one of the first things I wanted to explore when taking over Forge and Hammer. I’d been following the work of the team at Carrot who’d been exploring this and a number of the Static Tech aficionados.

Back in September, I published this post on the Beach blog, which kickstarted our exploration in this area.

From there, we started working with the team at Contentful on integration between Contentful and Hammer. This was also the catalyst for a number of other Hammer updates, including Slim support and the Hammer.json advanced configuration.

Getting Started With Contentful, Hammer & Forge

This post gives a solid introduction to getting started with both Contentful and Hammer and setting up your continuous deployment setup with Forge, using webhooks.

From there, we take a more in depth look at some more advanced capabilities of the integration, including the all important generation of new content.

Micro-services Example

In addition to the core setup, I wrote a post which demonstrated how you could use an additional micro-service to add e-commerce capabilities to your site, using Snipcart.

This is just the start, and there’s lots of areas for us to explore different micro-services working together.


#2

The feedback on the Contentful integration has generally been great. It works really well.

The Downsides

###Pricing

I chose contentful because they were the most advanced product so far as CaaS (Content as a Service / Content API) solutions were concerned. But, that comes at a price.

Most of the negative feedback I hear is directed at the Pricing of Contentful.

On one hand, it’s a fairly simple 3 tier structure, starting from Free, then $99 per month, then $199 per month.

In reality, most of the people I’ve heard with concerns about the pricing will find that the Free plan will suit their needs for pretty much the entire lifespan of their sites. You can eat API calls pretty quickly when developing your site, but after that, your site is built and content updates for most sites slow down significantly. If you don’t have a large team of admins and a lot of content, you’ll be just fine.

If you do have a decent sized team and a website of some value, then I think the $99 - $199 a month is the least of your worries.

###Admin UI

You would have thought that of all the things to get absolutely spot on, Contentful’s admin UI should be impeccable. I have used it quite a bit and would say that it’s pretty decent, once you’ve got your head around how it works with defining your content types and generating entries. Handling media assets is a little clunky from within entries, for example.

But I’ve heard a fair amount of negativity from other people about the UI. I would have to say, for the most part I absolutely disagree and yet, feel there is a real opportunity for improvement. I really miss the Squarespace post editor when working in Contentful for content creation, it has to be said.


#3

Hammer-Contentful in the Wild

Congratulations to the guys at Paddle who decided to use this workflow for their new product site build.

Not only their homepage, but they generated pages and pages of documentation for their platform…

Christian Owens, CEO at Paddle said:

My favourite thing about this is that prior to using contentful our two options were:

  • install wordpress and build something.
  • manage the whole thing statically with markdown files and forego some functionality.

the other option was to build some kind of docs CMS from scratch, but who has time for that!

i think this ticks almost every box (speed, reusability, updatability, and is collaborative for our team)


#4

##Introducing Cockpit

Cockpit is an alternative to Contentful, as an API-first Content as a Service.

There are some differences, though.

  1. Cockpit is built by one guy
  2. It’s Open Source
  3. It’s free to self-host
  4. It can be run locally
  5. It’s PHP-based

Why Cockpit?

We really like the project. It’s a good effort and ticks the boxes so far as having an accessible, specially designed Contentful alternative goes (as opposed to Wordpress API).

The latest Cockpit, called Next, looks even better than the old version and has a new and updated API.

Hammer Support

We’ve built a working integration with Hammer, which works in much the same way as the Contentful integration.

Cockpit Setup

First, you need to setup your Cockpit app locally or hosted somewhere, like a Digital Ocean Droplet for $5 a month.

I set it up locally and ran it using MAMP. Simply drop the code into htdocs and visit http://localhost:8888/yousite/install and you’re up and running.

Configure Hammer.json

The configuration works in much the same manner as Contentful.

hammer.json example

{
  "cockpit": {
    "apiUrl": "http://cockpit.xxx.xx",
    "apiKey": "xxxxxxx",
    "contentTypes": {
      "articles": {
        "name": "Article",
        "template": "_article.slim",
        "urlAliasPrefix": "posts",
        "urlAliasSource": "Title",
        "renderOnBuild": true
      }
    }
  }
}

You need to provide, in this case, the apiUrl and apiKey which you will find in your Cockpit instance at /restadmin/index

One thing to note here is the slight change in terminology. We’ve kept the hammer.json format consistent, to avoid confusion, but Cockpit refers to contentTypes as Collections.


#5

Cockpit-Hammer in the Wild

Chris Rault, the lead designer and coder at Headway Rocket has been using this workflow for the redesign of his site.

He wrote about his experience in this blog post.

The site also includes a Blog, demonstrating how easy it is to use to generate new and fresh content.

To complete a great example, Chris also uses the Disqus comments system to enable social comment son his static site.

It’s really well designed, clean and super zippy, with some great templates available for purchase.


#6

Hammer-Contentful-Forge in the Wild

We rebuilt the Nuwe website statically using this full integrated workflow. I wanted the site to be as modular as possible. There were relatively few pages, but I wanted to be able to generate new pages and configure for each, which modules were available.

Each page was defined as a Container, which was defined by the _container.slim file in our Hammer project.

/templates/_container.slim

doctype html
html lang="en"
  = include "_var"
  = include "_head"

  body
    = include "_header"

    - if container.bannerComponent?
      = include "_hero"

    - if container.aboutComponent?
      = include "_about"

    - if container.portfolioComponent?
      = include "_portfolio"

    - if container.teamComponent?
      = include "_team"

    - if container.typeform?
      = include "_form"

    - if container.footer
      = include "_feed"

    = include "_footer"

:rocket: How simple is that? :rocket:

We define each of the modules as slim partials.

Simple Partials

A simple partial, such as the About section, might look like this:

/partials/_about.slim

.message
  .container
    .avatar
      img src=container.aboutComponent.authorImage

    .title
      =container.aboutComponent.title

    .text
      =container.aboutComponent.body

The corresponding Contentful ContentType model looks like this:

It has just four fields

More Complex Partials

In our site we have 2 different Hero Banner layouts, with text aligned to different locations

and the image changes depending on whether the display size is mobile or desktop.

We handle this in our Hero component template code in our Hammer project, along with the definition of our Hero component contentType.

/partials/_hero.slim

- if container.bannerComponent.height?
  .hero(style="height: #{container.bannerComponent.height}px")
    .bg(style="background-image: url(#{container.bannerComponent.bannerImage})")
    - if container.bannerComponent.mobileImage?
      .bg_mobile(style="background-image: url(#{container.bannerComponent.mobileImage})")
    - else
      .bg_mobile(style="background-image: url(#{container.bannerComponent.bannerImage})")
    .container
      .text(class="#{container.bannerComponent.bannerStyle}")
        .slogan =container.bannerComponent.mainTitle
        - if container.bannerComponent.subtitle?
          .subslogan =container.bannerComponent.subtitle
        - if container.bannerComponent.bannerStyle.include? "centre"
          .scroll_down

- else
  .hero
    .bg(style="background-image: url(#{container.bannerComponent.bannerImage})")
    - if container.bannerComponent.mobileImage?
      .bg_mobile(style="background-image: url(#{container.bannerComponent.mobileImage})")
    - else
      .bg_mobile(style="background-image: url(#{container.bannerComponent.bannerImage})")
    .container
      .text(class="#{container.bannerComponent.bannerStyle}")
        .slogan =container.bannerComponent.mainTitle
        - if container.bannerComponent.subtitle?
          .subslogan =container.bannerComponent.subtitle
        - if container.bannerComponent.bannerStyle.include? "centre"
          .scroll_down

Excuse the slight disconnect between hero & banner components, we changed the terminology half-way through…

Firstly we check if the height has been provided, which is used for overriding the default full-height setting to a pre-determined max-height.

Then, if a dedicated mobileImage has been provided, we set that as the background image of our component, otherwise we use the default bannerImage.

Then we read the bannerStyle to determine the test layout.

We check if a subtitle has been provided, and if so, we render that.

The design of the centred layout, the one for the homepage, uses a scroll indicator, so if the bannerStyle is set to “centre”, then we render this scroll indicator.

Finally, let’s style it up.

/stylesheets/components/_hero.sass

.hero
  height: calc(100vh - #{$header-height})

  position: relative
  .bg, .bg_mobile
    position: absolute
    z-index: 0
    left: 0
    top: 0
    bottom: 0
    right: 0
    background-size: cover

  .bg
    +tablet
      display: none
    +mobile
      display: none
  .bg_mobile
    display: none
    +tablet
      display: block
    +mobile
      display: block

  +tablet
    background-position: 100% 100%, 0 0
  +mobile
    background-position: 100% 100%, 0 0
  position: relative
  overflow: hidden
  .text
    position: absolute
    text-align: center
    padding: 0 30px
    width: 100%
    left: 0
    top: 50%
    color: #fff
    text-align: center
    transform: translateY(-50%)
    .slogan
      +thin(80,97)

    .subslogan
      +thin(27,40)

    &[class*="left"]
      text-align: left
      width: 50%
      +tablet
        width: 90%
      +mobile
        width: 100%

    &[class*="centre"]
      width: 100%

    &[class*="right"]
      width: 50%
      left: initial
      right: 0
      +tablet
        width: 90%
      +mobile
        width: 100%

    &[class$="small"]
      .slogan
        +thin(36,45)
        +mobile
          +thin(24,30)
      .subslogan
        +light(12,16)
        +mobile
          +light(10,14)
    &[class$="medium"]
      .slogan
        +thin(60,74)
        +mobile
          +thin(40,60)
      .subslogan
        +light(20,30)
        +mobile
          +light(16,22)
    &[class$="large"]
      .slogan
        +thin(80,97)
        +mobile
          +thin(55,82)
      .subslogan
        +light(27,40)
        +mobile
          +light(22,28)

  @keyframes bounce
    0%, 20%, 50%, 80%, 100%
      transform: translateY(0)
    40%
      transform: translateY(-20px)
    60%
      transform: translateY(-10px)

  .scroll_down
    margin: 40px auto 0 auto
    width: 42px
    height: 42px
    background: url(/images/scroll_down.svg) center center no-repeat
    background-size: 41px 41px
    cursor: pointer
    &:hover
      animation: bounce 2s infinite

Another Complex Partial?

Well, OK… if you insist.

How about our Portfolio module?

On each page we list case studies of our work on the platform. The Home page contains a selection of featured projects, whilst each of the other 3 pages presents a selection of projects relevant to their particular category.

We handle this with one single Portfolio component template and one Portfolio contentType that contains many Portfolio Article entries (another contentType).

This time we’ll take a look at the content model first, to get a better idea.

Portfolio Component

Portfolio Article

On each of our site pages, we have a different Portfolio Component entry. This enables us to easily control the child Portfolio Articles to be displayed. We can just simply select from our created Articles and add them in the entry.

Let’s take a look at our Portfolio template in our Hammer project

.portfolio
  .container
    .title =container.portfolioComponent.title
    .text  =container.portfolioComponent.body
    .portfolio_items
      - if container.portfolioComponent.portfolioArticles?
        - container.portfolioComponent.portfolioArticles.each do |item|
          .item_container(class="col-#{item.columns}")
            - style = item.background? ? "background-image: url(#{item.background})" : ""
            - klass = item.textStyle? ? item.textStyle : ""
            .portfolio_item(style=style class=klass)
              .category
                - if item.brandCategory?
                  - if item.brandCategory == "technology"
                    = include "technology"
                  - if item.brandCategory == "development"
                    = include "development"
                  - if item.brandCategory == "integration"
                    = include "integration"
              .label
                = item.title

              .illustration
                img src=item.coverImage
            .details
              .close
              .arrow
                img [email protected]_arrow
              .author
                .avatar
                  img src=item.author.profilePhoto
                .info
                  .name
                    = item.author.name
                  .author_title
                    = item.author.jobTitle

              .title =item.title

              .text =item.body

              .links
                - if item.ctaButton?
                  - if item.ctaOverride == ""
                    a(href="#{item.ctaButton.url}")
                      = item.ctaButton.buttonText
                  - else
                    a(href="#{item.ctaOverride}")
                      = item.ctaButton.buttonText

We have the global component information, followed by iterating over the array of portfolio articles, if they exist.

We’ve given ourselves lot’s of configurable options here, including setting an optional background image, changing the text style to dark or light, assigning a particular category.

We also control things like the button text and url at an article level.

/stylesheets/components/_portfolio.sass

$portfolio_item_width: 392px
$portfolio_item_height: 388px

$container_padding: 30px
+tablet
  $container_padding: 20px
+mobile
  $container_padding: 10px

.portfolio
  padding-bottom: 130px
  padding-top: 30px

  +tablet
    padding-bottom: 80px
  +mobile
    padding-bottom: 50px

  .container > .title
    +thin(36,40)
    color: $blue
    margin: Rem(30) auto

  .container > .text
    max-width: 800px
    padding: 0 $container_padding
    +light(18,25)
    margin: 0 auto Rem(50) auto
    color: #9B9B9B

  .portfolio_items
    display: flex
    flex-flow: row wrap
    justify-content: flex-start
    padding: 0 0px
    position: relative

  .item_container
    flex: 0 1 33.3%

    height: $portfolio_item_height+16px
    padding: 8px
    transition: height .5s ease
    overflow-y: hidden
    +tablet
      flex: 0 0 100%
      padding: 12px 0
    +mobile
      flex: 0 0 100%
      padding: 12px 0

    &.col-2
      flex: 0 1 66.6%

      .portfolio_item
        &:hover
          transform: scale(1.021)

      +tablet
        flex: 0 0 100%

      +mobile
        flex: 0 0 100%

    &.col-3
      flex: 0 1 99.9%
      .portfolio_item
        &:hover
          transform: scale(1.011)

      +tablet
        flex: 0 0 100%

      +mobile
        flex: 0 0 100%

    .portfolio_item
      cursor: pointer
      padding-top: 1px
      transition: all 0.3s ease
      background: #F5F5F5 center center no-repeat
      background-size: cover
      height: $portfolio_item_height

      &:hover
        background-color: #fafafa
        transform: scale(1.031)

      .category
        margin-left: Rem(15)
        margin-top: Rem(20)
        height: 18px
        text-align: left
        svg
          height: 18px
          width: auto

      .label
        padding-left: 15px
        text-align: left
        +light(18,25)
        color: $blue

      &.light
        .category
          svg, path, g
            fill: #fff
            stroke: #fff
        .label
          color: #fff


      .illustration
        height: 310px
        line-height: 310px
        vertical-align: middle
        img
          max-height: 310px

    .details
      display: none
      position: absolute
      margin-top: 20px
      z-index: 2
      top:  auto


      background: $blue
      right: 10px
      left: 8px
      color: #FFFFFF
      padding: 30px 30px

      .close
        width: 34px
        height: 34px
        background: url(/images/close_icon.png) center center no-repeat
        background-size: cover
        cursor: pointer
        position: absolute
        right: 30px
        top: 30px

      .arrow
        width: 117px
        height: 62px
        position: absolute
        top: -55px
        left: 80%
        z-index: 3
        +tablet
          display: none
        +mobile
          display: none
        img
          max-width: 117px
          width: 117px
          height: 57px

      .author
        display: flex
        flex-flow: row nowrap
        align-items: center
        .avatar
          img
            width: 94px

        .info
          text-align: left
          margin-left: 23px
          .name
            +medium(18,24)
            color: #FFFFFF
          .author_title
            +thin(18,24)
            color: #FFFFFF

      .title
        text-align: left
        margin: 50px 0 40px 0
        +thin(30,35)
        color: #FFFFFF

      .text
        text-align: left
        white-space: pre-line
        +thin(18,25)

      .links
        margin-top: 30px
        text-align: left
        display: flex
        justify-content: space-between

        a
          vertical-align: middle
          +medium(14,14)
          padding: Rem(14) Rem(24) Rem(16) Rem(24)
          background: #fff
          color: $blue
          border-radius: 40px
          border: 1px solid $blue
          transition: all 0.3s ease
          &:hover
            color: #fff
            background-color: $blue
            border-color: #fff

    &:not(.show)
      height: $portfolio_item_height+16px !important
    &.show
      height: auto

Overall, I have to say that building this site in a short time frame was a joy from start to finish, given that we were building the Contentful integration at the same time, means you have no idea how pleasing that was.

As a team, with different roles and responsibilities, it just worked really well. The designers and myself when working on the content, initially with me defining the content model, then the copy being created in the Contentful Entries area.

The Code written by Boris and myself was managed via Github sync to Forge, which just works so well.

Finally, we setup a Slack Bot integration using the deployment webhooks and the whole team was able to feel very much part of the design and build process.