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
- Part 1. : Redesigning My Website with the t3-stack
- Part 2 : Displaying Individual Articles
- Part 3 : Adding a table of contents to our blog articles
- Part 4 : Tidying Up the final product
Main Goals in this article
Here are some of our main goals to work through in this article
- 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>
. - Refactor main page to use SSG instead of a tRPC useQuery hook which might result in me getting rate-limited at some point
- 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
};
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:
-
getPostIDs
: This grabs all the individual Issue Ids from the repository and returns a list containing all the individual article titles. -
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;
});
};
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);
};
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,
};
}
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
- Get relevant data on every single post in the form of a humoungous query
- Filter through all this data and find a post with a title that matches the slug
- 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
}
}
}
}
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;
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;
};
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,
},
};
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} />;
})}
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;
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");
})
);
};
We can then call this in the getStaticProps
of our Home
component
export async function getStaticProps() {
const posts = await getPublishedPosts();
return {
props: {
posts,
},
};
}
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;
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
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
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'm Ivan
</h2>
<p className="text-xl text-gray-500">I'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;
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;
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)