How to Build an API-Powered, Static Website: The Best of Both Worlds

Tony Spiro

This article originally appeared on Cosmic JS.

Static websites have been around for awhile, like since the beginning of the internet (the web used to be all static websites). Now we’re seeing a trend that is bringing these HTML-centric websites back, why?  Because HTML renders really fast when you don’t have a database or a server-side language slowing things down.  That’s not to say that programming languages should be done away with, because pretty much every major website you visit has some sort of server-side language and database attached to it.  But for smaller websites that don’t involve things like user session management or time-sensitive dynamic data, a static site will do, and actually may be more ideal.

In this article I’m going to show you how to build a static website that gives you the best of both worlds:  A website that is both dynamically powered by the Cosmic JS API and also a static website that can be edited using Markdown files.  As a bonus, you can also setup automatic builds using Webhooks.

TL;DR

View the Static Website demo here.

View the Static Website codebase on GitHub here.

View the Static Website App page on Cosmic JS.

The Problem With Markdown

A lot of static website processes involve translating Markdown to HTML.  And developers love Markdown, for good reason:  Markdown allows developers to save time by using a shorthand language that translates into HTML.

But most non-technical people would rather not have to learn Markdown to edit content.  Most content editors would rather use an intuitive content management system.  That’s where Cosmic JS comes in.


API-Powered, Static Website

In building our Cosmic JS-powered static website, our goal is to make it easy for developers to build the website and easy for non-developers to edit and deploy content.  For editors, adding new content will be as easy as going into the Cosmic JS Dashboard, adding a new Page, editing the different content areas and clicking Save.  For developers, they will still have the benefit of using Markdown.  By building our website this way, we can get the whole team involved.  Our static website will have the best of both worlds: A fast, static website that is easily managed by a developer using Markdown files, or content editor using the Cosmic JS CMS API.

Getting Started

To build our API-powered static website, we will need a few things:

1. A build process that converts the API response into static pages.

2. A rebuild process that makes it easy to rebuild our app after content edits.

3. The ability to create pages using Markdown.

To follow along with the steps download the Static Website on GitHub.


The Build Process

To handle our static website's files and rebuild process we’ll use Node.js, specifically the Express framework for our light-weight server.  In our app.js file we have the following:

var buildSite = require('./build-site')
buildSite()
var express = require('express')
var app = express()
app.set('port', process.env.PORT || 3000)
app.use(express.static('build'))
app.get('/rebuild-site', (req, res) => {
  buildSite()
  res.end('Site rebuilt!')
})
app.post('/rebuild-site', (req, res) => {
  buildSite()
  res.end('Site rebuilt!')
})
app.get('*', (req, res) => {
  res.redirect('/404')
})
app.listen(app.get('port') || 3000, () => {
  console.info('==> 🌎  Go to http://localhost:%s', app.get('port'))
})

There’s a few things happening.  First we’re initiating the build process right on app startup.  Our static files will be served from the /build folder which will also be the root of our application.


Other Routes

GET /rebuild-site will rebuild the website.

POST /rebuild-site is used for our Cosmic JS Webhooks.

GET /* When a route is not found and "falls through" our designated routes, it will redirect to the 404 page located at /404.


Building HTML Pages from the API

Next let’s look at the build process.  Here's what's in the build-site.js file:

var Metalsmith = require('metalsmith')
var markdown = require('metalsmith-markdown')
var layouts = require('metalsmith-layouts')
var permalinks = require('metalsmith-permalinks')
var sass = require('metalsmith-sass')
var Cosmic = require('cosmicjs')
var async = require('async')
var mkdirp = require('mkdirp')
var del = require('del')
var mv = require('mv')
var createPage = require('./create-page')
var config = require('./config')
module.exports = () => {
  async.series([
    // Create build-new folder
    callback => {
      mkdirp(__dirname + '/build-new', err => {
        callback()
      })
    },
    callback => {
      Cosmic.getObjects(config.cosmicjs, (err, res) => {
        var pages = res.objects.type.pages
        var cosmic = res
        // Create dynamic static pages
        async.eachSeries(pages, (page, callbackEach) => {
          var args = {
            page: page,
            pages: pages,
            cosmic: cosmic
          }
          createPage(args, callbackEach)
        }, () => {
          // Create markdown static pages
          var year = (new Date()).getFullYear() // make your footer year dynamic ;) 
          Metalsmith(__dirname)
            .metadata({
              cosmic: cosmic,
              year: year
}) .source('./src') .destination('./build-new') .clean(false) .use(sass({ outputDir: 'css/', sourceMap: true, sourceMapContents: true })) .use(markdown()) .use(permalinks()) .use(layouts({ engine: 'handlebars' })) .build((err, files) => { if (err) { throw err } callback() }) }) }) }, // Delete build folder callback => { del([__dirname + '/build']).then(() => { callback() }) }, // Move build-new to build folder callback => { mv(__dirname + '/build-new', __dirname + '/build', { mkdirp: true }, () => { callback() }) }, // Delete build-new folder callback => { del([__dirname + '/build-new']).then(() => { // done }) } ]) }

We're using the async module to make sure everything is happening in the correct order.  Here's what's happening:

1. The new build folder is created.
2. The Cosmic JS API is called to get the Pages from our Cosmic JS bucket.
3. The createPage function is called next (we’ll get to this in a moment).
4. Finally Metalsmith takes care of our frontend: preprocessing SASS files into CSS or concatenating and minifying JavaScript, etc.  Along with these frontend processes, Metalsmith will also convert any Markdown files that we have in our src folder into HTML pages in our build folder.

After all static HTML files from the API and from Markdown are added to the build-new folder our build-new folder is renamed to build for a zero-downtime update to our static website.

With Cosmic JS serving content from the CMS API and Metalsmith powering the frontend and Markdown files, you have the ability to let a non-dev manage content in the static site as well as giving the developer the freedom to still use Markdown.

Next let’s look at how the pages are rendered from the API to HTML pages.  In create-page.js we have the following:

var fs = require('fs')
var async = require('async')
var mkdirp = require('mkdirp')
var Handlebars = require('handlebars')
module.exports = (args, done) => {
  var page = args.page
  var pages = args.pages
  var cosmic = args.cosmic
  var locals = {}
  async.series([
    callback => {
      fs.readFile(__dirname + '/layouts/page.html', 'utf8', (err, data) => {
        if (err) {
          return console.log(err)
        }
        var template = Handlebars.compile(data)
        locals.template = template
        callback()
      })
    },
    () => {
      // Set variables
      var year = (new Date()).getFullYear() // make your footer year dynamic ;) 
      var markup = locals.template({ page, pages, cosmic, year })
      // If Home page found
      if (page.slug === 'home') {
        fs.writeFile(__dirname + '/build-new/index.html', markup, err => {
          if(err) {
            return console.log(err)
          }
          done()
        })
      } else {
        mkdirp(__dirname + '/build-new/' + page.slug, err => {
          fs.writeFile(__dirname + '/build-new/' + page.slug + '/index.html', markup, err => {
            if(err) {
              return console.log(err)
            }
            done()
          })
        })
      }
    }
  ])
}

Notice we are reading the contents of page.html from our layouts folder and creating a template using Handlebars.  You can use other template languages, but I prefer Mustache, or Handlebars for its logic-less simplicity.  Next we are passing in our variables from the API to translate the template variables into rendered output.  Next a file write happens to /build/:page.slug:/index.html for all pages in our Cosmic JS bucket.  And a special page is built for our home page at /build/index.html.

As you can see this is a pretty simple process of taking the output of our Cosmic JS bucket and translating it into HTML using Handlebars and the Node.js fs.writeFile function.

Building Pages from Markdown

In our src folder there is a file docs/index.md that will be rendered to HTML to /build/docs/index.html at the route /docs.  Here's the markdown in src/docs/index.md:

---
title: Documentation
layout: markdown.html
---
This documentation page is powered by Markdown.
Here's a block of code:
```bash
$ cd awesome
$ yarn
```
Here's another block of code:
```javascript
var fs = require('fs')
var async = require('async')
var mkdirp = require('mkdirp')
var Handlebars = require('handlebars')
module.exports = (args, done) => {
  var page = args.page
  var pages = args.pages
  var cosmic = args.cosmic
  var locals = {}
  async.series([
    callback => {
      fs.readFile(__dirname + '/layouts/page.html', 'utf8', (err, data) => {
        if (err) {
          return console.log(err)
        }
        var template = Handlebars.compile(data)
        locals.template = template
        callback()
      })
    },
    () => {
      // Set variables
      var year = (new Date()).getFullYear() // make your footer year dynamic ;) 
      var markup = locals.template({ page, pages, cosmic, year })
      // If Home page found
      if (page.slug === 'home') {
        fs.writeFile(__dirname + '/build-new/index.html', markup, err => {
          if(err) {
            return console.log(err)
          }
          done()
        })
      } else {
        mkdirp(__dirname + '/build-new/' + page.slug, err => {
          fs.writeFile(__dirname + '/build-new/' + page.slug + '/index.html', markup, err => {
            if(err) {
              return console.log(err)
            }
            done()
          })
        })
      }
    }
  ])
}
```

Any new folder that you add to src with an index.md Markdown file will create a new route at /your-new-folder.

Starting Our Static Website

If you haven't already, download the GitHub repository for the Static Website by running the following commands:

git clone https://github.com/cosmicjs/static-website
cd static-website
yarn install
yarn start

Now go to http://localhost:3000 to see your freshly built website.  Go to http://localhost:3000/rebuild-site whenever you would like to rebuild your website.

Automatic Builds Using Webhooks

Next, to make things even easier for your content editor you can add Webhooks to your Cosmic JS bucket to trigger a rebuild every time content is published in your bucket.

Check out the tutorial to learn how to add Webhooks.

Conclusion

By using Cosmic JS as your CMS API you can have a static website with the best of both worlds: A dynamic website that can be managed using an intuitive content editing experience as well as the ability to add developer-friendly Markdown pages.

I hope you've enjoyed this tutorial on creating an API-powered static website.  If you want to deploy the static website in just a few clicks, sign up for Cosmic JS, create a bucket and look for the Static Website App in the Apps tab of your bucket.  For any questions or help getting started, reach out to us in the Cosmic JS Slack channel, or reach out to us on Twitter.