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!
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 topics 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.
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.
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
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:
- be able to read posts from a directory (for me, stored in
posts/
); and - 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:
- Node.js’s
fs
module can be used to read the contents of a directory; and - 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.
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:
-
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 myposts/
directory,pages/ ├─ index.tsx └─ [post].tsx posts/ ├─ hello-world.mdx └─ my-post.mdx .../
readdirSync
will return a list —["hello-world.mdx", "my-post.mdx"]
. -
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 thathello-world
andmy-post
is passed.Illustrated,
paths
will look something like this:{ // paths { params: { post: "hello-world" } }, { params: { post: "my-post" } }, }
-
For
getStaticPaths
to work,paths
and another value —fallback
— needs to be returned. Iffallback
is false, a visitor will be redirected to the 404 page if no such path exists (for instance, if a visitor tries to accessexample.com/something
). For more details on this, visit thegetStaticPaths
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.
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,
},
};
};
-
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 intoparams
aspost
(since the name of the file is[post]
!). I stored this in a variable for easy use in the next line. -
I once again utilised
fs
to read the specific file synchronously and store its contents intounprocessedContent
. -
The
bundleMDX
function is used andunprocessedContent
is passed intosource
. I had an additional rehype plugin (part of the Unified.js collective) to use, so I configuredxdmOptions
andrehypePlugins
. Remember, always refer to the documentation (linked in the closest Resources section above) for help if there’s some trouble understanding! -
getStaticProps
accepts aprops
key with an object as the value. Anything in theprops
object will be passed to the component (created earlier on as aPost
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>
</>
);
};
-
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! -
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. -
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. -
The value returned from
getMDXComponent
stored inRenderedComponent
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!
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 topics = frontmatter["topics"]; // 3
typeof topics === "string"
? (frontmatter["topics"] = topics.split(","))
: (frontmatter["topics"] = topics);
return frontmatter;
});
return {
props: {
frontmatters: postFrontmatters, // 4
},
};
};
The process has generally not changed:
-
I read the list of files in the
posts/
directory and stored them intofiles
. -
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.
-
There appears to be an issue where topics may be automatically parsed into a list by splitting the comma between topics or passed entirely as a string; here, I check if the topics frontmatter is a string and break the topics if required.
-
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 topics page — another dynamic route that filters posts by their topics.
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.topics === undefined ? null : (
<span className="flex flex-row items-center space-x-2">
{frontmatter.topics.length > 1 ? <Fatopics /> : <FaTag />}
{frontmatter.topics.map((tag) => (
<Link href={`/topics/${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 topics
The topics page is (partially) an accumulation of the things that have been done previously. Specifically:
- the use of dynamic routes (
pages/topics/[tag].tsx
); - the use of
getStaticPaths
andgetStaticProps
to:- allow Next.js to create the links to all the existing topics ahead of time;
- fetch and parse the frontmatters of all posts; and
- 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.
This post has included the following resources:
Jamstack: Static Site Generators
Tailwind CSS: Installation
Next.js: Dynamic Routes
Next.js: getStaticPaths
Next.js: getStaticProps
mdx-bundler
Unified.js
next-mdx-remote
TypeScript: Interfaces
React: useMemo
MDX: Frontmatter
Tailwind CSS: Typography Plugin