fn()

Building my blog

Date written

5 December 2021

Reading time

15 minutes

It's been a while...

This post was written a while ago and some things have changed since this was written. An updated post will be created in due time, but this post remains here for archival purposes!

Heads up!

I expect the post ahead to be long for most people, and details can get very technical. If you are not as interested in learning more about the details of how I made my blog, this post may not be for you!

Ever since I got introduced to front-end web development early this year, I've come across blogs by many developers with varying purposes. Some were to have a platform to muse on the experiences they've had, while others (especially fellow developers!) would create blog posts to share little code tips — like snippets, tutorials, and more useful things — to pass on to other developers looking for some help.

I was particularly fond with this idea of being able to express yourself and pass on information at the same time, and being a developer myself meant that I could possibly create a blog for myself. I hadn't done anything like this before — that is, building a blog from scratch — and I had to rely on many resources I've found on the internet while developing it to make it the way it is now.

If you're an aspiring developer looking to create a blog for yourself, I hope this post will be of some help to you! Otherwise, perhaps it'll be interesting to spend some time reading through as well: it's all up to you!

I'll go through several aspects of developing my blog, which fairly resembles the goals or milestones I've set while developing the website as well. These aspects include:

  • setting up styling (thanks to Tailwind CSS, I can create beautiful-looking websites in a short amount of time);
  • reading and rendering Markdown posts (thanks to mdx-bundler, I can render and parse Markdown into HTML readable by React easily);
  • listing the posts on the index (home) page; and
  • creating tags to categorise and group posts together.

This certainly isn't a definitive guide to making your own blog, and I doubt that the source code for my blog alone is a prime example for other developers to follow. To that, I'll try to include several links in the form of resources to point you to the resources I've looked at while developing my blog. Perhaps you'll be able to code things differently and in your own way if you're a developer looking to create your own blog!

Part 0: Starting From Scratch

This website is powered by Next.js, one of the most popular React frameworks utilised by developers for the front-end web development of websites at the time of writing. My main goal was to use a product in the Jamstack architecture to leverage performance, security, and a better experience when developing. Next.js is just one of the many members of Jamstack, and there are many more that you may wish to consider for yourself.

Resource
The resources below were mentioned and may serve to help:

Jamstack: Static Site Generators

Part 1: Setting Up Styling

For styling, I relied on Tailwind CSS to help make designing and styling the website better. It is described to be a utility-first CSS framework with classes; this can be used to help you create consistent designs while still leaving you with enough freedom to customise and design at your own will.

Since Next.js uses npm to manage packages, Tailwind CSS can be easily installed using the npm install command. If you're using another package manager like yarn or pnpm, you can use their respective install commands as well:

npm install -D tailwindcss@latest postcss@latest autoprefixer@latest

Afterwards, you can generate the files required for Tailwind CSS to work:

npx tailwindcss init -p

Next up, you'll need to add Tailwind CSS to your framework; at this point, it is assumed that Next.js is being used. For other frameworks or methods, see the resource below.

Resource
For detailed instructions on installing Tailwind CSS, read here:

Tailwind CSS: Installation

In styles/global.css, prepend the following:

@tailwind base;
@tailwind components;
@tailwind utilities;

Once done, things should be all good to go! Try using a Tailwind CSS style somewhere. If things don't work as expected, try to consult the Tailwind CSS document for help.

Part 2: Reading and Rendering Markdown

Heads up!

This section assumes that Next.js is the framework used to develop the website.

To me, this was the most daunting challenge in creating a blog. I needed a way to:

  1. be able to read posts from a directory (for me, stored in posts/); and
  2. parse and render Markdown as HTML in a way that React can render it.

After much research (cough thank you, Google), I managed to find solutions to my concerns. Regarding the two points above:

  1. Node.js's fs module can be used to read the contents of a directory; and
  2. there are multiple parsers out there to help parse Markdown into plain HTML.

Since Next.js is being used, dynamic routes can be used to render any blog post at a dynamic path — for example, to see a post with the file name hello-world.mdx, Next.js can render it to example.com/posts/hello-world.

Dynamic routes in Next.js require getStaticPaths to know which paths to pre-render ahead of time — in the case of a blog, a list of slugs (i.e., the post names, possibly from their file names) can be passed for Next.js to dynamically generate.

getStaticPaths should be complemented with getStaticProps to fetch and render different data for different pages.

Resources
The resources below were mentioned and may serve to help:

Next.js: Dynamic Routes

Next.js: getStaticPaths

Next.js: getStaticProps

To create a dynamic route, I created a file somewhere in the pages/ directory. It could be in a subdirectory to your preference, but the link will be created at example.com/subdirectory/post instead of example.com/post if created at pages/ directory. The name of this file has to be surrounded with square brackets — for instance, the file I created was [post].tsx. In this case, Next.js will consider post to be a special value that I'll come to see later on.

For now, I made an 'empty' page with just the basic of things needed:

// [post].tsx
import type { NextPage } from "next";

const Post = () => {
  return <main>Hello, world!</main>;
};

export default Post;

Next, I had to utilise getStaticPaths to tell Next.js which paths I would like Next.js to generate ahead of time. For this to be done, I utilised the fs module from Node to read the contents of my intended posts folder (/posts) and fetch the paths from there.

// [post].tsx
export const getStaticPaths: GetStaticPaths = async () => {
  const files = fs.readdirSync(path.join(process.cwd(), "posts"), "utf-8"); // 1
  const paths = files.map((file) => ({
    params: {
      post: file.replace(".mdx", ""), // 2
    },
  }));

  return {
    // 3
    paths,
    fallback: false,
  };
};

As a breakdown for the code above:

  1. readdirSync is a function used to read the contents of a directory synchronously; it returns the name of the files found in the directory. For instance, if I had the following in my posts/ directory,

    pages/
    ├─ index.tsx
    └─ [post].tsx
    posts/
    ├─ hello-world.mdx
    └─ my-post.mdx
    .../
    

    readdirSync will return a list — ["hello-world.mdx", "my-post.mdx"].

  2. getStaticPaths require an object of objects to be returned. The objects inside the object should have a key of params and a value of another nested object; the key of this object should be the name of your dynamic route (what was in the square brackets in the current file name) and the value the route.

    The replace function is used to strip the .mdx off at the back of the file names, such that hello-world and my-post is passed.

    Illustrated, paths will look something like this:

    { // paths
        {
            params: { post: "hello-world" }
        },
        {
            params: { post: "my-post" }
        },
    }
    
  3. For getStaticPaths to work, paths and another value — fallback — needs to be returned. If fallback is false, a visitor will be redirected to the 404 page if no such path exists (for instance, if a visitor tries to access example.com/something). For more details on this, visit the getStaticPaths documentation in the resources above.

Where getStaticPaths generates the paths to possible blog posts in advance, getStaticProps actually fetches, renders, and passes the rendered Markdown (as HTML) to the component.

For my blog, I've settled to use mdx-bundler to compile and parse the Markdown into HTML. There are several other options to check out as well that you may wish to check out if you're interested in making a blog for yourself too. In essence, all options have the capability of turning your Markdown into HTML for React to render.

Resources
The resources below are some options for turning Markdown into HTML:

mdx-bundler

Unified.js

next-mdx-remote

I've settled on mdx-bundler since it can render imports (e.g., if using a custom component, like I intend to do!). Additionally, it supports the plugins from the Unified.js collective and allows more functionality!

To get started, I installed mdx-bundler:

npm install mdx-bundler

Once done, I can get started with getStaticProps by using the bundleMDX function from mdx-bundler:

// [post].tsx
export const getStaticProps: GetStaticProps = async (context) => {
  const slug = context.params?.post; // 1
  const unprocessedContent = fs
    .readFileSync(path.join("posts", slug + ".mdx"), "utf-8")
    .trim(); // 2

  const { code, frontmatter } = await bundleMDX({
    source: unprocessedContent,
    xdmOptions(options) {
      options.rehypePlugins = [...(options.rehypePlugins ?? []), rehypePrism];

      return options;
    },
  }); // 3

  return {
    // 4
    props: {
      frontmatter,
      code,
    },
  };
};
  1. I first started by retrieving the slug of the current path visited — for example, if a user visits example.com/hello-world, hello-world will be captured and passed into params as post (since the name of the file is [post]!). I stored this in a variable for easy use in the next line.

  2. I once again utilised fs to read the specific file synchronously and store its contents into unprocessedContent.

  3. The bundleMDX function is used and unprocessedContent is passed into source. I had an additional rehype plugin (part of the Unified.js collective) to use, so I configured xdmOptions and rehypePlugins. Remember, always refer to the documentation (linked in the closest Resources section above) for help if there's some trouble understanding!

  4. getStaticProps accepts a props key with an object as the value. Anything in the props object will be passed to the component (created earlier on as a Post functional component).

At this point, I'm getting a little closer to the end! Now, all I had to do was alter the Post functional component.

import { FaCalendar } from "react-icons/fa";

interface PostProps {
  // 1
  frontmatter: Frontmatter;
  code: string;
}

const Post: NextPage<PostProps> = ({ frontmatter, code }: PostProps) => {
  const RenderedComponent = useMemo(() => getMDXComponent(code), [code]); // 2

  return (
    <>
      <Layout>
        <section className="h-[33vh] lg:h-[50vh] bg-black pb-10 flex flex-col justify-end rounded-b-3xl px-10 md:px-20 lg:px-40">
          {/* 3 */}
          <h1 className="text-4xl font-bold font-heading sm:text-5xl lg:text-7xl 2xl:text-8xl">
            {frontmatter.title}
          </h1>
          <span className="flex items-center pt-5 space-x-2 sm:text-xl">
            <FaCalendar title="Date written" aria-label="Reading time" />
            <p>{frontmatter.date}</p>
          </span>
        </section>
        {/* 4 */}
        <article className="px-20 py-10 prose prose-lg lg:py-20 sm:prose-xl md:prose-2xl max-w-none md:px-40 lg:px-80">
          <RenderedComponent />
        </article>
      </Layout>
    </>
  );
};
  1. Since I'm using TypeScript, I have to create an interface to give type declarations for Post. If you're using JavaScript, feel free to move on!

  2. React's useMemo is used to prevent having to render every blog post every single time, possibly saving resources in the long run. This is covered in the documentation for mdx-bundler, so read it over there for more information! getMDXComponent will return the Markdown post in the form of HTML.

  3. Any frontmatter created in the Markdown file (between the three --- at the top of the file) will be accessible separately; I wish to render them uniquely and used Tailwind CSS to style the frontmatter.

  4. The value returned from getMDXComponent stored in RenderedComponent can be used as a React component and inserted easily into the page like so.

    Additionally, I utilised Tailwind's typography plugin and used the prose class to have Tailwind automatically style the compiled HTML (from Markdown). Everything you see here is the result of that plugin!

Resources
The resources below were mentioned and may serve to help:

TypeScript: Interfaces

React: useMemo

MDX: Frontmatter

Tailwind CSS: Typography Plugin

With all that done, I managed to get dynamic routes for my blog working!

Part 3: Listing the Post on the Index Page

I learnt that getStaticPaths can be used to fetch and pass data into the page component. From there, I decided to use it to fetch the frontmatter from all my posts and pass them into the component. Here's a brief overview of my getStaticPaths function:

import { matter } from "gray-matter";

export const getStaticProps: GetStaticProps = async () => {
  const files = fs.readdirSync(path.join(process.cwd(), "posts"), "utf-8"); // 1
  const postFrontmatters = files.map((file) => {
    const unprocessedContent = fs.readFileSync(
      path.join(process.cwd(), "posts", file),
      "utf-8"
    );
    const frontmatter = matter(unprocessedContent).data; // 2
    frontmatter["slug"] = "/" + file.replace(".mdx", "");
    const tags = frontmatter["tags"]; // 3
    typeof tags === "string"
      ? (frontmatter["tags"] = tags.split(","))
      : (frontmatter["tags"] = tags);
    return frontmatter;
  });

  return {
    props: {
      frontmatters: postFrontmatters, // 4
    },
  };
};

The process has generally not changed:

  1. I read the list of files in the posts/ directory and stored them into files.

  2. I mapped through each one to extract each post's frontmatter. This time, though, I didn't want to use mdx-bundler since it'll be heavy — I just needed the frontmatter and not the compiled Markdown. Therefore, I opted to use gray-matter to help extract only the frontmatter.

  3. There appears to be an issue where tags may be automatically parsed into a list by splitting the comma between tags or passed entirely as a string; here, I check if the tags frontmatter is a string and break the tags if required.

  4. Finally, after all post frontmatters have been extracted into postFrontmatters, postFrontmatters is passed into the index page component.

I created a Posts component to handle the rendering of posts in a grid. I made that decision so that I'll be able to use the same component in the tags page — another dynamic route that filters posts by their tags.

Passing the frontmatters of all (or some of) the posts, I can get the component to render a grid of posts. This chunk of code may be long, so feel free to ignore it if you'd like to!

const Posts = ({ frontmatters }: PostsProps) => {
  return (
    <section className="grid grid-cols-1 px-10 py-10 lg:grid-cols-2 lg:py-20 md:px-12 lg:px-20 gap-y-10 lg:gap-y-0 lg:gap-x-10">
      frontmatters.map((frontmatter) => {
          return (
            <Link href={frontmatter.slug!} key={frontmatter.title} passHref>
              <div className="p-10 space-y-2 transition-colors duration-200 border border-gray-700 cursor-pointer hover:border-blue-400 rounded-2xl">
                <h2 className="text-4xl font-bold font-heading">
                  {frontmatter.title}
                </h2>
                {frontmatter.description ? (
                  <p className="sm:text-lg md:text-xl">
                    {frontmatter.description}
                  </p>
                ) : null}
                <span className="flex flex-row items-center space-x-2">
                  <FaCalendar />
                  <p>{frontmatter.date}</p>
                </span>
                {frontmatter.tags === undefined ? null : (
                  <span className="flex flex-row items-center space-x-2">
                    {frontmatter.tags.length > 1 ? <FaTags /> : <FaTag />}
                    {frontmatter.tags.map((tag) => (
                      <Link href={`/tags/${tag}`} key={tag} passHref>
                        <p
                          className="px-2 transition-colors duration-200 border border-gray-700 rounded-lg cursor-pointer hover:border-blue-400"
                          key={tag}
                        >
                          {tag}
                        </p>
                      </Link>
                    ))}
                  </span>
                )}
              </div>
            </Link>
          );
        })
    </section>
  );
};

Part 4: Creating Tags

The tags page is (partially) an accumulation of the things that have been done previously. Specifically:

  • the use of dynamic routes (pages/tags/[tag].tsx);
  • the use of getStaticPaths and getStaticProps to:
    1. allow Next.js to create the links to all the existing tags ahead of time;
    2. fetch and parse the frontmatters of all posts; and
    3. filter the frontmatters based on the current tag; and
  • the rendering of the grid of posts using Posts.

Conclusion

If you've read to this point, you sure are interested! Thank you for reading this far, and I hope that I've managed to share what I know with potential viewers of this blog. I hope to create more verbose learning logs like this to document my experience learning everything; not only will it help having a place to refer back to, it's also nice to organise my thoughts in a way.

On the contrary, if there's anything that can be improved on in the way I developed my blog, please feel free to reach out! I'll be more than willing to hear you out and learn from you.

If you are a developer looking to get started with a blog, all the best with everything! May your experience and journey be a positive one.