Gatsby: How to use optional images in mdx frontmatter?

Created on 7 Oct 2019  ·  8Comments  ·  Source: gatsbyjs/gatsby

Summary

I'd like to use optional images in frontmatter of markdown files processed with gatsby-plugin-mdx. I have multiple markdown files which correspond to article pages which use the same template and same page query. Some of those articles have a hero image specified in frontmatter and some don't.

How can I tell GraphQL that the heroImage frontmatter key can be an image but doesn't need to be?

Here is the current GraphQL behaviour when specifying a key in frontmatter if the image at ../assets/cms/hero-image.png exists:

  • heroImage: ../assets/cms/hero-image.png → works fine
  • heroImage: '' → Field "heroImage" must not have a selection since type "String" has no subfields
  • # heroImage → Unknown field 'heroImage' on type 'MdxFrontmatter'.

I'm a beginner to Gatsby and GraphQL but I think what I need is to make the heroImage field in GraphQL of type File instead of type File!.

Relevant information

Here is a scenario on how it should work

---
# my-markdown-file.md

heroImageOne: ../assets/cms/hero-image.png
heroImageTwo: ''
# heroImageThree
---
// my-template.js

import React from 'react'
import { graphql } from 'gatsby'

export const pageQuery = graphql`
  query MyMdxQuery($fileAbsolutePath: String!) {
    mdx(fileAbsolutePath: { eq: $fileAbsolutePath }) {
      frontmatter {
        heroImageOne {
          childImageSharp {
            fluid {
              ...GatsbyImageSharpFluid
            }
          }
        }
        heroImageTwo {
          childImageSharp {
            fluid {
              ...GatsbyImageSharpFluid
            }
          }
        }
        heroImageThree {
          childImageSharp {
            fluid {
              ...GatsbyImageSharpFluid
            }
          }
        }
      }
    }
  }
`

export default function MyTemplateComponent({ data }) {
  const {
    heroImageOne, // ← { src, srcSet, … }
    heroImageTwo, // ← null
    heroImageThree // ← null
  } = data.mdx.frontmatter

  return <JSX />
}

Note that I'm using the hero image as an example but I'm also generally interested in how to create optional frontmatter fields.

The markdown files will be generated by netlifycms and the content editor should be able to use non-required fields. That would also enable adding frontmatter fields to the template without having to add the necessary key to every markdown file.

Environment

  System:
    OS: macOS 10.14.6
    CPU: (8) x64 Intel(R) Core(TM) i7-8559U CPU @ 2.70GHz
    Shell: 5.3 - /bin/zsh
  Binaries:
    Node: 10.16.3 - /var/folders/hl/cb4r8j7j0l79tbq9tzxntggr0000gn/T/yarn--1570473024192-0.3318895832156219/node
    Yarn: 1.16.0 - /var/folders/hl/cb4r8j7j0l79tbq9tzxntggr0000gn/T/yarn--1570473024192-0.3318895832156219/yarn
    npm: 6.9.0 - /usr/local/bin/npm
  Languages:
    Python: 2.7.10 - /usr/bin/python
  Browsers:
    Chrome: 77.0.3865.90
    Firefox: 69.0.2
    Safari: 13.0.1

File contents

gatsby-config.js:

The project uses TypeScript but I use JavaScript examples in this issue to make it less complicated.

module.exports = {
  siteMetadata: {
    title: 'x',
    description: 'x',
    author: 'x',
  },
  plugins: [
    'gatsby-plugin-catch-links',
    // FIXME: Conflicts with gatsby-transformer-sharp in GitHub Actions and leads to image fragments for graphql not being copied into cache directory.
    // {
    //   resolve: 'gatsby-plugin-generate-typings',
    //   options: {
    //     dest: 'src/generated/graphql-types.d.ts',
    //   },
    // },
    {
      resolve: 'gatsby-plugin-manifest',
      options: {
        name: 'x',
        short_name: 'x',
        start_url: '/',
        background_color: '#fff',
        theme_color: '#fff',
        display: 'standalone',
        icon: 'src/assets/logo.svg', // This path is relative to the root of the site.
      },
    },
    {
      resolve: 'gatsby-plugin-mdx',
      options: {
        extensions: ['.md', '.mdx'],
        gatsbyRemarkPlugins: [
          { resolve: 'gatsby-remark-copy-linked-files' },
          // gatsby-remark-unwrap-images needs to be in front of gatsby-remark-images in order to work
          { resolve: 'gatsby-remark-unwrap-images' },
          {
            resolve: 'gatsby-remark-images',
            options: {
              maxWidth: 2880,
              showCaptions: ['title'],
              // Markdown captions do not work in mdx yet. More info: https://github.com/gatsbyjs/gatsby/pull/16574#issue-306869033
              markdownCaptions: true,
              linkImagesToOriginal: false,
              backgroundColor: 'transparent',
              quality: 75,
              tracedSVG: true,
            },
          },
        ],
        remarkPlugins: [require('remark-slug')],
        // Necessary subplugin to fix bug with placeholder image not disappearing after final image is loaded. More info: https://github.com/gatsbyjs/gatsby/issues/15486
        plugins: ['gatsby-remark-images'],
      },
    },
    // this (optional) plugin enables Progressive Web App + Offline functionality. To learn more, visit: https://gatsby.dev/offline
    // 'gatsby-plugin-offline',
    'gatsby-plugin-react-helmet',
    'gatsby-plugin-scss-typescript',
    'gatsby-plugin-sharp',
    'gatsby-plugin-typescript',
    'gatsby-plugin-typescript-checker',
    {
      resolve: 'gatsby-source-filesystem',
      options: {
        name: 'assets',
        path: `${__dirname}/src/assets`,
      },
    },
    {
      resolve: 'gatsby-source-filesystem',
      options: {
        name: 'pages',
        path: `${__dirname}/src/pages`,
        ignore: ['**/.*'],
      },
    },
    'gatsby-transformer-sharp',
  ],
}

package.json:

Used as yarn workspace.

{
  "name": "x",
  "version": "1.0.0",
  "description": "x",
  "author": "x",
  "private": true,
  "scripts": {
    "build": "gatsby build",
    "build:ci": "yarn lint:ci && yarn build",
    "develop": "gatsby develop",
    "format": "prettier --write \"**/*.{js,jsx,json,ts,tsx}\"",
    "lint": "eslint --ext .ts,.tsx,.js,.jsx .",
    "lint:ci": "yarn lint --max-warnings 0",
    "serve": "gatsby serve",
    "start": "yarn develop",
    "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing \"",
    "type-check": "tsc --noEmit",
    "type-check:watch": "yarn type-check --watch"
  },
  "dependencies": {
    "@mdx-js/mdx": "^1.4.4",
    "@mdx-js/react": "^1.4.4",
    "@types/body-scroll-lock": "^2.6.1",
    "@types/classnames": "^2.2.9",
    "@types/react": "^16.9.2",
    "@types/react-dom": "^16.9.0",
    "@types/react-helmet": "^5.0.9",
    "body-scroll-lock": "^2.6.4",
    "classnames": "^2.2.6",
    "gatsby": "^2.15.9",
    "gatsby-image": "^2.2.17",
    "gatsby-plugin-catch-links": "^2.1.8",
    "gatsby-plugin-generate-typings": "^0.9.8-r1",
    "gatsby-plugin-manifest": "^2.2.14",
    "gatsby-plugin-mdx": "^1.0.39",
    "gatsby-plugin-offline": "^2.2.10",
    "gatsby-plugin-react-helmet": "^3.1.6",
    "gatsby-plugin-scss-typescript": "^4.0.8",
    "gatsby-plugin-sharp": "^2.2.20",
    "gatsby-plugin-typescript": "^2.1.6",
    "gatsby-plugin-typescript-checker": "^1.1.1",
    "gatsby-remark-copy-linked-files": "^2.1.17",
    "gatsby-remark-images": "^3.1.21",
    "gatsby-remark-unwrap-images": "^1.0.1",
    "gatsby-source-filesystem": "^2.1.21",
    "gatsby-transformer-sharp": "^2.2.13",
    "node-sass": "^4.12.0",
    "normalize.css": "^8.0.1",
    "react": "^16.9.0",
    "react-dom": "^16.9.0",
    "react-helmet": "^5.2.1",
    "remark-slug": "^5.1.2",
    "tsconfig-paths-webpack-plugin": "^3.2.0"
  },
  "devDependencies": {
    "typescript": "^3.6.2"
  }
}

gatsby-node.js:

const path = require('path')
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin')

exports.onCreateWebpackConfig = ({ actions }) => {
  // Makes absolute paths defined in tsconfig available as aliases in webpack, enabling absolute imports.
  actions.setWebpackConfig({
    resolve: {
      plugins: [new TsconfigPathsPlugin()],
    },
  })
}

exports.onCreatePage = ({ page, actions }) => {
  if (path.extname(page.component) === '.md' && page.context.frontmatter) {
    const { createPage, deletePage } = actions
    const { template } = page.context.frontmatter

    deletePage(page)
    createPage({
      ...page,
      component: path.resolve(`./src/templates/${template}.tsx`),
      context: {
        ...page.context,
        fileAbsolutePath: page.component,
      },
    })
  }
}

gatsby-browser.js: N/A
gatsby-ssr.js: N/A


Thanks for helping me out! 😊 If I'm missing something obvious in the documentation I apologise and would be happy if you just point me into the right direction.

stale? question or discussion

Most helpful comment

I found a solution myself, I'm posting it here in case anyone else searches for an answer.

In order to make the scenario above working, following code is needed in gatgsby-node.js:

exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions

  createTypes(`
    type Mdx implements Node {
      frontmatter: MdxFrontmatter!
    }
    type MdxFrontmatter {
      heroImageOne: File @fileByRelativePath
      heroImageTwo: File @fileByRelativePath
      heroImageThree: File @fileByRelativePath
    }
  `)
}

We have to start at a type which implements a built-in type like Node because the createSchemaCustomization hook runs before third party plugins implement their types. Our type definitions here will be merged with the third party types.

All 8 comments

Hiya!

This issue has gone quiet. Spooky quiet. 👻

We get a lot of issues, so we currently close issues after 30 days of inactivity. It’s been at least 20 days since the last update here.

If we missed this issue or if you want to keep it open, please reply here. You can also add the label "not stale" to keep this issue open!

As a friendly reminder: the best way to see this issue, or any other, fixed is to open a Pull Request. Check out gatsby.dev/contribute for more information about opening PRs, triaging issues, and contributing!

Thanks for being a part of the Gatsby community! 💪💜

I'm having the same issue. I have a list GraphQL type defined with

      type FlexibleListEntry @infer {
        name: String!
        text: String
        href: String
      }

      type FlexibleList {
        items: [FlexibleListEntry!]
        title: String
      }

      type Frontmatter @infer {
        templateKey: String
        title: String
        href: String
        tags: [String!]
        seo: SEO
        people: FlexibleList
        services: FlexibleList
      }

Every entry in the list may contain an image. Unfortunately when some of the entries don't have an image field, I get the error Field "image" must not have a selection since type "String" has no subfields. The weird thing is that this is pretty non-deterministic/intermittent. I'm testing it right now, and it failed the first time and it is working after removing the .cache and public folders. If I try to add the image field to the FlexibleListEntry with

      type FlexibleListEntry @infer {
        name: String!
        text: String
        href: String
        image: File
      }

then the build fails with Cannot return null for non-nullable field File.id, which is strange because the query it is complaining about is not null, and so should have an id. This occurs even when every FlexibleListEntry has a valid, non-null path for the image entry.

I found a solution myself, I'm posting it here in case anyone else searches for an answer.

In order to make the scenario above working, following code is needed in gatgsby-node.js:

exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions

  createTypes(`
    type Mdx implements Node {
      frontmatter: MdxFrontmatter!
    }
    type MdxFrontmatter {
      heroImageOne: File @fileByRelativePath
      heroImageTwo: File @fileByRelativePath
      heroImageThree: File @fileByRelativePath
    }
  `)
}

We have to start at a type which implements a built-in type like Node because the createSchemaCustomization hook runs before third party plugins implement their types. Our type definitions here will be merged with the third party types.

@WhiteAbeLincoln Sorry, I missed your comment. Did you try appending the directive @fileByRelativePath?

type FlexibleListEntry @infer {
  name: String!
  text: String
  href: String
  image: File @fileByRelativePath
}

It tells GraphQL that this is a string containing a relative path to a file.

I found a solution myself, I'm posting it here in case anyone else searches for an answer.

In order to make the scenario above working, following code is needed in gatgsby-node.js:

exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions

  createTypes(`
    type Mdx implements Node {
      frontmatter: MdxFrontmatter!
    }
    type MdxFrontmatter {
      heroImageOne: File @fileByRelativePath
      heroImageTwo: File @fileByRelativePath
      heroImageThree: File @fileByRelativePath
    }
  `)
}

We have to start at a type which implements a built-in type like Node because the createSchemaCustomization hook runs before third party plugins implement their types. Our type definitions here will be merged with the third party types.

@dcastil just wanna say you saved my bacon! thanks!

would you or anyone here know how to find these type definitions that are defined by gatsby?this seems to be the only answer to something that Gatsby would have documentation to.

@panzerstadt-dev You can create a Gatsby project without any plugins and check out the GraphiQL explorer. There should only exist the “native” Gatsby types.

It would be nice if this documentation was updated to reflect that you need to customize the schema.

@Idmontie That’s a good idea! Maybe you can create a PR or an issue and someone might pick it up.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ferMartz picture ferMartz  ·  3Comments

kalinchernev picture kalinchernev  ·  3Comments

jimfilippou picture jimfilippou  ·  3Comments

KyleAMathews picture KyleAMathews  ·  3Comments

benstr picture benstr  ·  3Comments