Petal
How to Make the Team Happy with Hugo and Headless

How to make the team happy with Hugo and Headless

The speed, security, and simplicity of Hugo caught your eye, but can this static site generator satisfy both you and your non-technical team?


Michael BerryPublished on Oct 19, 2020

In this article, I’ll show you how to feed a Hugo site data from a headless CMS so developers can reap the benefits of a static site while allowing content creators to use the tools they love.

How React burnout led me to Hugo

Static site generators (SSGs) have gained momentum recently, and after being bombarded by the concept in my technology news feed for weeks,  I felt I needed to better understand the trend.  After some research, I found that a lot of prominent SSGs use React, so I started improving my JavaScript skills. I built some neat React prototypes, but I kept running into errors or getting lost in the code. I had trouble untangling whether issues were with my JavaScript, React, or JSX—it was very frustrating. I learned a lot, but it was a humbling experience.

I started looking at other options to avoid burnout. Hugo’s popularity in the State of Jamstack 2020 Report piqued my interest, so I watched some tutorials. It seemed straightforward, so I fired up a "quick start" tutorial and experimented. Within two weeks, I went from zero Hugo experience to a working site pulling content from my headless CMS of choice! What a difference compared to my first encounter with React!

What is Hugo?

Hugo takes a source directory of files and templates and uses these as input to create a complete website. It’s built on GO, has its own templating language, and it can be hosted anywhere. The Hugo team (boldly) claims that it’s the world’s fastest framework for building websites, one of the most popular open-source static site generators, and sports amazing speed and flexibility. 

What does that mean to you?

You get the benefits of a statically generated site without having to learn some advanced React concepts or complicated JS. You don’t even really need to know how to program in GO. Armed with just some basic Hugo templating concepts, HTML/CSS, and knowledge about Hugo’s directory conventions is enough to build a site. 

In my experience, it is fast and simple, but I found two drawbacks: 

    1. There is no ability to truly create dynamic pages out of the box. To use Hugo’s out-of-the-box features like handling routing, content needs to be physically present in the “content” directory. This means by default pages created in external systems (like a CMS) can’t be dynamically added to the project. 
    2. Hugo uses physical markdown files with limited content creation tools. 

What do those limitations mean to your non-technical team?

Not everyone likes Markdown. I’d argue most content creators want a more graphical experience than hammering # or * symbols into a terminal-like editor. They also miss out on valuable CMS features that they’ve grown accustomed to like workflows or collaborative editing. Worst of all, in Hugo, they would have to make changes directly to markdown files that live alongside code files (😱).

So how can you bridge the gap between your team’s content creation needs and your desire to use Hugo?

Never fear, the CMS is here!

The key piece for making both developers and content creators happy with Hugo is integrating a headless CMS. A headless CMS provides content creation tools, like editorial calendars:

Editorial Calendar

and collaboration features like Simultaneous Editing

Kontent.ai

Quality-of-life features like those make content creators and editors happy. What about you as the developer? Well, you don’t have to host it, as it’s cloud-based, and you get an SDK for simple data gathering. Doesn’t that sound great?

I’ll summarize my solution in 5 steps: 

  1. Setting up Hugo
  2. Connecting Hugo to the headless CMS
  3. Converting JSON to Markdown
  4. Using build scripts
  5. Deploying the site

Setting up Hugo

First, I needed to install Hugo. Hugo can be run on multiple platforms, and since I am on a Windows machine, I used Chocolatey per their documentation:

choco install hugo -confirm

Once Hugo was installed, I ran the new site command:

hugo new site kontent-hugo-articles

This created a new Hugo site with all the boilerplate directories necessary to get started quickly. I created a new markdown file using the commands:

cd quickstart

hugo new articles/my-first-post.md

And I added markdown content to the newly created file:

---
title: "How to Make the Team Happy with Hugo and Headless"
date: 2019-03-26T08:47:11+01:00
draft: false
---
The speed, security, and simplicity of Hugo caught your eye, but can this static site generator satisfy both you and your non-technical team?

In this article, I'll show you how to feed a Hugo site data from a headless CMS, so developers can reap the benefits of a static site while allowing content creators to use the tools they love.

Once the test content had been created and the structure was established, I added a _default sub-directory to the layout directory and created single.html and list.html layouts:

~/layout/_default/list.html:

<ul>
    <!-- Ranges through content/posts/*.md -->
    {{ range .Pages }}
        <li>
            <a href="{{.Permalink}}">{{.Date.Format "2006-01-02"}} | {{.Title}}</a>
        </li>
    {{ end }}
    </ul>

~/layout/_default/single.html:

{{ .Content }}

Running the command hugo server started a server to test my sample content with. 

Kontent.ai

With the basics up and running, I could get started on integrating the headless CMS.

Connecting Hugo to the headless CMS

To get up and running quickly, I opted to use Kentico Kontent’s sample project and focused on grabbing the pre-made articles it contains.

Since Hugo uses the markdown format by default, I needed to fetch the content and convert it to markdown files. To achieve this,  I first installed Node.js and used npm to install the Kentico Kontent Delivery JavaScript SDK for retrieving content: 

npm i rxjs --save
npm i @kentico/kontent-delivery --save

Once the libraries were installed, I created a cms-scripts directory in the project to separate the CMS implementation details from Hugo’s.

From here, it was a matter of following the Kontent Delivery SDK’s documentation for setting up a Delivery Client and retrieving my Kontent project’s articles over the API. I separated this logic into two files:
cms-scripts/config.js:

const KenticoContent = require('@kentico/kontent-delivery');

const deliveryClient = new KenticoContent.DeliveryClient({
    projectId:' <your_project_id>'
});

exports.deliveryClient = deliveryClient;


cms-scripts/buildArticles.js:

const { deliveryClient } = require('./config')

const subscription = deliveryClient.items()
    .type('article')
    .depthParameter(2)
    .toObservable()
   .subscribe(response => { 
        console.log(response)
    })
Kontent.ai

Running node cms-scripts/buildArticles.js in the terminal resulted in a JSON response from the Kontent Delivery endpoint. Now it was time to transform the response into the physical markdown files Hugo expects.

Converting JSON to Markdown

There are three layers to converting Kontent’s JSON to Hugo’s default markdown file format: 

Extracting the content from the response:

cms-scripts/buildArticles.js:

//code continued from cms-scripts/buildArticles.js
//...
const markdownConverter = require('./markdownConverter');
//...
    .subscribe(response => {
        for (var item of response.items){
            //frontmatter:
            const title = item.title.value
            const codename = item.system.codename
             //correct mismatch between Kontent date format and Hugo's expected format
            let date = new Date(Date.parse(item.post_date.value))
            date = date.toISOString()

            //article content
            const body_copy = item.body_copy.resolveHtml()
            const teaser_image = item.teaser_image.value[0]

            //convert JSON values to markdown
            const data = markdownConverter.convert(title, date, body_copy, teaser_image)
      }

     subscription.unsubscribe();
    });

Creating a markdown converter that uses Turndown.js (npm install turndown) to change extracted JSON content into markdown:

cms-scripts/markdownConverter.js:

const TurndownService = require('turndown');

const convert = (title, date, body_copy, teaser_image) => {
    //markdown conversion
    const turndownService = new TurndownService()
    const markdown = turndownService.turndown(body_copy)
    const header_image = turndownService.turndown(`<img src="${teaser_image.url}" alt="${teaser_image.description}"/>`)
    
    const data = `---
    title: "${title}"
    date: ${date}
    draft: false 
    ---
    ${header_image}
    ${markdown}
    `    
    
    return data
    
    }
    
    exports.convert = convert;

Creating a physical file in the Hugo content directory or sub-folders using Node’s built-in fs library:

cms-scripts/buildArticles.js (complete code):

const { deliveryClient } = require('./config')
const markdownConverter = require('./markdownConverter');
const fs = require('fs');

const subscription = deliveryClient.items()
    .type('article')
    .depthParameter(2)
    .toObservable()
    .subscribe(response => {
        for (var item of response.items){
            //frontmatter:
            const title = item.title.value
            const codename = item.system.codename
             //correct mismatch between Kontent date format and Hugo's expected format
            let date = new Date(Date.parse(item.post_date.value))
            date = date.toISOString()

            //article content
            const body_copy = item.body_copy.resolveHtml()
            const teaser_image = item.teaser_image.value[0]

            //convert JSON values to markdown
            const data = markdownConverter.convert(title, date, body_copy, teaser_image)

            fs.writeFileSync(`content/articles/${codename}.md`, data)
      }

     subscription.unsubscribe();
    });

Once the above scripts were in place, which can be seen in this GitHub repository, I was able to start populating my Hugo project with content by running node cms-scripts/buildArticles.js in the terminal again.

Kontent.ai
Kontent.ai

With the markdown files in the /content/articles directory, I could bring the site online by running the command hugo server.

Kontent.ai

I was shocked! Connecting the headless CMS to Hugo in just three small JavaScript files definitely brought a smile to my face.

Using build scripts

At this point, I had a locally running Hugo site that used my headless CMS content. Great! But manually running node cms-scripts/buildArticles.js to populate my project followed by a separate command to run the Hugo server wasn’t ideal. To streamline this process, I used npm-run-all, which allowed me to run the scripts together on my Windows machine using a local:start command I added to package.json.
~/package.json:

"scripts": {
    "cms:prepare": "node cms-scripts/buildArticles.js",
    "local:serve": "hugo server -b http://localhost:1313", 
    "local:start": "npm-run-all -p cms:prepare local:serve",
  },

In addition to simplifying the local build, this sets the groundwork for automating builds in a hosted environment.

Deploying the site

The final obstacles before being completely happy with my Hugo project were:

  • deploying the site for the whole world to see
  • automating the build process

Luckily, both are made possible with hosting services like Netlify. Not only is deploying to Netlify as easy as connecting to your GitHub repository, it also lets you run automated builds of your website with each Git push.

To leverage Netlify for hosting and automated builds, I modified my package.json to include a Netlify build script:

~/package.json:

"scripts": {
    "cms:prepare": "node cms-scripts/buildArticles.js",
    "local:serve": "hugo server -b http://localhost:1313", 
    "local:start": "npm-run-all -p cms:prepare local:serve",
//added for deployment
    "netlify:serve": "hugo -b $DEPLOY_PRIME_URL",
    "netlify:build": "npm-run-all cms:prepare netlify:serve"
  },

This made it possible to set my "Build & Deploy build command" in Netlify. I created a new site from Git, set the build command to npm run netlify:build, and targeted publish as the Publish directory, all from within the Netlify interface. Now, any code change in my GitHub repository for this project resulted in an automatic rebuild of the site. 

Kontent.ai

To extend this automation to content updates in the CMS, I used Netlify’s build hooks paired with Kentico Kontent’s webhooks per the Kontent documentation. With this addition, whenever the content is published or unpublished in my Kentico Kontent project, Netlify rebuilds my site so the content is always up to date.

Summary

In this article, I introduced Hugo, discussed how to connect Hugo to a headless CMS, and how doing so benefits both developers and content creators. I hope that I showed you that both you and your non-technical teammates can enjoy working with Hugo in future projects!

If this article got you excited about the possibilities of Hugo and Kentico Kontent, a more robust Hugo + Kontent sample site can be seen here.

Written by

Michael Berry

I’m the US Support Lead at Kontent.ai. In between managing an all-star team of support engineers and helping customers, I like to challenge myself with technical Kontent.ai projects.

More articles from Michael

Feeling like your brand’s content is getting lost in the noise?

Listen to our new podcast for practical tips, tricks, and strategies to make your content shine. From AI’s magic touch to content management mastery and customer experience secrets, we’ll cover it all.

Listen now
Kontent Waves