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?
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.