Search code examples
node.jsgraphqlgatsbynetlifynetlify-cms

How to add 'slug' tag automatically in markdown files using NetlifyCMS and Gatsby?


Codesandbox link here.

Any time I try to publish a new blog post using NetlifyCMS, it says it publishes. However my Netlify build fails and doesn't actually push any blog posts live.

Here's the error I get:

12:44:22 PM: error Your site's "gatsby-node.js" must set the page path when creating a page.
12:44:22 PM: The page object passed to createPage:
12:44:22 PM: {
12:44:22 PM:     "path": null,
12:44:22 PM:     "component": "/opt/build/repo/src/templates/blogTemplate.js",
12:44:22 PM:     "context": {
12:44:22 PM:         "slug": null
12:44:22 PM:     }
12:44:22 PM: }
12:44:22 PM: See the documentation for the "createPage" action — https://www.gatsbyjs.org/docs/actions/#createPage
12:44:22 PM: not finished createPages - 0.042s

The reason why I get this error is because when new posts are published, the markdown file of the new blog post does not automatically get the 'slug' tag added. Example:

---
title: 10 of the best SEO strategies for 2021
slug: /posts/10-best-seo-strategies-2021/ <-- I had to manually add this in the markdown file. This line is completely missing when pushing new blog posts live. This is causing the site build to fail.
date: 2021-03-26T23:53:24.128Z
excerpt: >-
  In this post, we go over 10 of the best SEO strategies for 2021. If you want
  more business, read more now!
---

Once I manually go and add the blog post as a markdown file outside of NetlifyCMS, and add the slug tag and push up to master, it successfully builds. Obviously I don't want to do that every time, I want my site to publish normally from NetlifyCMS.

gatsby-node.js:

exports.createPages = async ({ actions, graphql, reporter }) => {
  const { createPage } = actions
  const blogPostTemplate = require.resolve(`./src/templates/blogTemplate.js`)
  const result = await graphql(`
    {
      allMarkdownRemark(
        sort: { order: DESC, fields: [frontmatter___date] }
        limit: 1000
      ) {
        edges {
          node {
            frontmatter {
              slug
            }
          }
        }
      }
    }
  `)
  // Handle errors
  if (result.errors) {
    reporter.panicOnBuild(`Error while running GraphQL query.`)
    return
  }
  result.data.allMarkdownRemark.edges.forEach(({ node }) => {
    createPage({
      path: node.frontmatter.slug,
      component: blogPostTemplate,
      context: {
        // additional data can be passed via context
        slug: node.frontmatter.slug,
      },
    })
  })
}

GraphQL pageQuery in my /src/pages/posts.js file:

export const pageQuery = graphql`
  query {
    allMarkdownRemark(sort: { order: DESC, fields: [frontmatter___date] }) {
      edges {
        node {
          id
          excerpt(pruneLength: 250)
          frontmatter {
            date(formatString: "MMMM DD, YYYY")
            slug
            title
          }
        }
      }
    }
  }
`

Config.yml:

backend:
  name: github
  repo: my-repo

media_folder: uploads
public_folder: /uploads

collections:
  - name: "posts"
    label: "Posts"
    folder: "posts"
    create: true
    slug: "{{slug}}"
    fields:
      - { label: "Title", name: "title", widget: "string" }
      - { label: "Publish Date", name: "date", widget: "date" }
      - { label: "Excerpt", name: "excerpt", widget: "string" }
      - { label: "Body", name: "body", widget: "markdown" }

blogTemplate.js file:

export const pageQuery = graphql`
  query($slug: String!) {
    markdownRemark(frontmatter: { slug: { eq: $slug } }) {
      html
      frontmatter {
        date(formatString: "MMMM DD, YYYY")
        slug
        title
        excerpt
      }
    }
  }
`

Any idea why this may be happening?


Solution

  • Any idea why this may be happening?

    Well, you are trying to query for a slug field and it's never been set (at least at the beginning). Your frontmatter has these fields:

    • Title
    • Publish
    • Excerpt
    • Body

    But not a slug.

    The standard way is to add it in your config.yml:

    - { name: slug, label: Slug, required: true, widget: string }
    

    Adding this, your query will work automatically.

    Another method is to use the built-in listeners and the resolvers (Node APIs) from Gatsby to generate a slug based on a parameter previously set, but you will need to change your query. On your gatsby-node.js add:

    exports.onCreateNode = ({ node, actions, getNode }) => {
      const { createNodeField } = actions;
    
      if (node.internal.type === `MarkdownRemark`) {
        let value = createFilePath({ node, getNode });
    
        createNodeField({
          name: `slug`,
          node,
          value,
        });
      }
    };
    

    With onCreateNode you are creating a new node based on some rules (more details). That will create a new collection to be queried named fields with a slug inside. So you only need to adapt it like:

    exports.createPages = async ({ actions, graphql, reporter }) => {
      const { createPage } = actions
      const blogPostTemplate = require.resolve(`./src/templates/blogTemplate.js`)
      const result = await graphql(`
        {
          allMarkdownRemark(
            sort: { order: DESC, fields: [frontmatter___date] }
            limit: 1000
          ) {
            edges {
              node {
                fields{
                  slug
                }
                frontmatter {
                  slug // not needed now
                }
              }
            }
          }
        }
      `)
      // Handle errors
      if (result.errors) {
        reporter.panicOnBuild(`Error while running GraphQL query.`)
        return
      }
      result.data.allMarkdownRemark.edges.forEach(({ node }) => {
        createPage({
          path: node.fields.slug,
          component: blogPostTemplate,
          context: {
            // additional data can be passed via context
            slug: node.frontmatter.slug,
          },
        })
      })
    }
    

    There's no "automated" way to achieve this without diving into more Node schemas. You are only creating a markdown file and querying its content. Which are the logic to create a slug from scratch? slug fields should be always required.

    You can try changing the following:

    createNodeField({
      name: `slug`,
      node,
      value,
    });
    

    To add a custom value based on some logic if the slug is not defined.


    Another thing outside the topic. You are creating duplicated an excerpt:

    • One in your markdown (coming from Netlify's CMS):

      - { label: "Excerpt", name: "excerpt", widget: "string" }
      
    • One created automatically in your GraphQL query. GraphQL + Gatsby filesystem adds a custom excerpt field that results from a splitting of the content of the body by using a pruneLength filtering outside the frontmatter:

        export const pageQuery = graphql`
          query {
            allMarkdownRemark(sort: { order: DESC, fields: [frontmatter___date] }) {
              edges {
                node {
                  id
                  excerpt(pruneLength: 250)
                  frontmatter {
                    date(formatString: "MMMM DD, YYYY")
                    slug
                    title
                  }
                }
              }
            }
          }
        `
      

    I think that you are mixing stuff here, I would recommend using only one of them to avoid misunderstandings in your code.