February 16th, 2020
I 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!
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.
tags-query.js
template and TagsPage
componentFirst, 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 TagsPage
export 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 {
title
social {
name
url
}
}
}
`
export const tagsGroupQuery = graphql`
fragment TagsGroupQuery on BlogPostConnection {
group(field: tags) {
fieldValue,
totalCount
}
}
`
export const edgesQuery = graphql`
fragment EdgesQuery on BlogPostConnection {
edges {
node {
id
excerpt
slug
title
date(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} = pageContext
const {edges} = data.allBlogPost
const siteTitle = data.site.siteMetadata.title
const socialLinks = data.site.siteMetadata.social
const tagsGroup = data.tagsGroup
return (
<Layout location={location} title={siteTitle}>
<TagsBar {...{tagsGroup, activeTag: tag}}/>
<main>
{edges.map(({node}) => {
const title = node.title || node.slug
const keywords = node.keywords || []
return (
<Fragment key={node.slug}>
<SEO title="Tags" keywords={keywords}/>
<div>
<Styled.h2
css={css({
mb: 1,
})}
>
<Styled.a
as={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.a
as={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.a
as={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) =>
<TagCount
key={i}
tag={tag}
isLast={i < tagsGroup.group.length - 1}
isActive={tag.fieldValue === activeTag}
/>,
)
}
</Styled.p>
}
export default TagsBar
Now, we should create tags pages!
gatsby-node.js
to add the creation of tags pagesTo 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 components
const 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} = actions
const {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 pages
tags.forEach(tag => {
createPage({
path: `/tags/${_.kebabCase(tag.fieldValue)}/`,
component: TagsTemplate,
context: {
tag: tag.fieldValue,
},
})
})
// Make posts page
createPage({
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.
Now let's shadow src/gatsby-theme-blog/templates/posts-query.js
:
import {graphql} from "gatsby"
import PostsPage from "../components/posts"
export default PostsPage
export 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} = data
return (
<Layout location={location} title={siteTitle}>
<TagsBar {...{tagsGroup}}/>
<main>
{posts.map(({node}) => {
const title = node.title || node.slug
const keywords = node.keywords || []
return (
<Fragment key={node.slug}>
<SEO title="Home" keywords={keywords}/>
<div>
<Styled.h2
css={css({
mb: 1,
})}
>
<Styled.a
as={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 Posts
export default ({location, data}) => {
const {site, allBlogPost, tagsGroup} = data
return (
<Posts
location={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.
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.p
css={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.
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!