70 %
Chris Biscardi

Authoring Queries in Themes

Building composable themes requires a slightly different mindset than building a conventional site. Consider the following cases with one Gatsby theme and one site.

Our site is built as usual, with whatever tech we prefer, etc. We also specify a theme as such:

JS
__experimentalThemes: ["gatsby-theme-mystuff"];

A typical query on a site can look like this. Note that there's nothing inherently wrong with this query, it sorts by the publish date, filters out draft posts. When using the same query in a site however, there's one main aspect of this query that becomes problematic quickly: allMdx.

GraphQL
{
allMdx(
sort: { fields: [frontmatter___date], order: DESC }
filter: { frontmatter: { draft: { ne: true } } }
) { ... }
}

To understand why allMdx is problematic in a themes setting, consider the scenario where you have blog posts in content/posts and pages that you've written using MDX in src/pages. The blog posts need to be pulled into the Gatsby node system using gatsby-source-filesystem and to add a table of contents to our src/pages pages, we put another gatsby-source-filesystem on src/pages. So we end up with a config like this in our gatsby-config.js.

JS
module.exports = {
plugins: [
{
resolve: `gatsby-source-filesystem`,
options: {
path: `content/posts`,
name: `posts`
}
},
{
resolve: `gatsby-source-filesystem`,
options: {
path: `src/pages`
}
}
]
};

So the query we've written collects all of the Mdx nodes across both filesystem sources, which is not likely to be what we want. The problem gets worse when you introduce plugins like gatsby-transformer-react-docgen, which generate Mdx nodes into the Gatsby system. This causes even more content to match our query.

To fix this, we can take a variety of different paths. One is to change the query to an allFile query. Functionally, this is a similar query as our original allMdx query. The main differences are that we end up fetching drafts as well and we have to programmatically sort after we fetch as well.

JS
allFile(filter: { sourceInstanceName: { eq: "posts" } }) {
edges {
node {
childMdx {
...
}
}
}
}

One unfortunate consequence of this is that it locks us into using files as our data source. If someone is using a headless CMS then they can't use our theme with this query because we are now querying the source of our content instead of the format.

createNodeField

We can solve this problem by adding a new field to our content: isNote. This is the ultimate in flexibility because it allows us to define whatever we want as the criteria for what constitutes a Note.

JS
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions;
if (node.internal.type === `Mdx`) {
const parent = getNode(node.parent);
if (
parent.internal.type === "File" &&
parent.internalSourceName === "posts"
) {
createNodeField({
name: `isNote`,
node,
value: true
});
}
}
};

Then if we have a title field in our Mdx files, we can filter the result set:

GraphQL
{
allMdx(filter: { fields: { isNote: { eq: true } } }) {
nodes {
frontmatter {
title
}
}
}
}

Fin

This is very flexible in that it allows us to access every field as we're used to on Mdx or any other type. It is restricting in that we can't combine multiple types to provide notes. To do that, we'll have to look in to Constructing Query Types in Themes.