DEV Community

Cover image for Gatsby tutorial: Build a static site with a headless CMS
Momcilo
Momcilo

Posted on

Gatsby tutorial: Build a static site with a headless CMS

This Gatsby tutorial shows how building and managing content has changed drastically. From having a central source that controls both the backend and frontend to building the backend on your own and providing APIs to communicate with the frontend. It has now evolved to a more advanced state that enables developers to focus on the front end and being able to manage content in a headless CMS.

Static site generator vs Headless CMS

A Gatsby site uses Gatsby, which leverages React and GraphQL to create fast and optimized web experiences. Gatsby is often used for building static websites, progressive web apps (PWAs), and even full-blown dynamic web applications.

Gatsby works by generating static HTML files during the build process, which are then served to users, offering excellent performance and improved SEO.

Despite being an SSG, Gatsby can also pull data from various sources, including CMS platforms, APIs, Markdown files, and databases, thanks to its integration with GraphQL. This allows developers to build dynamic websites that fetch and display content from multiple sources.

Gatsby & Headless CMS

A headless CMS like BCMS allows you to build Gatsby sites and build server-side rendering capabilities into your websites in three simple steps:

  • Use a headless CMS to create and manage your content
  • Write code in your front-end development framework of choice (this time Gatsby)
  • Use your SSG to combine your code with your content and deliver it to your static site using your CDN

Choosing the right headless Gatsby CMS is key to building an application that is future-proof, appealing to the eyes, and gives the user a nice experience.

This tutorial will show that BCMS has capabilities that match these requirements, including the following amongst many others:

  • User-Friendly Interface: BCMS provides a simple and clear interface, allowing developers to manage and model content easily.
  • Developer freedom: You can choose your preferred technology stack and it will pair well with BCMS with no restrictions, so you’re not limited to what you can build with BCMS, the choice is all yours.
  • Improved SEO: Your website can rank higher in search results due to faster loading speeds and optimized content delivery.
  • Content reuse: Create content once and repurpose it across several mediums to maximize productivity and consistency.

Now it's time to build the Gatsby website with headless CMS. For this tutorial, I chose to create a static podcast website inspired by BCMS Gatsby starters.

Introducing the BCMS Gatsby Podcast Starter

Image description

The BCMS Gatsby Podcast starter is a carefully created static page built with the Gatsby framework and BCMS. The starter is designed to give you a head start. It demonstrates the amazing capabilities of BCMS and how to pair it with Gatsby.

In this tutorial, you will learn how to build a podcast site with Gatsby and BCMS modeling this starter. There will not be coding from scratch, but rather just explore this starter see how it was built, and explain why it was built that way.

By following this Gatsby tutorial step by step, you will be equipped with all the knowledge you need to build your own Gatsby static sites with BCMS and add more functionalities as you desire. So please do well to give the starter repo a star.

Gatsby tutorial: Getting Started

Now that you’re familiar with the capabilities you have with Gatsby and BCMS, let’s get the knowledge you need to bring these capabilities to life. This is how this article will be structured:

  • Initializing BCMS
  • Populating BCMS with Podcast Data
  • Configuring Gatsby to use BCMS and using data from BCMS
  • Deployment

Initializing BCMS

You have two alternatives for setting up BCMS. BCMS can be hosted in the cloud or locally. The big difference between the two is that the local one is hosted by your server and you are responsible for setting it up and configuring it yourself, whereas the cloud one is hosted by the BCMS team and is configured to remain operational.

The cloud version should be your go-to for Live applications, especially large-scale ones. To get started, with the cloud version go to(https://cloud.thebcms.com/) to create an account.

For this tutorial, I’ll be setting up BCMS locally. Now if you set it up locally or on the cloud, the interface and everything will be the same, you will only specify what instance you’re using when creating the Gatsby project, but more on that later.

Installing BCMS Locally

To begin, install the BCMS CLI globally:

npm install @becomes/cms-cli -g
Enter fullscreen mode Exit fullscreen mode

To verify that the installation went well, run:

bcms --help
Enter fullscreen mode Exit fullscreen mode

If you see a successful bcms response, congrats👏 BCMS has been successfully installed.

Creating a Project

To create a BCMS project, run:

bcms --cms create
Enter fullscreen mode Exit fullscreen mode

Follow the prompts give it a name and wait till it completes. After it completes, it won’t automatically start. BCMS relies on Docker to package the application and run it, if you don’t have Docker on your machine head over to the official Docker documentation and install Docker.

After installation, on your terminal navigate back to the newly created BCMS project and run:

docker-compose up
Enter fullscreen mode Exit fullscreen mode

If the docker installation went well, the server should be up and running, go to localhost:8080 on your browser to verify it’s running. You should be greeted with a welcome screen. This program you just created is called an Instance.

Image description

Using Podcast Data to populate BCMS

So how to create data in BCMS? The principle behind BCMS data creation is no different from the ones you’ve been using, be it your custom-made backend with either PHP or Nodejs or another CMS if any: You create a data model, this is where you specify the inputs you need, and the property types for that input and then you create data based on those data models.

On the left side of your BCMS dashboard is an administration panel containing Templates, Widgets, and Groups…., and then further Down, you will find entries. What does this all mean, and how do they come together?

Templates - This is your content structure, your building block. Here is where you spell out what fields you need for the section you’re creating a template for.

Entries - An Entry is a simple record of a template, a dynamic piece of content that follows the content structure you defined in your template.

Groups - Groups in BCMS are reusable building blocks made of multiple properties. Groups can be included in any template, widget, or other group.

Learn more: BCMS Widgets - Reusable structured content - everything you need to know

Now let’s enter the information you want for a podcast website. To do so, browse the website and identify any information that should be stored on the backend(BCMS) rather than hard-coded into the front end. Let's have a look at the pages and portions of the website that require data to be saved on BCMS:

  1. The About Page
  2. Episodes (Section in the Home Page)
  3. Footer
  4. Guest( Which is linked to each episode)
  5. Header
  6. Home Page (The Content on the home page)

These will be the sections creating templates. Six total templates.

Let’s start with the About page. Note: The starter I’m using already has a BCMS instance created which you can also find on the starters repo so everything in this tutorial will match what is already made there, if you'd like to follow along you can clone it and run it to follow along.

About Page

Looking at the Page, you can identify the necessary things:

  • Title
  • Slug
  • Cover
  • Content
  • and Seo(This will be a group)

So first create a new group named Seo which will hold simple metadata details about the page. For this case, it will be just the title and Description.

Image description

Now, create the About page template from the template with the data mentioned earlier. Upon creating a new template automatic Title and Slug fields will be added, so the properties you will be adding are SEO, Cover, and Content.

Image description

Episode

Identifying the data needed for the episode:

  • Title
  • Slug
  • SEO
  • File (The Podcast File)
  • Cover (The podcast image cover)
  • Guest (This will not be a group but another template which will be linked)
  • Description
  • Tags (Will be an array of strings)
  • Date

The Episode template relies on the Guest template so you should create the Guest template first to enable you to link it to the podcast template.

It will be the default Title and Slug and an avatar.

Image description

With that created, you can now create an episode template.

Image description

Image description

Footer

From the Starter Page, you can see that the footer has email and social icons. So the properties that will be in the template are the default Title and slug along with email and social which will be a group holding the social icons.

So first, create the group and name it Social Link, which will hold the icon and the URL.

Image description

After that, you can create the Footer template itself.

Image description

Since I already created the Guest template, I can move on to the header template

Header

In addition to the Default properties, the header template needs a nav which will be an array that will contain the Navigation links and also a logo.

Image description

Home Page Template

Let’s take a look at the home page to determine what I need.

Image description

So, first, I need the hero section, which will include the cover image title and description. Then goes the podcast part, which includes the title and descriptions. I will separate these sections into two distinct templates:

  • Home Hero

As previously stated, the Home Hero template will have the Title, Description and Cover.

Image description

  • Home episodes

Home episodes will hold the title and description for the episodes that will appear on the home page. Please remember that this is not the main template for the episodes.

Image description

Now that I've built both groups, I can create a Home Page template.

Image description

This is how look a successfully developing the templates for our material. Remember, the template is similar to the data model and, therefore you still require data based on the models. This is where the entries come in. Scroll down from the administrative panel to discover the entires tab. Now, for each template, there will be an entry space where you may enter your actual content.

If you want the same content as the starter, download the BCMS instance from Github, run it, look at the content, download the media files, and use it in your instance, or stick with it instead of switching to your instance.

Configuring Gatsby to use BCMS and using data from BCMS

Now it’s time to set up the Gatsby frontend and learn how to fetch data from BCMS using Gatsby.

Creating A Gatsby BCMS Application

To create a Gatsby and BCMS application, I’ll use the BCMS CLI. But before that, there is an important step needed for this next step. You will need an API key.

API keys are used to connect your app with your CMS data. Each key allows you to manage permissions for templatesplugins, and functions.

To create an API Key, click on the key manager tab under administration and click on ‘Add New Key” Give the key a name and check all the template permissions you want. For this tutorial, I will only be fetching data with this key, so it will make sense to only allow ”Can get resources” for all the templates.

Image description

Now, navigate to the directory you want to create your Gatsby project and run the following command:

bcms --website create

Enter fullscreen mode Exit fullscreen mode

This will prompt you for some information such as the framework in our case which will be Gatsby, your project name, the BCMS instance you're using (either local or live), and the API key. After you provide this information, your Gatsby project will be set up and ready to go.

If you’re greeted with a “Not Logged in Message”, then on your terminal type

bcms --login true 
Enter fullscreen mode Exit fullscreen mode

Follow the URL that will be opened in your browser to log in. After that delete the newly created project where you couldn’t log in and re-create it again following the same steps. After Successfully installing, navigate into the newly installed project and run npm run dev. With that, you can navigate to localhost:3000 to view the project in your browser.

can now run the project and open it in a code editor to work on it.

Getting Data From BCMS in a Gatsby Project

So, the BCMS Gatsby Podcast project is created, but it is still the default content, let’s learn how to get data from BCMS.

While you install Gatsby project with BCMS, the BCMS CLI automatically generates Typescript types for you. These types are references to the properties created in all templates and their data types.

All types are located under the ~bcms/types/entry folder. Import these types into your pages and they act like the data you need.

Let’s see this in action. To perfectly demonstrate this, I’m going to create the footer Component.

Creating the Footer Component

Under the layout folder, create a new file named footer.tsx And then import react into your component.

import React from 'react';

Enter fullscreen mode Exit fullscreen mode

After that, import the BCMS data needed.

import { FooterEntryMeta } from '@/bcms/types';
Enter fullscreen mode Exit fullscreen mode

From the above code, I imported from the types folder, the FooterEntryMeta. If you view this file in your code editor, you’ll see it contains all the properties we added to our footer template as seen below.

Image description

After importing that, you will create an interface for the data you’re going to use in the component.

interface FooterProps {
  data: FooterEntryMeta;
}
Enter fullscreen mode Exit fullscreen mode

A typescript interface is a typescript’s way of typing an object. So here I declared a new object FooterProps which contains a data property, now that data property is what I will map to FooterEntryMeta which is already imported.

With that done, now, you can create the component that you will export.

export const Footer: React.FC<FooterProps> = ({ data }) => {}
Enter fullscreen mode Exit fullscreen mode

The React.FC syntax is a way of telling typescript that it is a functional component and it is how we can benefit from typescript’s intelligence.

Next, I declared <FooterProps> mandating that the prop type this component will receive will match <FooterProps>. And then I exposed the data value to the component to be able to use it.

Now let’s inspect what you’re going to get from that data attribute. Create a console.log statement that displays the data value. Use the useEffect react hook to display this message when the component renders,

//So first, import useEffect
import React, {useEffect} from 'react';

Enter fullscreen mode Exit fullscreen mode

Then you can use it in the component

export const Footer: React.FC<FooterProps> = ({ data }) => {
  useEffect(() => {
    console.log(data);
  });
};
Enter fullscreen mode Exit fullscreen mode

But this code itself is not what fetches the data, rather it expects to get the data prop and to render it. To be able to see the console value, you have to use the Footer component somewhere on the page.

To do that, create a PageWrapper.tsx component in the component folder. This component will act as a wrapper for each of the pages you will eventually create in our app. In there you will have a Header, children(the content of each page), a Footer, and additional head meta tags using react-helmet. But for this article, I will keep things simple and just use only the footer data there.

Image description

On line 3 of the above code, I’ll import a PageData type that ensures that the props received in this component match what I expect to receive. And I defined this type in a separate directory outside of the SRC directory.

So, create a new folder named types, and in that folder create the page-props.ts type which will contain the PageData type below.

//types/page-props.ts
import { FooterEntryMeta } from '../bcms/types/entry/footer';

export interface PageData<
  Page = { meta: { en: { title: string; } } },
> {
  location?: string;
  defaultTitle?: string;
  page: {
    bcms: Page;
  };
  footer: {
    bcms: {
      meta: {
        en: FooterEntryMeta;
      };
    };
  };
}

Enter fullscreen mode Exit fullscreen mode

Now with the imported Footer component into the new PageWrapper, it is time to use the on one of our pages. Iwill use it on the About page. On the pages folder under src, create a new page named about.tsx. The About page also needs its own types to validate props. Under the newly created types folder, create a subfolder named pages where you can then create the about.ts type validator.

import { AboutPageEntryMeta } from '../../bcms/types/entry/about_page';

export interface AboutPageData {
  meta: {
    en: AboutPageEntryMeta;
  };
}

Enter fullscreen mode Exit fullscreen mode

After that you can move on to create the about.tsx page as shown below.

import React from 'react';
import {  PageData } from '../../types/page-props';
import {  AboutPageData } from '../../types/pages/about';
import { PageWrapper } from '../../src/components/PageWrapper';
import { graphql } from 'gatsby';

interface AboutPageProps {
  data: PageData<AboutPageData>;
}
const AboutPage: React.FC<AboutPageProps> = ({
  data: { page, footer },
}) => {
  return (
    <PageWrapper page={page}  footer={footer}>
      <div>Hello World, this is the about page</div>
    </PageWrapper>
  );
};

export const query = graphql`
  {
    footer: bcmsFooter {
      ...Footer
    }
    page: bcmsAboutPage {
      ...AboutPage
    }
  }
`;
export default AboutPage;

Enter fullscreen mode Exit fullscreen mode

As you can see at the bottom of the page, there is an important export const "query". This is the graphql fetching the needed data, which we used above namely page and footer

You could also fetch the header data in same manner

export const query = graphql`
  {
    footer: bcmsFooter {
      ...Footer
    }
    page: bcmsAboutPage {
      ...AboutPage
    }
    header: bcmsHeader {
        ...Header
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

But since you’re not making use of it you will not import it.

Image description

If you try to run the project now, you will probably get a GraphQL-related error. This happens because you haven't defined a graphl fragment for each graphl data we're fetching ( for the about page that will be: footer, about(The page data) ). To do that, create a new graphql folder under the SRC folder. In there, you will make a file for each of the data we need using Graphql to fetch the data. For this case that will be about.ts and footer.ts

//
import { graphql } from 'gatsby';

export const query = graphql`
  fragment AboutPage on BcmsAboutPage {
    bcms {
      meta {
        en {
          title
          slug
          seo {
            title
            description
          }
          cover {
            _id
            caption
            alt_text
            height
            name
            src
            svg
            width
          }
          content {
            type
            value
            name
          }
        }
      }
    }
  }
`;

Enter fullscreen mode Exit fullscreen mode
  • -- Show some snippets ---

After this has been added, you can now navigate to http://localhost:8000/about/ and see the hello world on the footer component and the console.log statement on your browser.

So now you have data from the footer which you have successfully passed down to the footer component and also the about page data which lives in the about page. Let’s go ahead to style our footer component and make it look beautiful.

I will clean up the component remove the console.log and add the styling for the footer component.

import { FooterEntryMeta } from '@/bcms/types';
import { BCMSImage } from 'gatsby-source-bcms/components';
import { Link } from 'gatsby';
import React from 'react';

interface FooterProps {
  data: FooterEntryMeta;
}
export const Footer: React.FC<FooterProps> = ({ data }) => {
  return (
    <footer className="relative pt-8 pb-10 lg:pt-[86px] lg:pb-14">
      <div className="relative z-10 container">
        <div className="flex flex-col items-center">
          <a
            href={`mailto:${data.email.toLowerCase()}`}
            className="text-sm leading-none tracking-[-0.41px] text-appGray-200 mb-4 lg:text-lg lg:leading-none lg:mb-6"
          >
            {data.email}
          </a>
          <div className="flex items-center space-x-5 lg:space-x-8">
            {data.social &&
              data.social.map((item, index) => (
                <Link
                  key={index}
                  to={item.url}
                  target="_blank"
                  className="flex"
                  rel="noopener noreferrer"
                  aria-label={item.icon.alt_text}
                >
                  <BCMSImage
                    media={item.icon}
                    svg
                    className="w-5 h-5 lg:w-10 lg:h-10"
                  />
                </Link>
              ))}
          </div>
        </div>
      </div>
      <svg
        viewBox="0 0 375 384"
        fill="none"
        xmlns="http://www.w3.org/2000/svg"
        className="absolute bottom-0 left-0 w-screen h-[390px] pointer-events-none lg:hidden"
      >
        <g id="Ellipse_1" filter="url(#filter0_d_1105_1254)">
          <circle cx="188" cy="790" r="528" fill="#080808" />
        </g>
        <defs>
          <filter
            id="filter0_d_1105_1254"
            x="-590"
            y="0"
            width="1556"
            height="1556"
            filterUnits="userSpaceOnUse"
            colorInterpolationFilters="sRGB"
          >
            <feFlood floodOpacity="0" result="BackgroundImageFix" />
            <feColorMatrix
              in="SourceAlpha"
              type="matrix"
              values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
              result="hardAlpha"
            />
            <feOffset dy="-12" />
            <feGaussianBlur stdDeviation="125" />
            <feComposite in2="hardAlpha" operator="out" />
            <feColorMatrix
              type="matrix"
              values="0 0 0 0 1 0 0 0 0 0.294118 0 0 0 0 0.137255 0 0 0 0.5 0"
            />
            <feBlend
              mode="normal"
              in2="BackgroundImageFix"
              result="effect1_dropShadow_1105_1254"
            />
            <feBlend
              mode="normal"
              in="SourceGraphic"
              in2="effect1_dropShadow_1105_1254"
              result="shape"
            />
          </filter>
        </defs>
      </svg>
      <svg
        viewBox="0 0 1440 486"
        fill="none"
        xmlns="http://www.w3.org/2000/svg"
        className="absolute bottom-0 left-0 w-screen h-[480px] pointer-events-none max-lg:hidden"
      >
        <g filter="url(#filter0_d_1054_1063)">
          <circle cx="720" cy="1059" r="797" fill="#080808" />
        </g>
        <defs>
          <filter
            id="filter0_d_1054_1063"
            x="-327"
            y="0"
            width="2094"
            height="2094"
            filterUnits="userSpaceOnUse"
            colorInterpolationFilters="sRGB"
          >
            <feFlood floodOpacity="0" result="BackgroundImageFix" />
            <feColorMatrix
              in="SourceAlpha"
              type="matrix"
              values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
              result="hardAlpha"
            />
            <feOffset dy="-12" />
            <feGaussianBlur stdDeviation="125" />
            <feComposite in2="hardAlpha" operator="out" />
            <feColorMatrix
              type="matrix"
              values="0 0 0 0 1 0 0 0 0 0.294118 0 0 0 0 0.137255 0 0 0 0.5 0"
            />
            <feBlend
              mode="normal"
              in2="BackgroundImageFix"
              result="effect1_dropShadow_1054_1063"
            />
            <feBlend
              mode="normal"
              in="SourceGraphic"
              in2="effect1_dropShadow_1054_1063"
              result="shape"
            />
          </filter>
        </defs>
      </svg>
    </footer>
  );
};

Enter fullscreen mode Exit fullscreen mode

Now this will not be properly styled because the styling depends on TailwindCSS. Follow the official TailwindCSS documentation to install Tailwindcss onto the project and the styles will apply.

Image description

With this, I’ve been able to demonstrate how you can fetch data from BCMS using Gatsby and how BCMS makes it easy for us by automatically generating the data types created on the BCMS backend and being able to use them.

Now I will not go further and do the same for all the pages in this article, you can do that on your own using the knowledge you just gained. If you want to see the finished codebase for this application, then check the BCMS starters Gatsby podcast.

The starter contains a more robust setup and structure that allows the page data to be fetched along with the header and the footer and a content manager component to render the pages.

This pattern won’t be covered in this tutorial as I aimed to provide you with what you need to get up and running with Gatsby and BCMS and not how to structure your project. I will leave that to a future article.

Deployment

If you used a self-hosted BCMS instance like me, you now need to host it on a live server. You need to do this because it is currently only stored on our machine. However, with a BCMS Cloud Version, you won't have to worry about deployment because it will be handled for you, allowing us to continue using the same server from development into production.

To Deploy your local BCMS instance, follow the steps in this tutorial on the BCMS documentation. For now, DigitalOcean is the only server BCMS provides deployment steps for, we’ll keep updating it.

After deploying the instance, it's now time to deploy the frontend.

But first, keep in mind that your instance URL has been changed to a production URL, which means that instead of localhost:8080, you will be using a new URL provided by DigitalOcean or one that you have created on DigitalOcean. So before pushing your Frontend to production, it’s advisable to test that new URL.

To do that, go to all the places in your codebase where BCMS_API_ORIGIN is declared and change the value to your new URL. After doing that test your app to see if it is functioning as before, if all is successful it should be running as usual and you can now move on to deploy the Frontend.

You can deploy the Gatsby Frontend to Netlify easily by following the official guide on the Gatsby Website.

Top comments (0)