Brian McCaffrey

Creating a Tagging System With gray-matter

When I began maintaining my website, making it easy to navigate was a priority. I followed a tutorial [1] to initially create the site which provided a way for me to write posts as markdown files with metadata, and sorted the posts by date. The tutorial code naturally left something to be desired. I wanted a way to organize similar topics together. To address this, I implemented a tagging system. The tagging system seamlessly integrated with my current setup. It allows users to explore specific topics and does not interfere with the default sorting of posts in chronological order. Let's delve into the details of what it took to enhance the user experience on my website!

Requirements

I wanted the tagging system to adhere to these requirements.

  • The system must not interfere with the default sorting of posts by date on the post list page.
  • The blog page must include a list of selected tags
  • Each post will store its tags in the front matter metadata
  • The blog page must start with all tags selected. When the first tag is clicked or tapped, all other tags become unselected.
  • When a tag is clicked or tapped, only that tag is activated, displaying posts that include that tag in its tag list
  • If a user has already selected one tag and chooses another, it is added to the list of selected tags and posts that include either tag are listed
  • If a user clicks on a tag that is already selected, the system should remove that tag from the group of selected tags

Notes

I opted to write my own solution rather than utilizing any existing packages to implement my tagging system. My decision stemmed from a desire to deepen my understanding and sharpen my skills. Currently, my website is dependent on Next.js, a popular React framework, for its functionality. Additionally, I use gray-matter for parsing metadata from each of my blog posts. The metadata is stored as front matter[2], which is also stored in the markdown files alongside the content of my blog posts. If you're interested in implementing a tagging system using gray-matter for front matter parsing, feel free to follow along!

Steps

Step 1 - Store tags in the document and parse the tags in the code

I first needed to decide how we should represent the tags in our blog posts. To do so, I tried to write my tags like this.

front matter example

This approach did not work. When I checked the results of parsing the front matter seen above in the code, it seemed that the array I defined after the 'tags' key would simply be passed as a string; brackets, quote-ticks, commas, and all. The other front matter is also just a string. Therefore, I decided to implement a simple function to convert a comma-delineated string into an array, and utilize this function to parse the data. This way, I could store my tags in the front matter as such a sting. I decided that my tags would look like this:

front matter example

and that I would parse this part of the front matter like this:

import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';

...

/**
 *
 * @param {string} the post id
 * @returns {string[]} a list oftags
 */
export function getTags(id) {
  const fullPath = path.join(postsDirectory, `${id}.md`); //code to retrieve the post
  const fileContents = fs.readFileSync(fullPath, 'utf8');

  const matterResult = matter(fileContents); // Use gray-matter to parse the post metadata section

  return matterResult.data.tags.split(',');
}

Step 2 - Display the array at the top of the post page

To display the tags corresponding to each post on the post page itself, I called the getTags function in the component's getStaticProps function. Then, I could pass the results into the component's props.

getting tags of posts

Next, in the component function, I created a constant called tagsList which mapped each value of my tags prop to a li element.

generating tag html

Finally, in the returned template, I included the following at the top of the page:

 <ul style={PostStyles.tagList}>{tagsList}</ul>

These steps result in the tags being rendered at the top of every blog post page on my website as an unordered list.

Code

...

export async function getStaticProps({ params }) {
  const tags = getTags(params.id);
  const postData = await getPostData(params.id);
  return {
    props: {
      postData,
      tags,
    },
  };
}

...

export default function Post({ postData, tags }) {
  const tagsList = tags.map((tag) => (
    <li className={PostStyles.tagText} key={tag}>
      {tag}
    </li>
  ));

  return (
    <Layout post backHref={'/blog'}>
    ...
     <!-- Rendered list of tags -->.
        <ul className={PostStyles.tagList}>{tagsList}</ul>
    ...
    </Layout>
  );
}

Step 3 - Styling the tags

At that point, the tags looked like this:

tags POC

I wanted the tags to be a comma-separated list of grey text at the top of the post. To that end, I decided to use css to style the <ul> and <li> elements. In my stylesheet, I utilized the CSS :not(:last-child):after pseudo-class selector to select the end of the element for all of the li elements except for the final one. Then I applied the CSS content property to insert a comma and space after each element of the list. Tip: it turns out that to insert a space using the :after selector, you must escape it like this.

list element styling

This styling results in rendering the tags like this.

tags final state

Step 4 - Sorting by tags

Back to the blog page. Each of my blog posts were displayed on the blog page as a list of posts ordered by date. Per the requirements that I set out for myself, I wanted to be able to click a tag button and hide all posts without that tag. I broke the problem into 2 tasks:

  1. Get all tags from all posts, so we know what buttons to create at the top of the blog page
  2. Filter posts by selected tags

In my blog component, I populate a constant allPostsData, which includes the ID of every post. Therefore, I can leverage the getTags() function, which takes a post id, to create a getAllTags function.

This function returns a list which has the tags that my blog posts contain with no duplicates. Each tag is represented in this list only once, therefore we can use the list to render our tags as buttons at the top of the blog page.

getAllTags function

I called the new function in the blog component's getStaticProps.

getStaticProps

Finally, we can use the array of tags to create an array of button elements (and of course add some nice CSS).

export default function Blog({ allPostsData, allTags }) {
  const [selectedTags, setSelectedTags] = React.useState(allTags);

  const tagButtons = allTags.map((tag) => {
    return (
      <button
        onClick={() =>
          setSelectedTags(
            getToggleTagState(tag, selectedTags, allTags)
          )
        }
        key={tag}
        value={selectedTags.includes(tag) ? 'true' : 'false'}
        className={utilStyles.toggleButton}
      >
        {tag}
      </button>
    );
  });

  return (
    <Layout>
      <Head>
        <title>Bri's Blog</title>
      </Head>
      ...
        <div>
          <p>Choose tags: </p>
          {tagButtons}
          <button onClick={() => setSelectedTags([...allTags])}>
            Reset all tags
          </button>
        </div>
      ...
    </Layout>
  );
}

The next step was to track the state of the selected arrays. I initialized an array with all tags as an element. Then, conditionally show each post if and only if some tag on the post can be seen on the array of selected tags. In the starting state, when the user clicks a tag button, the array of selected tags should be cleared out, and then the clicked tag should be added to the array. Then, subsequent states will have to handle the following cases:

  1. the user clicks a tag button which is already selected -> the selected tag should be REMOVED from the array of selected tags
  2. the user clicks a tag button which has not been selected, and the selected tag array is not at the max length -> the clicked tag should be added to the array

Finally, I added one extra button as an extra utility, which simply resets the selected tag array to the starting state.

Step 5 - Next steps

I should implement the following features:

  • Click on a tag on the post page, be brought back to the blog page with only that tag selected (i.e., see more posts like this)

Conclusion

This was a great project to implement! The hardest part of this entire endeavor was writing about the development! I have never written a coding guide before, so please bear with me as I improve this post over time. If you found this guide while trying to implement your own tagging system, I hope it was at least of some help!

[1] https://next-learn-starter.vercel.app [2] https://dev.to/dailydevtips1/what-exactly-is-frontmatter-123g