Sometimes you have that one-off field you need to use. So
you throw it in the frontmatter of your Markdown file and...
then what? The official Gatsby blog theme defines a
BlogPost
interface that doesn't include your frontmatter
field! This is intentional because BlogPost
is meant to be
a type that other themes and plugins can depend on, allowing
extension. It's an interface so when you try to
createNodeField
it doesn't show up in fields
either
because fields
is on MdxBlogPost
(or some other proxy
type) and not the interface itself.
So what to do?
Inline fragments are the answer.
With inline fragments you can reach deep into the parent
types, whether you chose to createNodeField
or not, to
access your one-off frontmatter field.
Without changing any code in gatsby-node
or other
gatsby-*
file, we can use inline fragments to do a
type-switch on the possible return types. Using these type
switches we can reach up into the parent Mdx
node and grab
our frontmatter. This is nice because it makes what we're
doing explicit (that is, extending the data model with new
data that other themes won't depend on) and compatible with
other source types. This part of the query only affects the
types which it declares and the fields will be empty for
other types.
{blogPost {id... on MdxBlogPost {parent {... on Mdx {frontmatter {title}}}}}}
Let's say the frontmatter field we want is a status
field
that indicates published status. The frontmatter could look
like this.
---title: Hello World (example)date: 2019-04-15status: published---
To clean this up a bit we can put the field we want on our
MdxBlogPost
type if we want, since that is a concrete
node.
exports.onCreateNode = ({ node, actions, getNode }) => {const { createNodeField } = actions;if (node.internal.type === `MdxBlogPost`) {const parent = getNode(node.parent);createNodeField({node,name: "status",value: parent.frontmatter.status});}};
This simplifies our query while also potentially allowing us
to source this information from a different node (for
example, if we were manually creating MDX content and wanted
it to be represented as a BlogPost
anyway).
{blogPost {id... on MdxBlogPost {fields {status}}}}
What happens if we query for a field that doesn't exist in our frontmatter or fields?
{blogPost {id... on MdxBlogPost {fields {statusdate}}}}
We get an error.
{"errors": [{"message": "Cannot query field \"date\" on type \"MdxBlogPostFields\".","locations": [{"line": 7,"column": 9}],"stack": ["GraphQLError: Cannot query field \"date\" on type \"MdxBlogPostFields\"."," at Object.Field (/Users/chris/tmp/inline-fragments-interfaces/node_modules/graphql/validation/rules/FieldsOnCorrectType.js:53:31)"," at Object.enter (/Users/chris/tmp/inline-fragments-interfaces/node_modules/graphql/language/visitor.js:324:29)"," at Object.enter (/Users/chris/tmp/inline-fragments-interfaces/node_modules/graphql/language/visitor.js:375:25)"," at visit (/Users/chris/tmp/inline-fragments-interfaces/node_modules/graphql/language/visitor.js:242:26)"," at validate (/Users/chris/tmp/inline-fragments-interfaces/node_modules/graphql/validation/validate.js:73:24)"," at getGraphQLParams.then.then.optionsData (/Users/chris/tmp/inline-fragments-interfaces/node_modules/express-graphql/index.js:121:32)"," at processTicksAndRejections (internal/process/task_queues.js:86:5)"]}]}
We can fix this using the Schema Customization APIs.
Specifically createTypes
.
exports.createSchemaCustomization = ({ actions }) => {const { createTypes } = actions;const typeDefs = `type MdxBlogPost implements Node @infer {fields: MdxBlogPostFields}type MdxBlogPostFields @infer {status: Stringdate: Date @dateformat}`;createTypes(typeDefs);};
This doesn't guarentee we'll get a result. It does guarantee our query won't break the site if someone forgets a date field though!
{"data": {"blogPost": {"id": "664d82ef-96ab-53e4-a2a5-ecf967793df0","fields": {"status": "published","date": null}}}}