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.
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 duringonCreateNode
, insidegatsby-node.js
* see notes below why we need this - Create pages for each author, also implemented at
gatsby-node.js
duringcreatePages
. 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.
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
- Documentation missing installation step, #13134
- Add
mapping
documentation, #4007 - How to query all posts by an author when you have multiple authors per post, #7251
- Gatsby Config documentation on mapping
- Gatsby Node API on
createPage
gatsby-transformer-yaml
- Gatsby Curious Community CodeSandbox for the multi-author feature only, GitHub
- Handle multiple authors in Jekyll
- Pandoc title block multiple author support
- YAML