Sat Jun 06 2020
JAMStack: Integrating Sanity.io and Hugo
In this article I'm discussing my approach to creating a so-called JAMStack site, using this site (kinderas.com) as an example. The site is integrated with a CMS called Sanity for easy content management.
Goals
I wanted something simple, cheap, fast (load-times) and secure. After testing several combinations of CMSes and static site generators, I landed on Sanity.io as the CMS and Hugo as the static site generator.
Sanity
Sanity.io is the platform for structured content. With Sanity.io you can manage your text, images, and other media with APIs.
Source: The Sanity.io website
I chose Sanity because I like the approach to defining data structures as code. This makes it much easier to keep a backup and redeploy the CMS to new instances. Sanity is also responsive, great to work with and have a reasonable pricing model.
Hugo
Hugo is one of the most popular open-source static site generators. With its amazing speed and flexibility, Hugo makes building websites fun again.
Source: Hugo website
The main reason I went with Hugo instead of the massively popular Gatsby was build-speed and the approach to building pages. Building templates in Hugo consists mostly about writing HTML and CSS. In Gatsby everything is build using React, something which can be an advantage if you need to mix build time and runtime react components. I had no such need since a wanted a static blog site. Then there is the build speed... Hugo is way..way faster than Gatsby. This matter a great deal when you need for the entire site to build every time you click «publish» in the CMS.
With the setup explained below, deploying this site takes about 1 min and 30 seconds from clicking publish to the change is visible
AWS Amplify
For building and deploying the site I'm using AWS Amplify Deploy which has build in support for Hugo, Node.js and is powered by the AWS Cloudfront CDN, which makes things fast and cheap to host.
You can probably use Netlify or similar achieving the same results as using Amplify.
The setup
Normally when using Hugo you would store all the content in a folder named «content» at the root level. The structure within the «content» folder would then define the url structure of the resulting site. For example a blog posts with the url technology/2020/01/28/something
would mean that the markdown file «something.md» was stored at the /content/technology/2020/01/28
path.
// «/content/technology/2020/01/28/something}»
content/
└── technology/
└── 2020/
└── 01/
└── 28/
└── something.md
Since the content is now stored in Sanity we need a way to import the content from Sanity into the project at build time. For doing this I wrote a small JavaScript file which is executed by Node.js at build time. The script uses the Sanity JS client with a GROQ query (see example below).
In my case the query covers both the technology blog and the work blog in one request. This might have to be tweaked for sites with a large content base by adding pagination.
// PS: This query is based on my Sanity schema
const query = `*[_type == "workPost" || _type == "techPost"] {
_type,
title,
metaDescription,
slug,
publishedAt,
body
}`;
You would then go through each post and generate the appropriate markdown file at the correct path in the content folder.
// Again, this is my setup, yours will probably differ
const slug = fields.slug.current;
const date = new Date(fields.publishedAt);
const year = date.getFullYear().toString();
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
// The full folder path for the markdown file
const dirPath = path.join(contentFolderPath, dirName, year, month, day);
// The name of the markdown file
const fileName = `${slug}.md`;
// The markdown content with front-matter
const content = `---
title: '${fields.title}'
date: ${fields.publishedAt}
draft: false
slug: "${slug}"
description: '${fields.metaDescription}'
---
${fields.body}
`;
try {
// Create the full path for the markdown file
// This can be multiple folders, hence the recursive flag
await fs.promises.mkdir(dirPath, { recursive: true });
} catch (error) {
// Ignore the folder exists error since this will
// happen if one day has multiple posts
if (error.code !== "EEXIST") {
throw error;
}
}
try {
// Write the markdown file
await fs.promises.writeFile(path.join(dirPath, fileName), content);
return console.log(`✅ Wrote file ${fileName}`);
} catch (error) {
throw error;
}
When the script has completed, which will probably take less than a second unless you have many many posts, then you'll run Hugo build and wait for Amplify to do its thing.
The full build setup looks something like this.
A complete amplify config file can look like this:
version: 0.1
frontend:
phases:
preBuild:
commands:
# Install node modules for the importer script
- yarn install
# Import data from Sanity and create Markdown files
- node sanityToMarkdownImporter.js
build:
commands:
# Run the Hugo build
- hugo --minify
artifacts:
baseDirectory: public
files:
- "**/*"
cache:
# Cache node_modules for next deploy
paths: [node_modules/]
customHeaders:
# Cache css in the browser for a year
- pattern: "**/*.css"
headers: [{ key: "Cache-Control", value: "public,max-age=31536000,immutable" }]
# Cache images in the browser for a year
- pattern: "**/*.gif"
headers: [{ key: "Cache-Control", value: "public,max-age=31536000,immutable" }]
- pattern: "**/*.jpg"
headers: [{ key: "Cache-Control", value: "public,max-age=31536000,immutable" }]
- pattern: "**/*.png"
headers: [{ key: "Cache-Control", value: "public,max-age=31536000,immutable" }]
- pattern: "**/*.svg"
headers: [{ key: "Cache-Control", value: "public,max-age=31536000,immutable" }]
The result is a site which loads fast, it is cheap to host and works pretty well.