Search code examples
reactjstagsgatsbyblogscontentful

Adding tags to Gatsby Contentful blog


I'm trying to add tags on blog posts, and I struggle to find a resource, which explain how to implement them.

The end goal is to get clickable tags, which leads to a page, where all posts with same tag appears in a list.

I'm using GatsbyJS with Contentful integration.

I have a file called article-post.tsx with the following code:

import React from "react"
import { graphql } from "gatsby"
import { documentToReactComponents } from "@contentful/rich-text-react-renderer"

import Layout from "../components/layout/layout"
import Img from "gatsby-image"
import SEO from "../components/layout/seo"
import styled from "styled-components"
import { BodyMain, H1 } from "../components/styles/TextStyles"

export const query = graphql`
  query($slug: String!) {
    contentfulArticlePost(slug: { eq: $slug }) {
      title
      tags
      publishedDate(formatString: "Do MMMM, YYYY")
      featuredImage {
        fluid(maxWidth: 720) {
          ...GatsbyContentfulFluid
        }
      }
      body {
        json
      }
    }
  }
`

const ArticlePost = props => {
  const options = {
    renderNode: {
      "embedded-asset-block": node => {
        const alt = node.data.target.fields.title["en-US"]
        const url = node.data.target.fields.file["en-US"].url
        return <img alt={alt} src={url} className="embeddedImage" />
      },
    },
  }

  return (
    <Layout>
      <SEO title={props.data.contentfulArticlePost.title} />
      <Wrapper>
        <ImageWrapper>
          {props.data.contentfulArticlePost.featuredImage && (
            <Img
              className="featured"
              fluid={props.data.contentfulArticlePost.featuredImage.fluid}
              alt={props.data.contentfulArticlePost.title}
            />
          )}
        </ImageWrapper>
        <Title>{props.data.contentfulArticlePost.title}</Title>
        <Tags>
          {props.data.contentfulArticlePost.tags.map(tag => (
            <span className="tag" key={tag}>
              {tag}
            </span>
          ))}
        </Tags>
        <ContentWrapper>
          {documentToReactComponents(
            props.data.contentfulArticlePost.body.json,
            options
          )}
        </ContentWrapper>
      </Wrapper>
    </Layout>
  )
}

export default ArticlePost

const Wrapper = styled.div`
  display: grid;
  grid-gap: 1.875rem;
  margin: 0 auto;
  padding: 7rem 1.875rem;
  max-width: 900px;
`

const ImageWrapper = styled.div`
  .featured {
    border-radius: 15px;
  }
`

const Title = styled(H1)`
  margin: 0 auto;
  text-align: center;
`

const Tags = styled.div`
margin: 0 auto;
.tag {
  background: #8636E4;
  border-radius: 1rem;
  padding: 0.5rem;
  margin: 0.2rem;
  font-weight: 600;
}
`

const ContentWrapper = styled(BodyMain)`
  display: grid;
  grid-gap: 20px;
  max-width: 900px;
  margin: 0 auto;
  line-height: 1.6;

  .embeddedImage {
    padding: 50px 0px;
    width: 100%;
    height: auto;
  }
`

It does give me the tags and I'm able to style them. Though I don't know how to get them clickable like links/buttons.

I have a file called gatsby-node.js, which contains the following code:

const path = require("path")

exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions
  const response = await graphql(`
    query {
      allContentfulArticlePost {
        edges {
          node {
            id
            slug
          }
        }
      }
    }
  `)
  response.data.allContentfulArticlePost.edges.forEach(edge => {
    createPage({
      path: `/articles/${edge.node.slug}`,
      component: path.resolve("./src/templates/article-post.tsx"),
      context: {
        slug: edge.node.slug,
        id: edge.node.id
      },
    })
  })
}

Where do I go from here?


Solution

  • First of all, you need to create dynamic pages for each tag to create a valid link element. In your gatsby-node.js create a query to fetch all tags and create pages for each tag like:

    const path = require("path")
    
    exports.createPages = async ({ graphql, actions }) => {
      const { createPage } = actions
      const response = await graphql(`
        query {
          allContentfulArticlePost {
            edges {
              node {
                id
                slug
              }
            }
          }
        }
      `)
      response.data.allContentfulArticlePost.edges.forEach(edge => {
        createPage({
          path: `/articles/${edge.node.slug}`,
          component: path.resolve("./src/templates/article-post.tsx"),
          context: {
            slug: edge.node.slug,
            id: edge.node.id
          },
        })
      })
    
    
      const tags= await graphql(`
        query {
          allContentfulArticlePost {
            edges {
              node {
                tags
              }
            }
          }
        }
      `)
    
      tags.data.allContentfulArticlePost.edges.forEach(edge=> {
       let slugifiedTag= edges.node.tag.toLowerCase().replace("/^\s+$/g", "-");
    
        createPage({
          path: `/tag/${slugifiedTag}`,
          component: path.resolve("./src/templates/tag-post.tsx"), // your tagComponent
          context: {
            slug: edge.node.slug,
            tagName: edges.node.tag
          },
        })
      })    
    }
    

    Step by step, first of all, you need to retrieve all your tags from each blog in tags query.

    Then, for each tag, you need to create a valid slug based on the name (i.e: This Is a Sample Tag will be converted to this-is-a-sample-tag, slugifiedTag in the sample). This is done in edges.node.tag.toLowerCase().replace("/^\s+$/g", "-"), the regular expression will match all-white spaces globally and will replace them by hyphens replace("/^\s+$/g", "-"). You may need to parse the tags edge to remove duplicates in order to avoid duplicated entries creation, creating a Set should work for you.

    At this point, you'll have created all pages under /tag/${slugifiedTag} (i.e: /tag/this-is-a-sample-tag). So, you will need to change your article-post.tsx to point to the tag page:

    <Tags>
      {props.data.contentfulArticlePost.tags.map(tag => {
       let slugifiedTag= edges.node.tag.toLowerCase().replace("/^\s+$/g", "-");
    
        return <Link className="tag" key={tag} to={slugifiedTag}>
          {tag}
        </Link>
      })}
    </Tags>
    

    Note that you are repeating slugifiedTag function. You can avoid this by creating a tag entity in your CMS and adding a name and slug value. If you retrieve the slug in your gatsby-node.js query as well as in your template query, you can directly point to <Link className="tag" key={tag} to={tag.slug}>. Following this example, the name will be This is a Sample Tag while the slug will be direct this-is-a-sample-tag.

    What you last is to create a query in your tag-post.tsx that fetch all posts for each tag since you are passing via context the slug and the tagName. Your query should look like:

    export const query = graphql`
      query($slug: String!, $tags: [String!]) {
        contentfulArticlePost(slug: { eq: $slug }, tags: { in: $tags}) {
          title
          tags
          publishedDate(formatString: "Do MMMM, YYYY")
          featuredImage {
            fluid(maxWidth: 720) {
              ...GatsbyContentfulFluid
            }
          }
          body {
            json
          }
        }
      }
    `
    

    Since $tags is an array, should be declared as [String!]. If you want to make the field non-nullable just add the exclamation mark (!) like [String!]!. Then you only need to filter the by tags that contain at least one tag: tags: { in: $tags}).

    As I said, this should be improved and simplified by adding a tag entity in your CMS, with name and slug fields.

    It's a broad question without knowing your data structure and your internal components but you get the main idea of the approach.