javascriptreactjsgatsbynetlifynetlify-cms

How to create a single Gatsby Page to show and filter all blog posts by tag/category


Hello i'm building a blog using Gatsby and Netlify CMS. I started off from the gatsby-starter-netlify-cms template.

I have the /blog page where i currently display all posts and also a list of all the tags. When user clicks on a tag, it is currently redirected to the tags/name-of-tag page, where a list of all posts tagged like that is displayed.

What i want instead is to directly filter the list on the /blog page. This way i'll have only one page to display and filter blog posts by tag (and possibly by term search).

So the tag links should redirect to /blog?tag-name or something like that. I'm not sure how to tell Gatsby to create a single page with possibility to inject a filter value in order to pass it to the page query..

This is how /tags/ pages are created currently:

// Tag pages:
    let tags = []
    // Iterate through each post, putting all found tags into `tags`
    posts.forEach((edge) => {
      if (_.get(edge, `node.frontmatter.tags`)) {
        tags = tags.concat(edge.node.frontmatter.tags)
      }
    })
    // Eliminate duplicate tags
    tags = _.uniq(tags)

    // Make tag pages
    tags.forEach((tag) => {
      const tagPath = `/tags/${_.kebabCase(tag)}/`

      createPage({
        path: tagPath,
        component: path.resolve(`src/templates/tags.js`),
        context: {
          tag,
        },
      })
    })

This is my blog page query (the one i want to be able to filter by tag):

export const blogPageQuery = graphql`
  query BlogPageTemplate {
    markdownRemark(frontmatter: { templateKey: { eq: "blog-page" } }) {
      frontmatter {
        image {
          childImageSharp {
            fluid(maxWidth: 2048, quality: 80) {
              ...GatsbyImageSharpFluid
            }
          }
        }
        heading
        subheading
      }
    }
    allMarkdownRemark(
      sort: { order: DESC, fields: [frontmatter___date] }
      filter: { frontmatter: { templateKey: { eq: "blog-post" } } }
    ) {
      edges {
        node {
          id
          fields {
            slug
          }
          excerpt(pruneLength: 180)
          frontmatter {
            date(formatString: "MMMM DD, YYYY")
            title
            description
            featuredpost
            featuredimage {
              childImageSharp {
                fluid(quality: 50, maxWidth: 512) {
                  ...GatsbyImageSharpFluid
                }
              }
            }
          }
        }
      }
    }
  }
`

Solution

  • I'm not sure how to tell Gatsby to create a single page with possibility to inject a filter value in order to pass it to the page query.

    You can't. The only way to filter data in a page query is by passing data using the context. Like you are doing in with the tags page:

      createPage({
        path: tagPath,
        component: path.resolve(`src/templates/tags.js`),
        context: {
          tag,
        },
      })
    

    The easiest and native approach is to use the /tag/tag-name page, which is intended to filter by tag name, otherwise, you will need to get the URL parameter and using it in a JavaScript filter function to filter all the data from the page query. Since you are missing the rendering part of your blog page... Something like this approach should work:

    const BlogTemplate=({ data }){
        if(typeof window !== "undefined"){
          const queryString = window.location.search;
          const urlParams = new URLSearchParams(queryString);
          const urlTag= urlParams.get('tag');
          const filteredPosts= data.allMarkdownRemark.edges.node.filter(node=> node.frontmatter.tag.includes(urlTag))
          const loopData= urlParams.has('tag') ? filteredPosts : data
       }
       
    return loopData.allMarkdownRemark.edges.node.map(node=> <h1 key={node.title}>{node.title}</h1>)
    
    }
    

    Note: of course, adapt it to your needs but get the idea.

    Note also that you will need to wrap all your window logic in the typeof window !== "undefined" condition, since in the SSR window (and other global objects) are not available.

    The key part is to use wether use data or your filteredPosts depending on the existance of the URL parameter, so if it exists, you will need to filter the data otherwise, you need to use the "default" data (unfiltered).

    It's difficult to guess how the code will behave but the idea relies on changing your iterable data depending on the URL, use some hooks if needed.

    According to your query, it seems that your blogs don't contain any tag field in the blog template query so you will need to add it to allow the filter loop to work.

    Once the code is working, you will need to add the proper controls to avoid the code-breaking when some field is missing, but the idea and the path to follow is this.