Building this Website: Part 2
Or how the code works
So the decisions regarding the tech stack had been made, but now it had to be created. For this I'm just going to talk about the blog posts themselves and how the code for that works. As stated earlier, the aim was for there to be just one page.tsx for the blog posts that would take the markdown files and create the blog post page. As many of us who code know, DRY or Don't Repeat Yourself is very important. It never actually crossed my mind to use an alternative method.
The first thing the code needs to do is too get all the posts so that we can present either the latest three on the front page, or all of them on the journal page. Fr that, I have a 'lib' folder to typescript files for these methods and created types. What this method , getAllPosts(), does is take the folder directory and takes all the files that are present in it. Since we are using Typescript, we have to define a type for it, so the 'types.ts' file saved in the same lib folder has these definitions for us. The first important one is called PostFrontmatter which just contains important information about the post but not it's content. Then there are two more types, PostListing which extends PostFrontmatter and adds the slug to it, and PostFull which then extends PostFrontmatter and includes the content. The getAllPosts() just returns the PostListing of all the posts so that they can be displayed on the front page and the journal page.
export function getAllPosts(): PostListing[] {
const fileNames = fs.readdirSync(postsDirectory);
return fileNames
.map(fileName => {
const slug = fileName.replace(/\.mdx?$/, '');
const fullPath = path.join(postsDirectory, fileName);
const fileContents = fs.readFileSync(fullPath, 'utf8');
const { data } = matter(fileContents);
if (typeof data.title !== 'string' || typeof data.date !== 'string') {
console.warn(`Skipping invalid post: ${fileName}`);
return null;
}
return {
slug,
title: data.title,
date: data.date,
subtitle: data.subtitle,
category: data.category
};
})
.filter((post): post is PostListing => post !== null)
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
}
As you can see, there's a few checks in there to prevent issues, although all I have to do to avoid that is just make sure it has all four necessary things at the start. On the journal page, we can then iterate through all these posts and use the slug value to create a link to the post page.
The folder structure for the page is 'post/[slug]' with the slug essentially being the ID of the post and the name of it's file (if you see the URL you can see that this post's ID is '5' and the name of the file hence is 5.md). With that, the page.tsx file that presents the blog post can get the ID of the post using the url.
export async function generateMetadata({ params }:{params: Params}): Promise<Metadata> {
const { slug } = await params;
const post: PostFull = await getPostBySlug(slug)
return {
title: `${post.title} | My Personal Space`,
description: post.subtitle || "Blog post",
}
}
As you can see here, this calls a different method saved in the same file as getAllPosts() called getPostBySlug() that takes the slug and just gets a single post wiht it.
export async function getPostBySlug(slug: string): Promise<PostFull> {
const fullPath = path.join(postsDirectory, `${slug}.md`)
const fileContents = fs.readFileSync(fullPath, 'utf8')
const { data, content } = matter(fileContents)
const source = content;
return {
slug,
title: data.title,
date: data.date,
subtitle: data.subtitle,
category: data.category,
content: source
};
}
Now with the PostFull object, we can take the content and convert it to HTML to be presented on the page. This is done using next-mdx-remote-client, a third party extension that allows for the this. With this, we can create a file called MdxContent.tsx, the purpose of which is to take most of the possible HTML elements and convert it to how I'd like it displayed.
<MdxContent source={post.content} />
And thus, we have a markdown file converted to a blog post.