Updating Static Sites with Gatsby.js and Node.js for the Front End

Sometimes, generating static pages is more than enough for a project’s requirements; it can even be the most effective way to achieve optimal speed and scalability.

This article series began by demonstrating how to combine Node.js, Express, MongoDB, cron, and Heroku to build a CI-ready back end that uses GitHub’s API to retrieve data daily. Now, we’re going to integrate Gatsby to finish building our static page generation project.

Building the Front End: Creating a Gatsby Website

Gatsby websites are built using React, so familiarity with React web development is beneficial.

I opted for Bootstrap, reactstrap, and react-markdown for styling. It’s worth noting that release notes in GitHub are stored in Markdown format, so we’ll need a converter.

Project Structure for the Static Website

Our project will have the following file and folder organization:

A standard front-end project root with an empty public folder and a src folder for files like package.json. Beneath the src folder are a styles subfolder for global.css and a templates subfolder for all-repositories.js and repository.js.

Here’s a breakdown of these files:

  • env.development and env.production are configuration files for environment variables.
  • all-repositories.js is a template for our homepage, displaying a list of repositories.
  • repository.js is a template to show details of a specific repository.
  • gatsby-node.js is where we’ll consume our back-end endpoint and execute the createPage methods.
  • package.json lists project dependencies and properties, as usual.
  • global.css is our primary stylesheet.

Implementing the Gatsby Website

Similar to setting up the back end, you’ll need to run npm install (or yarn, if you have Yarn installed) after adding the necessary dependencies to your front end’s package.json:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  // ...
  "dependencies": {
    "axios": "^0.18.0",
    "bootstrap": "^4.3.1",
    "gatsby": "^2.0.0",
    "react": "^16.5.1",
    "react-dom": "^16.5.1",
    "react-markdown": "^4.0.6",
    "reactstrap": "^7.1.0"
  }
  // ...
}

The env.development and env.production files contain only the back-end URLs for their respective environments. Here’s what env.development looks like:

1
API_URL=http://localhost:3000

…and here’s the env.production file:

1
API_URL=https://[YOUR_UNIQUE_APP_NAME].herokuapp.com

The code in gatsby-node.js is executed only once during the build process. Therefore, we need to retrieve all the required data from our back end at this stage. The response will then be used with our templates to generate the appropriate static pages.

In this case, all-repositories.js needs access to all repositories, while repository.js requires the data of the corresponding repository for each iteration. Finally, we can generate page paths dynamically, incorporating the repository owner and name parameters within the path field:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const axios = require('axios');

require("dotenv").config({
  path: `.env.${process.env.NODE_ENV}`
});

const getRepositoryData = async () => {
  console.log(process.env.API_URL);
  return axios.get(`${process.env.API_URL}/repositories`);
};

exports.createPages = async ({
  actions: {
    createPage
  }
}) => {
  let repositories = await getRepositoryData();
  repositories = repositories.data;

  // Create a page that lists all repositories.
  createPage({
    path: `/`,
    component: require.resolve('./src/templates/all-repositories.js'),
    context: {
      repositories
    }
  });

  // Create a page for each repository.
  repositories.forEach(repository => {
    createPage({
      path: `/repository/${repository.owner}/${repository.name}`,
      component: require.resolve('./src/templates/repository.js'),
      context: {
        repository
      }
    });
  });
};

For both all-repositories.js and repository.js, ignoring styling for now, we’re simply fetching data from pageContext and using it like we would parameters in React.

In all-repositories.js, the repositories field of pageContext will be used like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
export default ({pageContext: {repositories}}) => (
// ...
    <ListGroup>
      {/* Because the repositories parameter is a list, we are iterating over all items and using their fields */}
      {repositories.map(repository => (repository.tagName &&
        <ListGroupItem className="repository-list-item">
// ...
              <Row>
                {`${repository.repositoryDescription}`}
              </Row>
// ...
        </ListGroupItem>
      ))}
    </ListGroup>
// ...
);

In repository.js, we’ll use the repository field of pageContext instead:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
export default ({pageContext: {repository}}) => (
  <div className="layout">
    {repository.tagName &&
    <ListGroupItem className="repository-list-item">
// ...
          <h1 className="release-notes">{`Release notes`}</h1>
          <hr/>
          {/* This the place where we will use Markdown-formatted release notes */}
          <ReactMarkdown source={`${repository.releaseDescription}`}/>
    </ListGroupItem>
    }
// ...
  </div>
);

Ensure your back end is up and running. In this project, we configured it as http://localhost:3000.

Now, navigate to the root of your front-end project and run gatsby develop. Then, open your web browser and go to http://localhost:8000.

If you’ve added some repositories (owner/name) to your back end and executed the GitHub API update feature at least once, you should see something like this:

The repositories listing home page of our app, showing basic info for a sample of GitHub repos

And upon clicking one of the repositories, you should see a page like this:

A sample repository detail page, showing more information about the facebook/react GitHub repo

After these modifications, our front-end implementation is done.

Excellent! We’re ready for deployment.

Deploying the Front End

No changes are needed in our front-end application for this part. We’ll deploy it to Netlify. (You’ll need an account to proceed.)

Before that, we must push our code to a Git repository hosting service like GitHub, GitLab, or Bitbucket. Like we did with the back end, I deployed my code to GitHub.

Log in to Netlify and click the “New site from Git” button on your dashboard. Follow the prompts to choose your Git provider and locate your repository.

If your code structure is correct, Netlify will automatically configure the build command and publish directory as follows:

The correct settings for Netlify's build options: Deploy master, use "gatsby build" as the build command, and publish to the "public/" directory.

Finally, click “Deploy site.” Netlify will deploy your website to a randomly generated subdomain, which you can customize at any time. I set my deployment to https://sample-create-page-api-gatsby.netlify.com, where you can find a live demo of the completed app.

Because we are working with a static page application, we need to rebuild it every day to ensure its information stays current.

Using a Build Hook for Daily Updates

Netlify’s build hooks act as triggers for rebuilding your site. This allows you to trigger them from your back end once the cron job is completed. Let’s start by creating a build hook in Netlify.

Go to “Build & deploy → Continuous Deployment” where you’ll find the “Build hooks” option. Click “Add build hook.”

Where to find build hooks in Netlify

Provide a descriptive name and save the build hook. (I named mine build-from-backend.) Copy the generated link for later use.

Now, open your back-end project and add the following lines of code to the cron.controller.js file. These lines send a POST request to your Netlify build hook URL.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const Axios = require('axios');
const Config = require('../config/env.config');

const NETLIFY_BUILD_HOOK_URI = Config.netlifyEndpoint;

function updateGatsby() {

  if (NETLIFY_BUILD_HOOK_URI) {
    console.log('Gatsby build request will be send');

    Axios.post(NETLIFY_BUILD_HOOK_URI).then(() => {
      console.log('Gatsby build request was successful');
    });
  }
}

Next, update the updateDaily function:

1
2
3
4
5
function updateDaily() {
  RepositoryController.updateRepositories().then(() => {
    updateGatsby();
  });
}

Update the env.config.js file to retrieve the netlifyEndpoint property from environment variables:

1
"netlifyEndpoint": process.env.NETLIFY_BUILD_HOOK || ""

Now you need to set the NETLIFY_BUILD_HOOK environment variable with the link you copied from Netlify. To do this in Heroku, you can set environment variables from the “settings” section of your application.

Once you push your commit, the back-end application will send a build request to Netlify, and your static pages will be updated daily after the cron job finishes. Here’s what the repo should look like after adding the build hook functionality, as well as the final versions of the back-end repo and the front-end repo.

To wrap up the project, we’ll set up an AWS Lambda function triggered by AWS CloudWatch to wake up your back end in time for the daily updates.

AWS Lambda and AWS CloudWatch Simple Request

AWS Lambda is a serverless computing service. We’ll only use the very basics from this platform, and you’ll need an AWS account.

Start by logging into your AWS account and go to the Lambda Management Console. For instance, if you’re in the us-east-2 region, the console can be found at https://us-east-2.console.aws.amazon.com/lambda/home.

Navigate to the “Functions” section if it’s not already selected:

The AWS Lambda "Functions" section

Here, we will create a new function by clicking on “Create function.” Choose a clear and descriptive name for your function. We will use Node.js for the runtime environment. To finalize the creation process, click on the next “Create function” button.

AWS Lambda's "Create function" page, filled out at create triggerMyBackendAtUTCMidnight with a Node.js runtime and a new role with basic Lambda permissions

This will redirect you to the newly created function’s page. Here, you can write your code directly in the index.js file.

Let’s proceed with implementing our first Lambda function. As we do not have any external dependencies, we will need to rely on the core modules provided by Node.js. (If you prefer to utilize third-party dependencies, please refer to follow this guide from AWS.)

Ensure that the exported method name, which is handler in our case, matches the “Handler” parameter specified on the page:

The Handler parameter with "index.handler" as its value

The remaining code is a simple GET request sent to your back-end application:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const https = require('https');

exports.handler = async (event) => {
  return new Promise((resolve, reject) => {
    https.get(process.env.HEROKU_APP_URL, (resp) => {
      let data = '';
      resp.on('data', (chunk) => {
        data += chunk;
      });

      resp.on('end', () => {
        resolve(JSON.parse(data));
      });
    }).on("error", (err) => {
      reject("Error: " + err.message);
    });
  });
};

Make sure to set the HEROKU_APP_URL environment variable on the page, and don’t forget to save your configuration:

Setting the required environment variable in AWS Lambda. The value shown is https://sample-github-api-consumer-nod.herokuapp.com/repositories.

The last step is to add a CloudWatch trigger rule that will execute this function ten minutes before each daily update—in the context of this article series, this translates to 23:50:

Configuring CloudWatch Events to add a trigger rule

We will set the CloudWatch trigger rule type to “Schedule expression.” Following the accepted format, our cron expression will be cron(50 23 * * ? *).

The AWS CloudWatch "Configure triggers" page, set to create a new rule called cronDailyUpdate with the expression given above and the trigger enabled

With this configuration in place, our AWS Lambda function is now set up to be triggered by the CloudWatch rule.

Our Static Web Page Powerhouse: Gatsby/React and Netlify

We’ve reached the finish line! Our application is complete, powered by a Node.js/MongoDB/Heroku back end with a sprinkle of AWS Lambda/CloudWatch, and a Gatsby and Netlify-driven front end for generating and hosting.

Feel free to revisit the live demo link shared earlier. You can also check out an enhanced version of my prototype, which includes some additional enhancements that I think you’ll appreciate.

Consider this project a template for your own endeavors. I hope these articles empower you to prototype your applications more efficiently and cost-effectively. Happy coding!

Toptal is an Advanced AWS Consulting Partner.
Licensed under CC BY-NC-SA 4.0