A Work in Progress

developer's blog by Wei
lots of work in progress notes
TimelineCredits

Building A Multi Author Blog with Gatsby

April 29, 2019

Note This is a very flat post where I list out all the steps I did to create a multi author blog. You may not want to read this because it can be very boring. It's just not one of those post I give a lot of thought in the creative side of things.

And if you don't want to read this post but still want the feature, you can check out this starter I created after this post, demo on Netlify.

starter


So recently I want to create a multi-author blog. Besides marking each post by its own author, I'd like to have a place to display the authors' details as well as being able to query posts by each author on that author's page.

At first it seems to me a fairly common request. "Fairly common" means I expect there is already a starter or a plugin that does that. A few Googling did not suggest desired result. Instead, I see people asking questions and requesting features in Gatsby's GitHub repo. Not a good sign 🙈 I remember for a fact that Gatsby accepts a mapping in gatsby-config which allows us to add a mapping to author details. So adding a couple of pages on top of that should follow directly, isn't it?

To verify whether my naive idea will work or not, I decided to give it my own shot and see how.

Spoiler: The feature is possible, but not without a hiccup. I guess that's the fun part.

Implementation

In this post, we'll cover the following topics:

  • Map author from front matter to an author details specified with YAML
  • Create all authors page
  • Create author detail pages

Map the author field in posts' front matter

So we want to create a mapping between an author id and author details. Gatsby supports creating mapping between node types. We can specify that inside gatsby-config.js with an optional field called mapping, docs here. It accepts mappings defined in a single file, or multiple files with a mapping scheme on file names. It also supports multiple file types such as YAML, JSON, markdown, etc.

We'll follow a common practice that specifies multi author information with YAML.

In order to parse the YAML file, add gatsby-transformer-yaml to your package.

$ yarn add gatsby-transformer-yaml

And in the plugins

// gatsby-config.js
module.exports = {
  plugins: [`gatsby-transformer-yaml`],
}

Note here that the file we later create needs to live in a directory that gets picked up by the file source plugin. If you intend to create a separate directory, say mappings, you may need to:

// gatsby-config.js
module.exports = {
  plugins: [
    `gatsby-transformer-yaml`,
    {
      resolve: 'gatsby-source-filesystem',
      options: {
        name: 'mappings',
        path: `${__dirname}/mappings/`,
      },
    },
  ],
}

Then, to tell Gatsby to map author to author detail, specify mapping as a field in your configurations like:

// gatsby-config.js
module.exports = {
  mapping: {
    'MarkdownRemark.frontmatter.author': `AuthorYaml`,
  },
  plugins: [`gatsby-transformer-yaml`],
}

Next, create the author.yaml file that contains the author details. Take note that the this file needs to be picked up by gatsby-source-filesystem. Consider putting it under the root of your blogs directory. With gatsby-advanced-starter I'm using, I put it under the directory /content.

# content/author.yaml

- id: Curious Cat
  bio: Very curious about this world and blogging whenever learning something new
  twitter: 'nonexistencecuriouscat'

- id: Wei Gao
  bio: First curious cat at Gatsby Curious Community. Blogs at aworkinprogress.dev.
  twitter: 'wgao19'

With the above mapping set up, Gatsby will now treat the author field you specify inside our posts' frontmatter as the id to an author. And it will map it to the actual details of that author in the author.yaml file.

Now, the author field in frontmatter is no longer a string, but of shape specified in our authors mapping. To pick up the author information in a post, update the GraphQL queries when needed. Once again with the gatsby-advanced-starter I'm using, this would come down to:

// templates/post.jsx

export const pageQuery = graphql`
  query BlogPostBySlug($slug: String!) {
    markdownRemark(fields: { slug: { eq: $slug } }) {
      html
      timeToRead
      excerpt
      frontmatter {
        title
        cover
        date
        category
        tags
-       author
+       author {
+         id
+         bio
+         twitter
+       }
      }
      fields {
        nextTitle
        nextSlug
        prevTitle
        prevSlug
        slug
        date
      }
    }
  }
`;

Create the list of authors page

We need to create the author page. Inside gatsby-node.js, let's drop in the template for authors page, and create the page. We don't need to pass in anything during page creation, we can later on query the all authors information within the page.

// gatsby-node.js

exports.createPages = ({ graphql, actions }) => {
  const { createPage } = actions;

  return new Promise((resolve, reject) => {
    const postPage = path.resolve("src/templates/post.jsx");
    const tagPage = path.resolve("src/templates/tag.jsx");
    const categoryPage = path.resolve("src/templates/category.jsx");
+   const authorsPage = path.resolve("src/templates/Authors/index.jsx");
    resolve(
      graphql(
        `
          {
            allMarkdownRemark {
              edges {
                node {
                  frontmatter {
                    tags
                    category
                  }
                  fields {
                    slug
                  }
                }
              }
            }
          }
        `
      ).then(result => {
        // ... other page creations

+       createPage({
+         path: `/authors/`,
+         component: authorsPage
+       });

      })
    );
  });
}

And inside this page, we can query all authors using allAuthorYaml:

// src/templates/Authors/index.jsx

import React from 'react'

export default ({
  data: {
    allAuthorYaml: { edges: authorNodes },
  },
}) => (
  <div>
    {authorNodes.map(({ node: author }, index) => (
      <div key={`author-${author.id}`}>{author.id}</div>
    ))}
  </div>
)

export const pageQuery = graphql`
  query AuthorsQuery {
    allAuthorYaml {
      edges {
        node {
          id
          bio
          twitter
        }
      }
    }
  }
`

Create the author pages

An author’s page displays the author details and lists all the posts by that author. So our page query will include two parts, one queries the author information, and the other queries the posts.

Let me outline the few things we need to do first:

  • Add a field authorId to each post during onCreateNode, inside gatsby-node.js * see notes below why we need this
  • Create pages for each author, also implemented at gatsby-node.js during createPages. And we pass the author's id in context to be later consumed to query current author's posts.
  • In author's page, query:

    • author's details
    • all posts from this author

*There is a hiccup, currently we cannot filter posts with mapped schema. So the normal filter that looks like

allMarkdownRemark(
  filter: {
    fields: { author: { id: { eq: $authorId } } }
  }) {
    edges {}
}

will not work.

I’m sure there are works around. What I eventually came up with was to create a field authorId for each post, and filter with that created field. I'm not very sophisticated with GraphQL so if there is a standard or better way to do this, please let me know.

Create authorId field to post nodes

// gatsby-node.js

exports.onCreateNode = ({ node, actions, getNode }) => {
  // ...

  if (Object.prototype.hasOwnProperty.call(node.frontmatter, 'author')) {
    createNodeField({
      node,
      name: 'authorId',
      value: node.frontmatter.author,
    })
  }
}

Query authorId in the createPages query

Once we've created this field, it will show up on the field schema of our queries. We can also use this field to feed in to the context for author's pages that will later be consumed to query author's posts:

// gatsby-node.js

// the query for createPages:
graphql(
  `
    {
      allMarkdownRemark {
        edges {
          node {
            frontmatter {
              tags
              category
            }
            fields {
              slug
+             authorId
            }
          }
        }
      }
    }
  `

Create author pages

Now we can create authors' pages

// gatsby-node.js

// resolves from the query from 👆
const authorSet = new Set();
result.data.allMarkdownRemark.edges.forEach(edge => {
  if (edge.node.fields.authorId) {
    authorSet.add(edge.node.fields.authorId);
  }
}

// create author's pages inside export.createPages:
const authorList = Array.from(authorSet);
authorList.forEach(author => {
  createPage({
    path: `/author/${_.kebabCase(author)}/`,
    component: authorPage,
    context: {
      authorId: author
    }
  });
});

Component implementation

Finally, we can implement the front end component that:

  • queries author details as well as filtered posts by this author
  • renders the data
// templates/Author/index.jsx
import React from 'react'

export default ({
  data: {
    authorYaml: { id, bio, twitter },
    allMarkdownRemark: { edges: postNodes },
  },
}) => (
  <div>
    <div>
      <h2>{id}</h2>
      <a href={`https://twitter.com/${twitter}/`} target="_blank">
        {`@${twitter}`}
      </a>
      <p>
        <em>{bio}</em>
      </p>
    </div>
    <hr />
    <p>{`Posted by ${id}: `}</p>
    {postNodes.map(({ node: post }, idx) => (
      <div key={post.id}>
        <a href={post.fields.slug}>{post.frontmatter.title}</a>
      </div>
    ))}
  </div>
)

export const pageQuery = graphql`
  query PostsByAuthorId($authorId: String!) {
    allMarkdownRemark(filter: { fields: { authorId: { eq: $authorId } } }) {
      edges {
        node {
          id
          frontmatter {
            title
            author {
              id
            }
          }
          fields {
            authorId
            slug
          }
        }
      }
    }
    authorYaml(id: { eq: $authorId }) {
      id
      bio
      twitter
    }
  }
`

If you'd like, you may check out this feature at this CodeSandbox.

And you may as well directly use this starter that I created following this post, demo on Netlify.

starter

Till next time 🤞

To wrap up, we can map node types by specifying mapping in gatsby-config.js. This API is designed to fit generic use cases that involve mapping certain node type to another. And it is flexible in terms of types of files it accepts for the mapping, as well as how the mapping is defined.

However, mapped node types are not supported in query filtering, yet 😭 Hack this around by duplicating a field for authorId that we later consume for filtering.

References

© 2019 - 2021 built with ❤