One of my biggest frustrations since migrating swizec.com from Wordpress to Gatsby was that I never added categories. Always wanted to, never did. Thought it would be super hard.
For ... years ... I diligently wrote categories in the frontmatter of all my markdown and the software dutifully ignored all that. 💩
For example, I added a bunch of categories to Saving time in UTC doesn't work, and offsets aren't enough:
And until a few days ago, those categories rendered as useless plaintext:
Between you and me, I doubt any human in the history of reading articles on the web has ever wanted to click on one of those. But the Google bot does. And Google loooooves internal links.
Use Gatsby to create category pages
You can use Gatsby's wonderful Creating Pages from Data Programmatically tutorial as a guide. Here's what I did.
Add a new function call to the createPages
hook in gatsby-node.js
. This lets you hook into the page creation part of Gatsby's build lifecycle.
// gatsby-node.js
exports.createPages = async ({ graphql, actions }) => {
createCategoryPages({ graphql, actions })
}
I like to name my functions so it's easier to keep track.
The createCategoryPages
function works in 3 steps:
- Use GraphQL to collect all category lists
- Create a
Set()
of unique categories - Iterate and make pages
Use GraphQL to collect all category lists
Gatsby revolves around the idea of using GraphQL to access everything. And they mean everything. Even markdown files in your project.
// gatsby-node.js
async function createCategoryPages({ graphql, actions }) {
const result = await graphql(`
query Categories {
allMdx(filter: { fileAbsolutePath: { regex: "/blog|articles/.+/" } }) {
nodes {
frontmatter {
categories
}
}
}
}
`)
if (result.errors) {
console.error(result.errors) // eslint-disable-line no-console
throw result.errors
}
That GraphQL query looks for all MDX files under the /blog/
or /articles/
paths and extracts the categories
field from their frontmatter. If there's an error, we throw.
2. Create a Set()
of unique categories
The query gives us an array of strings like "Time, UTC, Daylight Saving, Lessons, Technical"
. We need to split that into individual categories and make sure we clear duplicates.
// gatsby-node.js
// extract categories out of graphql and make a set
const allCategories = new Set(
result.data.allMdx.nodes
.filter((node) => !!node.frontmatter.categories)
.map((node) => node.frontmatter.categories)
.map((categories) => categories.split(","))
.flat()
.map((category) => category.trim().toLowerCase())
)
allCategories.add("uncategorized")
First, a filter clears empty strings. Second, a map extracts the categories
property. Third, a map splits by comma. Fourth, a flat flattens the array of arrays. Fifth, a map clears leading and trailing whitespace and lowercases the categories.
We're now ready to make pages.
3. Iterate and make pages
The last step in createCategoryPages
, uses Gatsby's createPage action to create pages based on a React component.
for (const category of allCategories) {
await actions.createPage({
path: `/categories/${category}`,
component: require.resolve("./src/templates/category.js"),
context: { category, categoryRegex: `/${category}/gi` },
})
}
We iterate through our list of categories and call actions.createPage
for each.
Each page is going to live on a /categories/*
path using the category name. It's going to render with the category.js
React component. And will receive the category
and a categoryRegex
as its props.
A React template for each category
To render each category page, we need a page component and a query that fetches all the articles.
The component
The component is nothing special. A bunch of copy, email signup forms, and an <ArticleListing>
thingy that iterates through a list of articles and renders titles with descriptions.
// /src/templates/category.js
const CategoryPage = ({ data, pageContext: { category } }) => {
const title = `Swizec's ${category} articles`
const description = `Learn from Swizec's raw and honest from the heart articles filed under "${category}".`
return (
<>
<Head title={title} description={description} />
<Container>
// ...
<FormCK copyBefore={<></>} />
{data.allSitePage.nodes.map((props, i) => (
<ArticleListing {...props} key={i} />
))}
<FormCK copyBefore={<></>} />
</Container>
</>
)
}
We're using Gatsby's support for page queries to get article data into the data
prop at build time. On every page deploy.
The page query
The page query leans heavily on GraphQL's flexibility to get a list of articles for a specific category from the file system.
export const pageQuery = graphql`
query ArticlesInCategory($categoryRegex: String) {
allSitePage(
filter: {
path: { regex: "/blog|articles/.+/" }
context: { frontmatter: { categories: { regex: $categoryRegex } } }
}
sort: { fields: context___frontmatter___published, order: DESC }
) {
nodes {
path
context {
frontmatter {
title
description
published
}
}
}
}
}
`
$categoryRegex
is a query variable that gets populated from the page context we defined when creating pages.
We use that to filter
all pages on the site by whether they include the category in their frontmatter. We also filter by filepath to ensure only blogs or articles are included.
The query sorts by descending publish time and extracts properties that <ArticleListing>
will use to render.
Link categories on each article
Adding links to categories is the easy part. You split the category string by comma and render links in a loop.
const Categories = ({ categories }) => {
const cleaned = categories.split(",").map((category) => category.trim())
return (
<>
{cleaned.map((category) => (
<>
<Link to={`/categories/${category.toLowerCase()}`}>{category}</Link>,{" "}
</>
))}
</>
)
}
I put that in my existing ArticleMetaData
component.
Voila
A Sunday afternoon of hacking around and swizec.com has 1440 category pages like this.
Google bot is happy, readers probably don't care, and every page has a link from somewhere. Perfection.
Cheers,
~Swizec
PS: I think this is a great example of the elegance at the core of Gatsby. Yes it feels more complicated than its competitors, but there's something beautiful about how this came together
Continue reading about Adding categories to a Gatsby blog (for better SEO)
Semantically similar articles hand-picked by GPT-4
- Lessons from migrating a 14 year old blog with 1500 posts to Gatsby
- Using YouTube as a data source in Gatsbyjs
- Moving 13 years of Wordpress blog to Gatsby Markdown
- Livecoding Recap 48- Why contributing to big opensource projects is still hard
- How I Added a Related Articles Feature on Swizec.com Using GPT-4 Embeddings
Learned something new?
Read more Software Engineering Lessons from Production
I write articles with real insight into the career and skills of a modern software engineer. "Raw and honest from the heart!" as one reader described them. Fueled by lessons learned over 20 years of building production code for side-projects, small businesses, and hyper growth startups. Both successful and not.
Subscribe below 👇
Software Engineering Lessons from Production
Join Swizec's Newsletter and get insightful emails 💌 on mindsets, tactics, and technical skills for your career. Real lessons from building production software. No bullshit.
"Man, love your simple writing! Yours is the only newsletter I open and only blog that I give a fuck to read & scroll till the end. And wow always take away lessons with me. Inspiring! And very relatable. 👌"
Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.
Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.
Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.
Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev
Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization
Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections
Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog
Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com
By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️