Search code examples
reactjsgatsby

Crate Tag & Category taxonomy in Gatsby


I have a development on Gatsby with allMdx. I created a "Category" taxonomy and to create a category page I used a file gatsby-node.js . There's such a code inside.

const _ = require("lodash")
const { transliterate } = require('./src/functions/transletter');

function dedupeCategories(allMdx) {
    const uniqueCategories = new Set()
    // Iterate over all articles
    allMdx.edges.forEach(({ node }) => {
      // Iterate over each category in an article
      node.frontmatter.categories.forEach(category => {
        uniqueCategories.add(category)
      })
    })
    // Create new array with duplicates removed
    return Array.from(uniqueCategories)
  }

  exports.createPages = async ({ graphql, actions, reporter }) => {
    const { createPage } = actions

    // Query markdown files including data from frontmatter
    const { data: { allMdx } } = await graphql(`
      query {
        allMdx {
          edges {
            node {
              id
              frontmatter {
                categories
                tags
                slug
              }
            }
          }
        }
      }
    `)


    // Create array of every category without duplicates
    const dedupedCategories = dedupeCategories(allMdx)
    // Iterate over categories and create page for each
    dedupedCategories.forEach(category => {
      reporter.info(`Creating page: blog/category/${category}`)
      createPage({
        path: `blog/category/${_.kebabCase(transliterate(category))}`,
        component: require.resolve("./src/templates/categories.js"),
        // Create props for our CategoryList.js component
        context: {
          category,
          // Create an array of ids of articles in this category
          ids: allMdx.edges
            .filter(({ node }) => {
              return node.frontmatter.categories.includes(category)
            })
            .map(({node}) => node.id),
        },
      })
    })
}

Now I want to create a "Tag" taconomy, but I can't figure out how to do it beautifully and briefly, what and where to add to the gatsby-node.js so that I have two taxonomies created that work the same way as one. It is clear that you can simply duplicate this code and write "tag" instead of "category", but this is not very nice.

Just in case, here is my template code category.js

import React from "react"
import { Link, graphql } from "gatsby"
import Layout from '../components/layout'
import Seo from '../components/seo'




const CategoryList = ({ pageContext: { category }, data: { allMdx }, }) =>
(
  <Layout pageTitle={category}>

      {
      allMdx.edges.map(({ node }) => {
        return (
          <article key={node.id}>
            <h2>
                <Link to={`/blog/${node.frontmatter.slug}`}>
                {node.frontmatter.title}
                </Link>
            </h2>
            <p>Posted: {node.frontmatter.date}</p>
            <p>{node.excerpt}</p>
          </article>
        )
      })
      }

  </Layout>
)

export const query = graphql`
  query CategoryListQuery($ids: [String]!) {
    allMdx (filter: { id: { in: $ids } }) {
      edges {
        node {
          frontmatter {
            title
            date(formatString: "MMMM DD, YYYY")
            slug
          }
          id
          excerpt
        }
      }
    }
  }
`



export const Head = ({ pageContext }) => (
    <Seo
        title={pageContext.category}
        description={`Статьи из категории ${pageContext.category}`}
    />
)

export default CategoryList


Solution

  • I don't see anything wrong in your approach. In fact, is the way to go in terms of getting all tags and categories (hence a map) and loop through them to create the pages, at least given your approach.

    However, I think you can save some steps if you change your markdown structure: if each MDX has a key attribute (or similar) containing the type of taxonomy it is (article, tag, category, page, etc) you can create more succinct GraphQL queries, hence you can save the filters.

    For instance, in that way, you would be able to create a single query tags and categories:

      const tagsQuery = await graphql(`
        query getAllTags {
          allTags: allMarkdownRemark (
            filter: { frontmatter: { key: { in: ["tag"] }}}) {
            edges {
              node {
                id
                frontmatter {
                  name
                  slug
                  type
                  key
                }
              }
            }
          }
        }
      `);
    

    Note: you can use eq operator instead of in. In this case the array will accept more types of tags like projectTags, articleTags, etc.

    This allows you to create more specific approach because your data will contain allTags and allCategories, so you can create a general dedupeCategories (which will be named dedupeMdx) which no matter the input, will return a unique array (of tags or categories) because you don't care about the data, all MDX will have the same internal structure to the loop and the function can be agnostic to that.

    Following that approach, you can omit the following filter:

        context: {
          category,
          // Create an array of ids of articles in this category
          ids: allMdx.edges
            .filter(({ node }) => {
              return node.frontmatter.categories.includes(category)
            })
            .map(({node}) => node.id),
        },
    

    The filter won't be necessary if you pass the dedupedCategories array (and so with the tags) and use a filter GraphQL in the template query, which in fact is what you would do either way, so you are saving one step.

    In other words, you create pages for each category (or tag), pass the array of categories via context and get, from allMdx (filtered by the key + the unique array) the needed data.