fionawhim

Personal site of Fiona Hopkins. Coding, crafting, gaming, and queer shit.

My custom theme for this blog is derived from Gatsby’s gatsby-theme-blog-core package, and I try to hew to it structurally, even as I build things on top of it. When adding comments to this blog I came across a part of Gatsby I hadn’t really explored: sourcing my own nodes, and associating them with nodes from another plugin.

Comments for this blog are stored in a Cloud Firestore database, keyed by the path (i.e. “slug”) of the post they belong to. I chose the path because it’s a stable identifier1, which I can’t guarantee of the Gatsby node IDs.

But node IDs are exactly what gatsby-theme-blog-core’s createPages lifecycle function passes in as context. Which means they are the inputs to the GraphQL query for making a post page.

So how do I query for my path-indexed comments when all I have is a post ID?

Here’s (roughly) what the post page query looks like with its ID inputs:

query ExtendedPostPageQuery(
  $id: String!
  $previousId: String
  $nextId: String
) {
  blogPost(id: { eq: $id }) {
    id
    excerpt
    body
    slug
    title
    tags
    keywords
    date(formatString: "LL")
  }

  previous: blogPost(id: { eq: $previousId }) {}

  next: blogPost(id: { eq: $nextId }) {}
}

The best way I found to bridge this divide is to use the createTypes function from Gatsby’s new schema customization API. The @link field extension lets us associate our comment’s path field with the slug field on MdxBlogPost nodes. Here’s the SDL you can pass to createTypes:

type PostComment implements Node @dontinfer {
  name: String!
  body: String!
  createdAt: Date! @dateformat

  gravatarHash: String!
  post: MdxBlogPost! @link(by: "slug", from: "path")
}

Okay, so now we can go from comment -> post. That’s a thing, but is it a useful thing for our problem? We’re trying to go from post -> comments.

It turns out that, thanks to Gatsby’s extensive filtering support, it is.

Here’s how we can add to the post query to get the comments, given a post’s ID:

query ExtendedPostPageQuery(
  $id: String!
  $previousId: String
  $nextId: String
) {
  blogPost(id: { eq: $id }) {}

  comments: allPostComment(
    filter: { post: { id: { eq: $id } } }
    sort: { fields: createdAt, order: DESC }
  ) {
    nodes {
      id
      name
      body
      gravatarHash
      createdAt(formatString: "LL")
    }
  }}

Since we made an association with @link, we can reach through the post field to filter by its id.2 Problem solved.

I’m pretty happy with using @link and I’m now going to look at other ways to make associations between my custom nodes (such as the ones for Projects and Sidebar Items) and the Mdx and MdxBlogPost nodes from gatsby-theme-blog-core. Querying via filter seems a lot nicer than listening for onCreateNode events and adding custom fields.

Alternatives

There are some other approaches I looked at:

  • I added a custom resolver to MdxBlogPost to create a comments field of type [PostComment!]!. I wasn’t too keen on adding fields to a type I didn’t own, and I ran into issues with tracking dependencies so that the pages would rebuild when a new comment came in. I didn’t even get to adding sort options.
  • I also tried making a map of slug -> id for posts, and referencing it when I called createNode for each comment so that they would natively have a postId field to filter by. This would rely on the order of sourceNodes happening first for posts, however, and added a small chunk of code. The @link solution is far cleaner and straightforward.

Caveats

One piece that I’m slightly unsatisfied with is getting a comment count for each post when rendering the index page. I’m currently using group to get a list of comment counts by ID, and then creating a Map from that that I can lookup when rendering each post.

query LatestBlogPostsQuery {
  allBlogPost(
    sort: { fields: [date, title], order: DESC },
    limit: 10
  ) {
    nodes {
      ...
    }
  }

  commentCounts: allPostComment {
    group(field: post___id) {
      totalCount
      fieldValue
    }
  }
}

And then before rendering:

const postToCommentCountMap = new Map<string, number>();

data.commentCounts.group.forEach(({ fieldValue, totalCount }) => {
  if (fieldValue) {
    postToCommentCountMap.set(fieldValue, totalCount);
  }
});

(See: latest-blog-posts.tsx)

Ideally perhaps comments could be a Group Connection field on MdxBlogPost, but I couldn’t find any documentation for creating a custom one. runQuery doesn’t seem to include helpers for pagination or counts.

Is there something I’m missing? Is it right to be reluctant to add fields and resolvers on to nodes generated by a different plugin? Let me know in the comments, or at @fionawhim on Twitter.


  1. Or, if I do ever change it, I would do so deliberately and could update comments to match.
  2. Is this efficient? I don’t know, but also I don’t need to care! All of this happens during build time. And I don’t have very many posts or comments.

Comments

Post a Comment