Previously, we covered how seemingly innocuous queries can prevent your themes from being composable when used with other themes and even sites you didn't plan for. In this post, we'll cover the core issue a bit more directly.
To refresh your memory, we explored a query for Mdx
nodes
and a similar query for File
nodes, shown here.
{allMdx(sort: { fields: [frontmatter___date], order: DESC }filter: { frontmatter: { draft: { ne: true } } }) { ... }}
allFile(filter: { sourceInstanceName: { eq: "posts" } }) {edges {node {childMdx {...}}}}
The core problem with both of these queries is that we have
a conceptual content type that these represent (BlogPost
,
Note
, AuthorBio
, etc) and we're querying a format
(Mdx
) or a source (File
) and not a content type
(BlogPost
). Mdx
is how our files are written while
File
is where our files come from. Neither accurately
represents a conceptual content type accurately, ie: what
the content means to us.
So to start, we need to create a new type in the GraphQL
system called BlogPost
. To do this, we'll use the
sourceNodes
lifecycle method and the
schema customization
APIs. Specifically we're using buildObjectType
to create a
new type with a set of resolvers at the same time.
Most of our fields have no resolvers defined because they can be infered by Gatsby itself. There are roughly two types of fields here:
For the second type of field we need to pass through our
resolver to the source type's resolver to get the final
value (note: this resolver code will be simplified by
additional future Gatsby APIs). We take this approach for
excerpt
and body
.
// gatsby-node.jsexports.sourceNodes = ({ actions, schema }) => {const { createTypes } = actions;createTypes(schema.buildObjectType({name: `BlogPost`,fields: {id: { type: `ID!` },title: {type: "String!"},tags: { type: `[String]!` }excerpt: {type: "String!",resolve: async (source, args, context, info) => {const type = info.schema.getType(`Mdx`);const mdxNode = context.nodeModel.getNodeById({id: source.parent});const resolver = type.getFields()["excerpt"].resolve;const excerpt = await resolver(mdxNode,{ pruneLength: 140 },context,{fieldName: "excerpt"});return excerpt;}},body: {type: "String!",resolve(source, args, context, info) {const type = info.schema.getType(`Mdx`);const mdxNode = context.nodeModel.getNodeById({id: source.parent});const resolver = type.getFields()["body"].resolve;return resolver(mdxNode, {}, context, {fieldName: "body"});}},},interfaces: [`Node`]}));};
So now we have our BlogPost
type, but we still need some
nodes using that type. To do this, we'll use some
boilerplate createNode
calls that translates fairly
directly from MDX frontmatter to BlogPost
fields. We only
want to create BlogPost
nodes from Mdx
nodes with File
parents from the posts
filesystem source in this case. We
could, in the future, source BlogPost
s from different
places as well.
// gatsby-node.jsconst crypto = require("crypto");exports.onCreateNode = ({node,actions,getNode,createNodeId}) => {const {createNodeField,createNode,createParentChildLink} = actions;if (node.internal.type === `Mdx`) {const { frontmatter } = node;const parent = getNode(node.parent);if (parent.internal.type === "File" &&parent.sourceInstanceName === "posts") {const fieldData = {title: node.frontmatter.title,tags: node.frontmatter.tags || []};createNode({...fieldData,// Required fields.id: createNodeId(`${node.id} >>> BlogPost`),parent: node.id,children: [],internal: {type: `BlogPost`,contentDigest: crypto.createHash(`md5`).update(JSON.stringify(fieldData)).digest(`hex`),content: JSON.stringify(fieldData),description: `Blog Posts`}});createParentChildLink({parent: parent,child: node});}}};
Once we've done this, the following query will always work
regardless of if we have any BlogPost
s in the node system.
Note that even though we haven't coded any explicit support
for filtering or sorting, we can still use them because
Gatsby handles that for us.
Note also that because we created a parent/child
relationship for the BlogPost
nodes, we can reach up into
any of the parents to access interesting information, so no
user of our theme will be overly restricted if they want to
do something outside of the 80% use case.
{allBlogPost(sort: { fields: title, order: ASC }) {totalCountnodes {titletagsbodyparent {... on Mdx {parent {... on File {relativePath}}}}}}}
With no posts, the result is:
{"data": {"allBlogPost": {"totalCount": 0,"nodes": []}}}
With results, we get the following instead (note: I've removed the body value for brevity as it can get quite long).
{"data": {"allBlogPost": {"totalCount": 2,"nodes": [{"title": "Something Else","tags": [],"body": "","parent": {"parent": {"relativePath": "page-2.mdx"}}},{"title": "my post","tags": [],"body": "","parent": {"parent": {"relativePath": "index.mdx"}}}]}}}
Now we've gotten ourselves to a point where we can use a
BlogPost
type instead of Mdx
, simplifying our code base
and also ensuring that when users of our theme want to query
for blog posts on their pages, they don't have to worry
about filtering all of the other Mdx
or File
results
out.