Apostrophe

Building a Gatsby site with ApostropheCMS data: Part 1

This Apostrophe + Gatsby tutorial is for developers looking to use Apostrophe as a back end content source for their Gatsby site. While approaching beta, ApostropheCMS 3.0 is ready for developers to get things connected quickly with the best tools.

Site building with a headless CMS takes many forms, but one of the most popular these days is using a static site builder. Like others, Gatsby, the most popular of these tools, turns a set of templates and content files into (relatively) simple HTML files that can be served to browsers super quickly. With the 3.0 version, ApostropheCMS is ready to support these projects and help developers get things connected quickly.

This tutorial is written for developers looking to use Apostrophe as a back end content source for their Gatsby site. I hope that project managers, marketing folks, and others will be able to get something out of this as well, but we will be using technical directions meant for people used to developing workflows.

This is Part one of two, where we'll cover how to use the Apostrophe source plugin for Gatsby and use the data it makes available in your Gatsby site. Part two will go into some more advanced, and optional, ways to use Apostrophe page data and page structure in a Gatsby site.

What is Gatsby, again?

Gatsby is a static site framework that helps create really fast websites with the flexibility and extensibility of the React library. It’s very easy to add pages to a Gatsby site by creating a new file for each page you want on the website, but that doesn’t scale well. Using a headless CMS, like Apostrophe, you can benefit from easier content creation with a user interface, then Gatsby can suck all that up and process it into its super fast web pages.

If you have not gotten to know Gatsby yet, it would be worth going through their initial tutorial. You can get through this tutorial without doing so, but we are going to skip over Gatsby code structure and React conventions to focus on how to hook it up with Apostrophe.

🆒. So how does this all come together? Let’s walk through that together.

The Apostrophe app

If you have experience building Apostrophe 2 websites, this is your chance to start playing with an Apostrophe 3 project since the Gatsby source plugin (we’ll get to that) will depend on A3 APIs.

As this isn’t a full Apostrophe 3 tutorial, let’s start out with some working code and go through the important parts. First, make sure your system is ready to run an Apostrophe 3 site. Then go ahead and download this site built for the headless demo. Once you do, and cd into the project in your terminal, run:

npm install && node app @apostrophecms/user:add admin admin

When prompted, provide a password for the "admin" user. Then run npm run build && npm start. Go to http://localhost:3000/login and enter the username ("admin") and password you just added.

You now have an Apostrophe 3 site up and running! But what is going on in there?

First, we've added some custom piece types. We'll pretend we're making a summer camp website, so I've added piece types for "programs" (summer camp sessions) and "staff members." There's also a "Default page" page type for simple, flexible, content pages. Finally, there is a custom "Two column" layout widget with content areas inside.

// app.js
require('apostrophe')({
  shortName: 'a3-headless-demo',

  modules: {
		// ... other module configuration
    // A custom widget with two columns
    'two-column-widget': {},

    // A page type for ordinary pages
    'default-page': {},

    // A piece type for camp programs
    'program': {},
    // A piece type for camp staff
    'staff-member': {}
  }
});

You should dig into the code of those modules (in the /modules directory), but there are a few important things to highlight:

  • The custom piece types and the widget include a variety of fields, from simple string fields to nested content area for adding more content widgets.
  • The staff one also includes an area supporting a single image widget for adding their photo.
  • All of this varying data will need to be received and displayed by the Gatsby site. String fields are simple, but what about those flexible content areas? Keep that in mind as we go on.

Another file to check out is modules/@apostrophecms/express/index.js. In that file you'll find this API key configuration:

apiKeys: {
  q7vKZQuqkWf: {
    role: 'admin'
  }
}

That creates an API key, q7vKZQuqkWf, with full admin permissions. We'll use that later to access the REST APIs.

Apostrophe uses in-context content editing. When using Apostrophe for both the content management and serving web pages, this is great because pages and content areas appear to the editor the same as they do to the logged-out visitor. If Apostrophe is only the content editor and manager, how do we maintain that connection between editing appearance and the ultimate end result?

Using Tailwind CSS for consistent styles

If the problem is that Gatsby won't have the Apostrophe app's front end styles to maintain consistent appearance, the solution needs to somehow involve sharing styles between Gatsby and Apostrophe. There could be many ways to implement such a solution. A production application might be well served by a custom front end build and scripts that copy stylesheets from one app to the other. Fortunately, CSS frameworks offer drop-in styles that can be dropped-in as easily in the Apostrophe app as in the Gatsby app. Shared styles with none of the hours of work!

Tailwind CSS is one such CSS framework that is quite popular and liked by many developers. To be honest, we don't use it on the Apostrophe team, but this seemed like a great opportunity to try it out and a great use case for such a framework. It is a "utility class" framework, using classes like mb-6 (adding margin bottom), container (establishing a container wrapper), and flex (adding Flexbox behavior).

Since the Apostrophe app is not going to be the ultimate front end for this website, minifying the Tailwind CSS styles in the browser is not a huge concern. Only editors will ever visit this Apostrophe app. To get Tailwind in the app quickly, I used their CDN to add it in the wrapper template at views/layout.html:

{% block extraHead %}
  <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
{% endblock %}

Once my browser has this cached, it'll load quickly and I can move on to using the styles.

For one example, the two column widget looks like this:

<div class="flex flex-wrap md:-mx-3">
  <div class="w-full md:my-3 md:px-3 md:w-1/2">
    {% area data.widget, 'one' %}
  </div>
  <div class="w-full md:my-3 md:px-3 md:w-1/2">
    {% area data.widget, 'two' %}
  </div>
</div>

It's using Flexbox positioning and at the "medium" breakpoint it puts the inner div tags side-by-side. If you want to go deeper on Tailwind, please check out their documentation.

So we have an Apostrophe app with some basic styles. This is not a CSS demo, so we won't go wild with styles, but regardless, what is most critical is that editors have a feel for the layout of content they are editing. Things can get fancier on the Gatsby end, but as long as the layout styles from Tailwind are respected, you have enough parity to go pretty far.

If you're following along, add a few pages, programs, and staff members to your Apostrophe site for some test content, because it's time to start pulling that into a Gatsby site! Then leave the Apostrophe site running so Gatsby can request data from it.

Pulling Apostrophe content into Gatsby

Go to your projects directory (mine is ~/Sites) in a new terminal window. Make sure you are all set up for Gatsby and its CLI tool.

Once you have that working, clone, fork, or download the demo for this Gatsby site from https://github.com/apostrophecms/gatsby-demo-apostrophe. There is a lot to learn for Gatsby and Tailwind CSS, but we want to stay focused on our work bringing Apostrophe data into the site, so this demo code will help us skip some steps.

Go to this project in your terminal and open it in your code editor.

Configuring the Apostrophe source plugin

Gatsby uses "source plugins" to access files/data that aren't normal Gatsby page files and turn them into GraphQL data. We'll configure the source plugin for Apostrophe.

Open gatsby-config.js and add gatsby-source-apostrophe to the plugins array:

module.exports = {
  plugins: [
    // ... PostCSS and Google Fonts plugins already there
    {
      resolve: "gatsby-source-apostrophe",
      options: {
        apiKey: process.env.APOS_KEY,
        baseUrl: "http://localhost:3000",
        pieceTypes: ["program", "staff-member"],
      },
    },
  ],
}

This is identifying the Apostrophe source plugin (resolve: "gatsby-source-apostrophe",) and setting options for:

  1. the API key we set in the Apos app (apiKey: process.env.APOS_KEY,)
  2. the baseURL for the Apostrophe app (baseUrl: "<http://localhost:3000>", since we're running it locally),
  3. and the piece types that we're going to want from Apostrophe[1] (pieceTypes: ["program", "staff-member"],)

Before starting this up, you'll also need to add a .env file with the API key we set in the first section:

APOS_KEY=q7vKZQuqkWf

While testing this stuff out, you could put the API key directly in the config file, but this is a good practice to maintain.

We can now start up the Gatsby site with gatsby develop. (You can leave this running while working, but if it ever crashes as you add code, simply run that to start up again.) Once it is started, visit http://localhost:8000/___graphql, the GraphQL query interface that comes with Gatsby. You should see something like this (assuming your Apos site is still running):

The highlighted fields in the interface are GraphQL fields to access Apostrophe data. The fields to access single nodes are prefixed with apos, and the "get all" fields are prefixed allApos, per the convention. So with allAposCorePage, allAposProgram, allAposStaffMember, aposCorePage, aposProgram and aposStaffMember we can now query Apostrophe data directly in the Gatsby site.

What is the Apostrophe source plugin doing?

I realize that this last step was a bit of "hand waving" covering up a bunch of work to request Apostrophe data of several types and convert it all to GraphQL nodes that Gatsby understands. Well, that's sort of what a source plugin is meant to do. You are welcome to explore the source plugin code, but put simply, the plugin:

  • requests all individual piece documents of each piece type you configured, helping to handle request errors,
  • generates Gatsby nodes for each piece,
  • requests all public individual Apostrophe page documents, including fully rendered HTML for each page, and
  • generates Gatsby nodes for each page.

Gatsby has its own system of querying data from a GraphQL database. The plugin lets you focus on that rather than also having to request data from the Apostrophe database as well.

Adding a program listing

Our program data is simplest, so let's start there. I added programs in the Apostrophe site for a kids camp, pre-teen camp, and teen camp with their own age groups, dates, and prices. Let's add a page listing each of our programs.

First, open src/pages/programs.js. We'll be making a GraphQL query here, so first add import { graphql } from "gatsby" to top of the file:

import React from "react"
import { graphql } from "gatsby" // <=== Adding this.
// Layout is a wrapper template
import Layout from "../components/Layout"

Toward the bottom of the file, under the default export, we'll add our GraphQL data query. We are querying for all program data, so we'll use the allAposProgram query field and specify the property we need:

export const query = graphql`
  query ProgramList {
    allAposProgram() {
      nodes {
        title
        startDate
        endDate
        description
        ageGroup
        cost
      }
    }
  }
`

We'll get all the program data from this, but we can go a little further to put the returned programs in a logical order here so we don't have to sort them later.

Replace allAposProgram() with allAposProgram(sort: { fields: [ageGroup], order: ASC }). That will sort the camp programs by age group. For our simple demo this should be enough.

If you were to log the data object coming into the default function above, it should now have an allAposProgram property. We can assign the nodes from that to a variable:

export default function Programs({ data }) {
  const programs = data.allAposProgram.nodes

We now have this Apostrophe data to display! It's time to drop in a bunch of JSX:

<Layout>
  <h1 className="text-2xl mb-6">Camp Sessions</h1>
  <div>
    {programs.map(item => {
      return (
        <section className="mb-6" key={item.id}>
          <h2 className="text-xl">{item.title}</h2>
          <ul>
						{/*
              The following pattern is a conditional, checking if we 
              have a `cost` value. We'll continue to use this in other places.
            */}
						{item.cost &&
              <li>Price: ${item.cost}</li>
            }
            {item.ageGroup &&
              <li>Ages: {ageGroupToRange(item.ageGroup)}</li>
            }
            <li>
              Dates: {formatDate(item.startDate)} to {formatDate(item.endDate)}
            </li>
          </ul>
          {item.description && <p className="mt-3">{item.description}</p>}
        </section>
      )
    })}
  </div>
</Layout>

Ultimately, this is mapping over the program nodes and returning some JSX markup for each one. Each program in the array is assigned a key using a Gatsby data ID (key={item.id}) and the program data is structured in some HTML. We're using two helper functions, included in the starter code, ageGroupToRange and formatDate to make things look nicer, but so far this isn't anything too fancy (assuming you've used React before).

Depending on the programs you added to your apostrophe site, you should have something like this at http://localhost:8000/programs.

In addition to keeping things very simple, you might notice Tailwind CSS classes in the programs page template. The starter code for this Gatsby site included the Tailwind code all configured for you, so the same structure we had while editing the Apostrophe site is here as well!

Adding the camp staff members

Adding the staff listing will work mostly the same way. Open up src/pages/staff.js to start editing it.

First, add import { graphql } from "gatsby" at the top of the file so we can make GraphQL queries.

At the bottom of the file, we'll add our query:

export const query = graphql`
  query StaffQuery {
    allAposStaffMember {
      nodes {
        id
        jobTitle
        title
        funFact
        photo {
          _rendered
        }
      }
    }
  }
`

This looks mostly the same as the programs query with two big differences. First, there's no sorting. No big deal there since it actually got simpler (in a production site you might want to be sorting, of course). Second, the photo node property has a sub-property identified, _rendered. You could certainly get the full photo object here. If we query the photo object for a staff member (you added them in the Apostrophe sites with photos, right?), we'll see something like:

{
  "data": {
    "aposStaffMember": {
      "photo": {
        "_id": "ckkelgygm00j52a67lhd48knc",
        "metaType": "area",
        "_edit": true,
        "_docId": "ckkeljwi10066nb3r2wy7jo36:en:published",
        "_rendered": "\n<div class=\"apos-area\">\n\n  <img class=\"rl-image mb-4\"\n    srcset=\"http://localhost:3000/uploads/attachments/ckkelhwm20060nb3rpgr9qph8-photo-by-luiza-braun-on-unsplash.max.jpg 1600w, http://localhost:3000/uploads/attachments/ckkelhwm20060nb3rpgr9qph8-photo-by-luiza-braun-on-unsplash.full.jpg 1140w, http://localhost:3000/uploads/attachments/ckkelhwm20060nb3rpgr9qph8-photo-by-luiza-braun-on-unsplash.two-thirds.jpg 760w, http://localhost:3000/uploads/attachments/ckkelhwm20060nb3rpgr9qph8-photo-by-luiza-braun-on-unsplash.one-half.jpg 570w, http://localhost:3000/uploads/attachments/ckkelhwm20060nb3rpgr9qph8-photo-by-luiza-braun-on-unsplash.one-third.jpg 380w, http://localhost:3000/uploads/attachments/ckkelhwm20060nb3rpgr9qph8-photo-by-luiza-braun-on-unsplash.one-sixth.jpg 190w\"\n    src=\"http://localhost:3000/uploads/attachments/ckkelhwm20060nb3rpgr9qph8-photo-by-luiza-braun-on-unsplash.full.jpg\"\n    alt=\"photo by luiza braun on unsplash\"\n  />\n</div>\n"
      }
    }
  },
  "extensions": {}
}

Because Apostrophe area data is generally best used via the widgets' templates, the source plugin provides you with the rendered widget template on the _rendered property rather than providing all the often-confusing data. So that's why our query for staff members includes the photo sub-property _rendered. That's your direct access to a rendered area (including this single photo).

With this GraphQL query added to the page, we can add our JSX. Like the allAposProgram data, I'll suggest assigning data.allAposStaffMember.nodes to a staff variable:

export default function Staff({ data }) {
  const staff = data.allAposStaffMember.nodes

Then our JSX template:

<Layout>
  <h1 className="text-2xl mb-6">Camp Staff</h1>
  <div>
    {staff.map(item => {
      return (
        <section className="mb-4" key={item.id}>
          <h2 className="text-xl">{item.title}</h2>
          <p className="text-lg">{item.jobTitle}</p>
          {item.funFact && <p>Fun fact: {item.funFact}</p>}
          {item.photo && item.photo._rendered && (
            <div
              dangerouslySetInnerHTML={{ __html: item.photo._rendered }}
            />
          )}
        </section>
      )
    })}
  </div>
</Layout>

Again, this is similar to the program template code. We're mapping over the staff nodes array and putting data properties into HTML. But now there's this scary dangerouslySetInnerHTML thing as well. This is an intentionally scary React attribute, forcing you to understand that injecting HTML from some other source into your app can be very risky.

In our case, this is HTML that Apostrophe rendered for us from a core image widget, so we can trust that it's safe. If all of the rendered HTML you're getting from Apostrophe is template code that would be safe to put on an Apostrophe site, then it's safe to put here. If you have allowed something like the raw HTML widget, @apostrophecms/html, in your Apostrophe app, you should be as cautious about using that code here as you would if served directly from Apostrophe.

That word of caution aside, we're done with the staff page!

If you also got photos of people from Unsplash, your page might look something like this (at http://localhost:8000/staff):

Turning Apostrophe page data into Gatsby pages

You could clearly get more complex with your piece data than that, but ultimately they are pieces of structured content (get it?) that you choose to display in a page or across pages in some way. Using Apostrophe pages in a Gatsby site raises some new questions:

  • If pages I created in Apostrophe are mostly self-contained web pages already, how should we use that data in this new context?
  • We know the home page exists in Apostrophe, but as editors add new pages, how do we make new, individual web pages from them in Gatsby?

Let's take those one at a time. The first one is as simple or complex as your design, just like with piece data. The home page is a good place to show this.

In the Gatsby codebase, open src/pages/index.js. This is the Gatsby site's home page. Again, add import { graphql } from "gatsby" to the top of the file so we can make a GraphQL query. This time, we'll be using the GraphQL aposCorePage field, not one of the allApos fields, since we want a single, specific page.

This query is the simplest yet:

export const query = graphql`
  query HomeContent {
    aposCorePage(slug: { eq: "/" }) {
      _rendered
    }
  }
`

We're using the aposCorePage field and specifying that we want the piece that has a slug value equal to '/', our home page from Apostrophe. All we need from that is the _rendered property.

Like Apostrophe areas, pages are usually used as fully rendered templates, so the Apostrophe source plugin includes the full page as a string of HTML on the _rendered property. If you want the granular properties of the page document they are there, ready to be queried, but this extra affordance is there if you want it all rendered together. Our example page templates in the Apostrophe app, at modules/@apostrophecms/home-page/views/page.html and modules/default-page/views/page.html are intentionally very simple to make them easy to use in Gatsby.

With the HomeContent query added, we can add the JSX template for the home page:

export default function Home({ data }) {
  const page = data.aposCorePage

  return (
    <>
      <Layout>
        <h1 className="text-2xl mb-6">Welcome to Camp Rainbow Lake</h1>
        {page._rendered && (
          <div dangerouslySetInnerHTML={{ __html: page._rendered }} />
        )}
      </Layout>
    </>
  )
}

Similar to the staff member page's photos, we are simply adding the Apostrophe-rendered HTML string under the page title. In my Apostrophe site I used the two-column widget and and an additional photo widget to add some picture and text, resulting in a Gatsby page that looks like this (http://localhost:8000):

I'll note that we haven't added any CSS to the Gatsby site other than Tailwind CSS, some font families (to fancy things up) and some bottom margin for rich text paragraphs. The two column layout you see here is thanks to the Tailwind CSS classes that were added in Apostrophe and thus were included in the rendered HTML. Since we also include Tailwind in the Gatsby site, we get the same layout with no extra work. Not bad!

Doing more with pages

Adding rendered page content for a known page (such as the home page) might be simple enough. You might want to make use of any number of Apostrophe pages through the Gatsby GraphQL database or even generate Gatsby pages and navigation based on that page data. We'll go into that in the second part of this tutorial.

You can see the state of the demo Gatsby site built here as the part-1 branch in the repo. Please let us know if you are someone who builds with both Gatsby and Apostrophe already or if you are interested in the possibilities here on Twitter or in the Apostrophe Discord chat. Do you have questions or thoughts on how to improve the Gatsby integration? We'd love to hear from you.

 

[1] It's worth noting here that the Apostrophe source plugin for Gatsby is in beta release at the time of writing. We plan on a future feature where all piece types are included by default.

Sign up for the latest product news and updates.

Apostrophe