How to add tags to gatsby-theme-blog?
February 16, 2020 • #GatsbyJSI recently wrote an article about
how to refactor from starter to theme.
Now we want tags on our site but the article from the
Gatsby
documentation
doesn’t cover the case where you used a theme (here gatsby-theme-blog
)
and want to extend the theme by shadowing its components.
The end result we want looks like this:
We will add a bar containing tags and the number of articles for the
specific tag. We will also add tags link on every article on the
posts.js
and post.js
page.
The tags page will look like the home page but will only contain the articles for a specific tag.
We will also do some refactoring on the queries so that the code looks cleaner!
Adding tags to your articles
The markdown field tags
is already declared in gatsby-theme-blog
. It
means that you can start adding fields to the header of your articles,
like this one for example:
---...tags: ["GatsbyJS"]---
It should be an array of tags containing their display name as strings.
Creating tags-query.js
template and TagsPage
component
First, we will create a tags-query.js
template in
src/gatsby-theme-blog/templates/
. We will follow the same convention
as the gatsby-theme-blog
: queries go in the template
folder and
React components go in components
folder.
import {graphql} from "gatsby"import TagsPage from "../components/tags"export default TagsPageexport const query = graphql`query($tag: String) {site {...SiteQuery}allBlogPost(sort: { fields: [date, title], order: DESC },limit: 1000,filter: { tags: { eq: $tag }}) {totalCount...EdgesQuery}tagsGroup: allBlogPost(limit: 1000) {...TagsGroupQuery}}`
Note that I used some
GraphQL fragments for
some parts of the query. After finishing the development, I saw that a
lot of query code were duplicated so I created a
src/graphQLFragments.js
file that looks like this:
import {graphql} from "gatsby"export const siteQuery = graphql`fragment SiteQuery on Site {siteMetadata {titlesocial {nameurl}}}`export const tagsGroupQuery = graphql`fragment TagsGroupQuery on BlogPostConnection {group(field: tags) {fieldValue,totalCount}}`export const edgesQuery = graphql`fragment EdgesQuery on BlogPostConnection {edges {node {idexcerptslugtitledate(formatString: "MMMM DD, YYYY")tags}}}`
These fragments will be used multiple time so we wont repeat ourselves.
Note that the TagsGroupQuery
includes the totalCount
of the article
within a specific tag.
Also let’s create the React component associated with this query:
src/gatsby-theme-blog/components/tags.js
import React, {Fragment} from "react"import {Link} from "gatsby"import {css, Styled} from "theme-ui"import Layout from "gatsby-theme-blog/src/components/layout"import SEO from "gatsby-theme-blog/src/components/seo"import Footer from "../components/home-footer"import TagsBar from "./TagsBar"import {Tag} from "../components/Tag"const Tags = ({pageContext, data, location}) => {const {tag} = pageContextconst {edges} = data.allBlogPostconst siteTitle = data.site.siteMetadata.titleconst socialLinks = data.site.siteMetadata.socialconst tagsGroup = data.tagsGroupreturn (<Layout location={location} title={siteTitle}><TagsBar {...{tagsGroup, activeTag: tag}}/><main>{edges.map(({node}) => {const title = node.title || node.slugconst keywords = node.keywords || []return (<Fragment key={node.slug}><SEO title="Tags" keywords={keywords}/><div><Styled.h2css={css({mb: 1,})}><Styled.aas={Link}css={css({textDecoration: `none`,})}to={node.slug}>{title}</Styled.a></Styled.h2><small>{node.date}</small>{node.tags.length ? <>{` `} • {` `}</> : null}<small>{node.tags.map((tag, i) =><Tag tag={tag} isLast={i < node.tags.length - 1}/>)}</small><Styled.p>{node.excerpt}</Styled.p></div></Fragment>)})}</main><Footer socialLinks={socialLinks}/></Layout>)}export default Tags
We will also create Tag.jsx
:
import React from "react"import {Styled, css} from "theme-ui"import _ from "lodash"import {Link} from "gatsby"export const TagCount = ({tag, isLast, isActive}) =><><Styled.aas={Link}css={css({fontWeight: isActive ? "bold" : "normal"})}to={`/tags/${_.kebabCase(tag.fieldValue)}`}>#{tag.fieldValue} ({tag.totalCount})</Styled.a>{isLast && ", "}</>export const Tag = ({tag, isLast}) =><><Styled.aas={Link}to={`/tags/${_.kebabCase(tag)}`}>#{tag}</Styled.a>{isLast && ", "}</>
This file contains the reusable Tag
elements that will be used in our
pages.
The TagsBar
component (src/gatsby-theme-blog/components/TagsBar.tsx
)
will use the TagCount
and display the tag name and their count:
import React from "react"import {TagCount} from "./Tag"import {Styled} from "theme-ui"import _ from "lodash"const TagsBar = ({tagsGroup, activeTag}) => {return <Styled.p><span>These are the topics I like to write about: </span>{_.orderBy(tagsGroup.group, ["totalCount", "fieldValue"], ["desc", "desc"]).map((tag, i) =><TagCountkey={i}tag={tag}isLast={i < tagsGroup.group.length - 1}isActive={tag.fieldValue === activeTag}/>,)}</Styled.p>}export default TagsBar
Now, we should create tags pages!
Extending gatsby-node.js
to add the creation of tags pages
To create the tag pages we will have to add a gatsby-node.js
file at
the root of our project (if it doesn’t exist yet). Fortunately, we will
only have to add our own createPage
calls. The other pages are created
in gatsby-theme-blog-core
and we won’t have to handle this part.
const _ = require(`lodash`)const withDefaults = require(`gatsby-theme-blog-core/utils/default-options`)// These templates are simply data-fetching wrappers that import componentsconst PostsTemplate = require.resolve(`./src/gatsby-theme-blog/templates/posts-query`)const TagsTemplate = require.resolve(`./src/gatsby-theme-blog/templates/tags-query`)exports.createPages = async ({graphql, actions, reporter}, themeOptions) => {const {createPage} = actionsconst {basePath} = withDefaults(themeOptions)const result = await graphql(`{tagsGroup: allBlogPost(limit: 1000) {group(field: tags) {fieldValue}}}`)if (result.errors) {reporter.panic(result.errors)}const tags = result.data.tagsGroup.group// Make tag pagestags.forEach(tag => {createPage({path: `/tags/${_.kebabCase(tag.fieldValue)}/`,component: TagsTemplate,context: {tag: tag.fieldValue,},})})// Make posts pagecreatePage({path: basePath,component: PostsTemplate,context: {},})}
The posts
page is created here. But it should have been created by the
theme, right? It is. But we are going shadow the posts-query.js
template so that it also fetches the tags so we need to recreate this
page here.
Shadowing posts to get a list of tags on your posts list
Now let’s shadow src/gatsby-theme-blog/templates/posts-query.js
:
import {graphql} from "gatsby"import PostsPage from "../components/posts"export default PostsPageexport const query = graphql`query PostsQueryAndTagsGroup {site {...SiteQuery}allBlogPost(sort: { fields: [date, title], order: DESC }, limit: 1000) {...EdgesQuery}tagsGroup: allBlogPost(limit: 1000) {...TagsGroupQuery}}`
Again, we are using our fragments to not repeat ourselves.
Then the src/gatsby-theme-blog/components/posts.js
file should look
like this:
import React, {Fragment} from "react"import {Link} from "gatsby"import {css, Styled} from "theme-ui"import Layout from "gatsby-theme-blog/src/components/layout"import SEO from "gatsby-theme-blog/src/components/seo"import Footer from "../components/home-footer"import TagsBar from "./TagsBar"import {Tag} from "./Tag"const Posts = (data) => {const {location, posts, tagsGroup, siteTitle, socialLinks} = datareturn (<Layout location={location} title={siteTitle}><TagsBar {...{tagsGroup}}/><main>{posts.map(({node}) => {const title = node.title || node.slugconst keywords = node.keywords || []return (<Fragment key={node.slug}><SEO title="Home" keywords={keywords}/><div><Styled.h2css={css({mb: 1,})}><Styled.aas={Link}css={css({textDecoration: `none`,})}to={node.slug}>{title}</Styled.a></Styled.h2><small>{node.date}</small>{node.tags.length ? <>{` `} • {` `}</> : null}<small>{node.tags.map((tag, i) =><Tag tag={tag} isLast={i < node.tags.length - 1}/>)}</small><Styled.p>{node.excerpt}</Styled.p></div></Fragment>)})}</main><Footer socialLinks={socialLinks}/></Layout>)}// export default Postsexport default ({location, data}) => {const {site, allBlogPost, tagsGroup} = datareturn (<Postslocation={location}posts={allBlogPost.edges}tagsGroup={tagsGroup}siteTitle={site.siteMetadata.title}socialLinks={site.siteMetadata.social}/>)}
Note that some code can be put in common between the tags
and posts
pages.
Shadowing post.js to have tags in the post
Let’s create the src/gatsby-theme-blog/components/post.js
. I used the
one that is present in gatsby-theme-blog
and modified it:
import React from "react"import {Styled, css} from "theme-ui"import PostFooter from "gatsby-theme-blog/src/components/post-footer"import Layout from "gatsby-theme-blog/src/components/layout"import SEO from "gatsby-theme-blog/src/components/seo"import {MDXRenderer} from "gatsby-plugin-mdx"import {Tag} from "./Tag"const Post = ({data: {post,site: {siteMetadata: {title},},},location,previous,next,}) => (<Layout location={location} title={title}><SEO title={post.title} description={post.excerpt}/><main><Styled.h1>{post.title}</Styled.h1><Styled.pcss={css({fontSize: 1,mt: -3,mb: 3,})}>{post.date}{` `} • {` `}{post.tags.map((tag, i) =><Tag tag={tag} isLast={i < post.tags.length - 1}/>)}</Styled.p><MDXRenderer>{post.body}</MDXRenderer></main><PostFooter {...{previous, next}} /></Layout>)export default Post
This will display the tags on one article, near the date.
Conclusion
We now have a functional tags system in our Gatsby blog and we made it
by extending the gatsby-theme-blog
. It avoided us a lot of boilerplate
and it’s easier to keep our blog up to date by updating the base theme
instead of being in “ejected” mode with the starter.
The next step would probably be to create a blog theme with tags so that it’s easier for other developers to use it!