Adding custom fields to Markdown Remark nodes

In my case I moving from Jekyl to Gatsby so has bazillion of YYYY-MM-DD-title.md files.

Moved everything to:

src/
└── notes/
    └── YYYY/
        └── YYYY-MM-DD-title/
            └── README.md

Which allows my to hold some related assets side by side with note itself.

Oh and finally I get rid of frontmatter stuff in markdown files.

Idea here is following:

  • folder name, without date will be and url path
  • first heading in markdown will be title

So the question now how to tell Gatsby how to build site from this

After few attempts ended up with following:

const assert = require('assert')
const { resolve } = require('path')

exports.createPages = async ({ graphql, actions }) => {
  const seen = new Set()
  const { data } = await graphql(`
    query {
      allMarkdownRemark {
        nodes {
          # id will be passed as a page context
          id
          fields {
            # here is our custom field
            # for: src/notes/2021/2021-11-14-hello/README.md
            # outputs: hello
            path
          }
        }
      }
    }
  `)

  data.allMarkdownRemark.nodes.forEach((node) => {
    const path = node?.fields?.path

    // just in case, we are not allowing same names
    if (seen.has(path)) {
      assert.fail(`"${path}" already exists`)
    }
    seen.add(path)

    actions.createPage({
      path,
      component: resolve('./src/templates/note.js'),
      context: {
        id: node?.id // pass page id to context
      }
    })
  })
}

// Example of adding custom fields to a node
exports.onCreateNode = ({ node, actions }) => {
  const { createNodeField } = actions

  // we are dealing only with markdown
  if (node.internal.type === 'MarkdownRemark') {
    // extract usefull pieces from file absolute path
    const match = node.fileAbsolutePath.match(/\/(\d{4})\/(\d{4}-\d{2}-\d{2})-([^/]+)\/README\.md/)
    if (match) {
      createNodeField({
        node,
        name: 'year',
        value: parseInt(match[1])
      })
      createNodeField({
        node,
        name: 'path',
        value: match[3]
      })
    }
  }
}

Now inside our template we can perform query like:

query GetNoteById($id: String!) {
  markdownRemark(id: { eq: $id }) {
    html
    fields {
      path
      year
    }
  }
}

Or for example on a home page to get last articles:

{
  recent: allMarkdownRemark(sort: { fields: fields___year, order: DESC }, limit: 5) {
    nodes {
      fields {
        path
        year
      }
    }
  }
}

As you can see, we can not only query fields but also use them for filtering and sorting