Skip to content

Gatsby generate related posts at build time

GatsbyQuick TipJavascriptReact

Gatsby is exceptional. There are no two ways about it. But a static site can pose some challenges.

One challenge devs building Gatsby sites come across is: how can I automatically generate "related" posts at build time?

Until the Gatsby team gives us a way to make multiple queries to build one page (main page query and then sub queries), we'll have to find workarounds.

One solution I've seen is to query all post data and then filter through them at runtime. It should be fairly obvious that this might not be a winning strategy. What happens when you've got 10,000 posts? 20,000?

I wanted a solution for NimbleWebDeveloper where I could have some pseudo related posts generated automatically, at build time.

This method should work regardless of where you're pulling content from. In our case the posts are coming from Sanity.io.

createSchemaCustomization

A little while back, the Gatsby team introduced the Schema Customization API. The schema customization API is quite helpful for a number of things; we can set default field values (really useful when working with datasets where missing data is not defined), and extend existing fields.

We're going to use it to add an 'artificial' field to our posts which uses the Gatsby runQuery API to look for related posts using any and all of our existing data sources, and do it at build time.

Don't believe me? Read on!

What you're going to need

To use this example, you're going to need a Gatsby site, with some posts from some source (where they're coming from shouldn't matter), and some way of linking posts to each other. I've gone with categories but you might use tags, categories and tags, a text match of the titles, it's up to you.

Gatsby Node APIs

The Schema Customization API is part of the Gatsby Node APIs. So if you haven't already got a gatsby-node.js file in your project, go ahead and create one in your Gatsby project root.

// gatsby-node.js

//Hook into the createSchemaCustomization API
//This hook runs after all our nodes have been created
exports.createSchemaCustomization = ({ actions, schema }) => {
  //The createTypes action allows us to create custom types
  //and modify existing ones
  const { createTypes } = actions
  
  //...
  // Create our schema customizations
  //...
}

Associate posts

You'll need some way to associate posts with other posts. In my case I have categories, and each post has many categories, so my data looks something like this;

{
  "posts":[
    {
      "id":"...",
      "slug":"...",
      "title":"...",
      "categories":[
        {
          "id":"<CategoryID>"
        },
        {
          "id":"<CategoryID>"
        },
        {
          "id":"<CategoryID>"
        }
      ]
    }
  ]
}

How you do this is up to you, just have some way you can query posts (in GraphQL) by a category, tag or some other identifier.

(To find out how your data can be queried, use the GraphiQL tool which by default is located at http://localhost:8000/__graphql)

Generate related posts

Finally, we can create a new field on our post type which contains our related posts.

// gatsby-node.js

//Hook into the createSchemaCustomization API
//This hook runs after all our nodes have been created
exports.createSchemaCustomization = ({ actions, schema }) => {
  //The createTypes action allows us to create custom types
  //and modify existing ones
  const { createTypes } = actions
  
  // Create our schema customizations
  const typeDefs = [
    // Replace "sanity_post" with your _typename of your post type
    "type sanity_post implements Node { related: [sanity_post] }",
    schema.buildObjectType({
      name: "sanity_post",
      fields: {
        related: {
          type: "[sanity_post]",
          //The resolve field is called when your page query looks for related posts
          //Here we can query our data for posts we deem 'related'
          //Exactly how you do this is up to you
          //I'm querying purely by category
          //But you could pull every single post and do a text match if you really wanted
          //(note that might slow down your build time a bit)
          //You could even query an external API if you needed
          resolve: async (source, args, context, info) => {
            //source is the current (post) object
            //context provides some methods to interact with the data store
            
            //Map a simple array of category IDs from our source object
            //In my data each category in the array is an object with a _id field
            //We're just flattening that to an array of those _id values
            //E.g. categories = ["1234", "4567", "4534"]
            const categories = source.categories.map((c) => c._id)
            
            //If this post has no categories, return an empty array
            if (!categories.length) return []
            
            //Query the data store for posts in our target categories
            const posts = await context.nodeModel.runQuery({
              query: {
                filter: {
                  //We're filtering for categories that are sharedby our source node
                  categories: { elemMatch: { _id: { in: categories } } },
                  //Dont forget to exclude the current post node!
                  _id: { ne: source._id },
                },
              },
              //Change this to match the data type of your posts
              //This will vary depending on how you source content
              type: "sanity_post",
            })

            //Gatsby gets unhappy if we return "null" here
            //So check the result and either return an array of posts,
            //or an empty array
            return posts && posts.length ? posts : []
          },
        },
      },
    }),
  ]
  
  createTypes(typeDefs)
}

Use it!

If you restart your development instance (gatsby develop) and navigate to your GraphiQL tool (usually http://localhost:8000/__graphql) you should see your posts now have an additional field related available!

We didn't put any limits on the related field, but you might want to limit it in the resolver to only a couple of results. Or you can do that in your page query.

Now that you can access the data, you can use it to build your page. Like so;

(This query is specifically for data sourced from Sanity, your query will be a little different depending on your data)

// templates/post.js

export const query = graphql`
  query SanityBlogPost($slug: String) {
    post: sanityPost(slug: { eq: $slug }) {
      slug
      title
      #... other fields
      related: {
        slug
        title
        #... other fields
      }
    }
  }
`