DEV Community

ivanleomk
ivanleomk

Posted on

Tidying Up the Final Product

Initial Set Up

This is a series of blog posts which detail how I'm setting up my website with the t3 stack. If you haven't read the previous articles, I suggest looking at

Main Goals in this article

Here are some of our main goals to work through in this article

  1. Rewrite the existing slugs for the article pages so they map from <url>/blog/1 which is honestly kind of ugly to <url>/blog/<slug generated from title>.
  2. Refactor main page to use SSG instead of a tRPC useQuery hook which might result in me getting rate-limited at some point
  3. Prettifying the main page

Rewriting Existing Slugs

Previous Configuration

In Part 2 of this series, we looked at using Github Issues as a CMS. One of the things that we implemented there was to grab data from Github Issues using the issue number assigned to the article.

This resulted in url slugs such as <url>/blog/1 which admittedly was not the most satisfying to look at. I was hoping instead to be able to have article urls that looked more like <url>/blog/<slug generated from title>.

If you're unsure what a slug is, it is a modified version of a string which can be used in a url. A good example might be of an Article which is titled "Adding a Table Of Contents To Blog Articles" that can be converted into a slug adding-a-table-of-contents-to-blog-articles. Notice here that all letters are lowercased and spaces are replaced with -.

Let's first start by writing a quick function to generate slugs for our individual titles. We can do so by chaining a series of replace function calls

export const slugify = (text: string) => {
  return text
    .toString()
    .toLowerCase()
    .replace(/\s+/g, "-") // Replace spaces with -
    .replace(/[^\w-]+/g, "") // Remove all non-word chars
    .replace(/--+/g, "-") // Replace multiple - with single -
    .replace(/^-+/, "") // Trim - from start of text
    .replace(/-+$/, ""); // Trim - from end of text
};
Enter fullscreen mode Exit fullscreen mode

This allows us to now generate our slugs. Now we need to rewrite our [issueId].tsx file which will statically generate all the relevant pages for each respective article.

Refactoring getStaticProps

Let's first rename [issueId].tsx to [slug].tsx so it more accurately reflects what we are trying to achieve. We have two functions to modify/write:

  1. getPostIDs : This grabs all the individual Issue Ids from the repository and returns a list containing all the individual article titles.
  2. getSinglePost : This grabs the information for a post given a slug that we provide

getPostIDs

Currently, we are returning the individual issue Ids of the various issues that we have in our respository using the function as seen below

export const getPostIds: () => Promise<number[]> = async () => {
  const { repository } = await graphqlWithAuth(
    `query getPostIds {
      repository(owner: "ivanleomk", name: "personal_website_v2") {
        issues(last: 100) {
            nodes {
                number
            }
        }
      }
  }
`,
    {}
  );

  // Quick Type Definition here

  return repository.issues.nodes.map((issue: { number: number }) => {
    return issue.number;
  });
};
Enter fullscreen mode Exit fullscreen mode

What we want is the titles of the individual articles so we should just replace the number with the title instead. This gives us a new function which looks something like what we see below

export const getPostIds: () => Promise<string[]> = async () => {
  const { repository } = await graphqlWithAuth(
    `query getPostIds {
      repository(owner: "ivanleomk", name: "personal_website_v2") {
        issues(last: 100) {
            nodes {

                title
            }
        }
      }
  }
`
  );

  // Quick Type Definition here
  return repository.issues.nodes.map((issue: { title: string }) => issue.title);
};
Enter fullscreen mode Exit fullscreen mode

What we see in turn is that this now returns for us a list of the individual titles. We can then call this in getStaticPaths in order to generate the relevant paths for our specific function as seen below.

If you're unaware of what getStaticPaths does, it basically generates a list of paths that can be matched using the [slug] pattern. By doing so, we can prevent users from accessing unauthorized/invalid paths and is a feature that is built into NextJS.

export async function getStaticPaths() {
  const posts = await getPostIds();
  const paths = posts.map((issueId) => `/blog/${slugify(issueId)}`);

  return {
    paths,
    fallback: false,
  };
}
Enter fullscreen mode Exit fullscreen mode

getSinglePost

I struggled quite a bit trying to find an optimal solution to this and realised that I was indeed dealing with a Yacht problem. You can read more about it here. As a result, I realised that I should prioritise banging out a solid prototype and instead work on optimizing it down to road.

After giving it some thought, I realised that what might be a potential solution would be to

  1. Get relevant data on every single post in the form of a humoungous query
  2. Filter through all this data and find a post with a title that matches the slug
  3. Return the data that has been filtered

I think the main downside with this approach is that I am making multiple repetitive calls to the same API for the same payload but I wasn't quite sure how to best memoize the data when generating data with NextjS. We first begin by writing out a quick graphql request which will give us all the data that we need as seen below.

query getPost{
      repository(owner: "ivanleomk", name: "personal_website_v2") {
        issues(last:100){
          edges{
            node{
              title
              number
              createdAt
              body
            }
          }
        }
      }
Enter fullscreen mode Exit fullscreen mode

We then filter through this entire list of information by using a simple filter higher order function

const post = repository.issues.edges.filter((issue) => {
    return slugify(issue.node.title) === slug;
  })[0].node;
Enter fullscreen mode Exit fullscreen mode

before finally returning the single element node which corresponds to the valid article. We can combine this in the form of a function getSinglePost as seen below

export const getSinglePost: (slug: string) => Promise<githubPost> = async (
  slug: string
) => {
  const { repository } = await graphqlWithAuth(
    `
    query getPost{
      repository(owner: "ivanleomk", name: "personal_website_v2") {
        issues(last:100){
          edges{
            node{
              title
              number
              createdAt
              body
            }
          }
        }
      }
}
    `
  );

  const post = repository.issues.edges.filter((issue) => {
    return slugify(issue.node.title) === slug;
  })[0].node;

  return post;
};
Enter fullscreen mode Exit fullscreen mode

We can then call this in our getStaticProps function. This replaces our initial getPostByIssueId that we wrote in Part 2

Once this is done, we can then return this in our getStaticProps function as seen below.

export async function getStaticProps({ params }: BlogPostParams) {
  const { slug } = params;
  // const post = await getPostByIssueId(parseInt(issueId));
  const post = await getSinglePost(slugify(slug));
  const { title, body, createdAt } = post;

  const { content: parsedBody } = matter(body);

  const content = await renderToHTML(parsedBody);

  return {
    props: {
      content: String(content),
      title,
      createdAt,
      rawContent: body,
    },
  };
Enter fullscreen mode Exit fullscreen mode

The code inside is otherwise identical. We can then save our changes, refresh the page and voila, we have now ported over our articles to the new url standard. We just need to update the links that we populate in PostLink to utilise this new url convention and we have now ported our article pages over to the new URL standard

{posts.data?.posts?.map((item) => {
        return <PostLink key={item.title} post={item} />;
 })}
Enter fullscreen mode Exit fullscreen mode

Refactoring index.tsx to use SSG instead of useQuery

Why we need to refactor this .tsx option

Currently, our index.tsx looks something like this

import type { NextPage } from "next";
import Head from "next/head";
import PostLink from "../components/PostLink";
import { trpc } from "../utils/trpc";

const Home: NextPage = () => {
  const posts = trpc.useQuery(["github.get-posts"]);

  return (
    <>
      <h1>Posts</h1>
      {posts.data?.posts?.map((item) => {
        return <PostLink key={item.title} post={item} />;
      })}
    </>
  );
};

export default Home;
Enter fullscreen mode Exit fullscreen mode

it does the job but the problem with this is that everytime someone loads the page, we are making a call to the github API. What this means is that if 1000 people access my site within an hour, I will be rate-limited. As a result, I think it's a pretty glaring error to fix.

This is where our wonderful friend getStaticProps comes in! Notice here that we don't need to declare getStaticPaths because we are not matching on a wildcard like we do in [slug].tsx . Instead we only have a single defined route, that is <url>/ that we match in index.tsx.

Therefore, we can add in a getStaticProps function that loads up the entire list of posts when the index page is being generated. Let's quickly scaffold this out.

Writing out a getStaticProps function

We already have an existing function which we have written called getPublishedPosts which extracts out metadata of all published posts. However, since it's the front page, I'll like to only display the latest 10 posts instead of all my posts. That way users don't feel overwhelmed by my website and can see more recent and up-to-date content. We first begin by rewriting our getPublishedPosts function to only return 10 articles at a time.

export const getPublishedPosts: () => Promise<githubPostTitle[]> = async () => {
  const { repository } = await graphqlWithAuth(`
        {
          repository(owner: "ivanleomk", name: "personal_website_v2") {
            issues(last: 10) {
              edges {
                node {
                    number
                    title
                    createdAt
                    body
                    labels(first: 3) {
                        nodes{
                            name
                        }
                    }
                }
              }
            }
          }
        }
      `);

  return (
    repository.issues.edges
      //@ts-ignore
      .map(({ node }) => {
        return { ...node };
      })
      .filter((post: githubPostTitle) => {
        return post.labels.nodes.some((label) => label.name === "published");
      })
  );
};
Enter fullscreen mode Exit fullscreen mode

We can then call this in the getStaticProps of our Home component

export async function getStaticProps() {
  const posts = await getPublishedPosts();
  return {
    props: {
      posts,
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

which allows us to pass in a list of Posts into our component as a prop. We can then utilise our previous githubPost declaration which we wrote in a previous post to type this prop, thus allowing us to generate the new Home page as seen below

import Head from "next/head";
import PostLink from "../components/PostLink";
import { getPublishedPosts, githubPostTitle } from "../utils/github";

type HomePageProps = {
  posts: githubPostTitle[];
};

const Home = ({ posts }: HomePageProps) => {
  return (
    <>
      <h1>Posts</h1>
      {posts &&
        posts?.map((item) => {
          return <PostLink key={item.title} post={item} />;
        })}
    </>
  );
};

export async function getStaticProps() {
  const posts = await getPublishedPosts();
  return {
    props: {
      posts,
    },
  };
}

export default Home;
Enter fullscreen mode Exit fullscreen mode

And with this, we've successfully ported over our index page to a statically generated page using SSG! Now all that is left to do is to fix up the post links that we've generated. Ideally, we want to add more metadata and information for them so that users can make more educated choices when browsing through them.

Currently they look like this

Screenshot 2022-07-31 at 9 53 13 PM

and I think we can do a lot better. Let's now add a little bit of parsing such that we display the data which the

  • The date it was created
  • The tags which I want to display for this specific article

Feel free to copy the following code in order to achieve the desired result

Screenshot 2022-08-04 at 1 32 18 PM

which corresponds to the code as seen below

import PostLink from "../components/PostLink";
import { getPublishedPosts, githubPostTitle } from "../utils/github";

type HomePageProps = {
  posts: githubPostTitle[];
};

const Home = ({ posts }: HomePageProps) => {
  return (
    <div className="flex items-center justify-center mt-10">
      <div className="max-w-4xl w-full ">
        <div>
          <h2 className="text-3xl font-extrabold tracking-tight sm:text-4xl">
            Yo I&apos;m Ivan
          </h2>
          <p className="text-xl text-gray-500">I&apos;m a software engineer</p>
        </div>
        <div className="container mx-auto mt-10">
          <div className="max-w-lg">
            <div className="flex flex-col items-start justify-content">
              <h1 className="font-bold text-lg tracking-tight">Latest Posts</h1>
              <ul>
                {posts &&
                  posts?.map((item) => {
                    return <PostLink key={item.title} post={item} />;
                  })}
              </ul>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

export async function getStaticProps() {
  const posts = await getPublishedPosts();
  return {
    props: {
      posts,
    },
  };
}

export default Home;
Enter fullscreen mode Exit fullscreen mode

And the code for the post link. Note that you'll need to install day.js in order to be able to nicely format the code.

import Link from "next/link";
import React from "react";
import { githubPostTitle } from "../utils/github";
import { slugify } from "../utils/string";
import dayjs from "dayjs";

type PostLinkProps = {
  post: githubPostTitle;
};

const PostLink = ({ post }: PostLinkProps) => {
  return (
    <Link href={`blog/${slugify(post.title)}`}>
      <div className=" py-3 pl-5">
        <p className="cursor-pointer hover:underline">
          {post.title} | {dayjs(post.createdAt).format("DD-MM-YYYY")}
        </p>
        <div className="ml-2 flex-shrink-0 flex"></div>
      </div>
    </Link>
  );
};

export default PostLink;
Enter fullscreen mode Exit fullscreen mode

Now all that's left is to automatically generate code which will allow us to display each individual series of posts which we've written! We'll cover this in the next article so stay tuned! :)

Top comments (0)