Revamp Your Blog for the New Year: Open Source Edition
December 13, 2023 (11 months ago)
As we welcome the new year, it's the perfect time to refresh our digital spaces. My journey into redoing my website began with an encounter with a tweet from Lee Robinson, sparking inspiration for a complete overhaul. Here, I share the steps, tools, and decisions that shaped my revamped blog – now open for you to explore, fork, and enjoy.
Every year, I procrastinate writing so that I can instead rebuild my blog. This year I did both. leerob.io/blog/2023
The Inspiration Behind the Change
Inspired by Lee's minimalist approach, I embarked on a journey to declutter my package.json
. I evaluated my existing tools, keeping some, discarding others, and embracing new ones. Here's a glimpse of the decisions I made:
If you want to jump directly to the finished code, check here
I started from scratch with simplicity in mind. Let's go through the decision making process:
- From Next pages router to App router: Futureproofing and keeping up to date with Next >13.
- Replacing CollectedNotes and MDXBundler with next-mdx-remote: Simplifying content management.
- Switching from HeadlessUI to shadcn/ui: Opting for a more customizable, accessible, and lightweight UI toolkit.
- Introducing a View Counter using Redis (Upstash): A fun, non-intrusive way to keep track of engagement.
App router
Wit the paradigm shift of the Next.Js team to the app router it was a no-brainer. I have to be honest here: At Antartida we were early adopters of the app router and we had to deal with some performance and compatibility issues at first. I'm glad the Next.Js team has focused so much in fixing those issues in following releases and now it's the way to go.
The Next team's shift towards the app router was a big paradigm shift. At Antartida, we were early adopters, initially facing some performance and compatibility challenges. However, the Next team's dedication to improvement made it an essential part of our toolkit.
Simplifying with next-mdx-remote
Aiming for simplicity, I decided to eliminate third-party services. This also allows me to include more dynamic content in the future.
Here's my mdx.ts
file in my lib
folder:
import path from "path";
import fs from "fs";
import { compileMDX } from "next-mdx-remote/rsc";
import { config } from "@/config";
function getMDXFiles(dir: string) {
return fs.readdirSync(dir).filter((file) => path.extname(file) === ".mdx");
}
async function readMDXFile(filePath: string) {
let rawContent = fs.readFileSync(filePath, "utf-8");
const mdx = await compileMDX({
source: rawContent,
options: { parseFrontmatter: true },
});
return { ...mdx, rawContent };
}
async function getMDXData(dir: string) {
const files = getMDXFiles(dir);
const promises = files.map(async (file) => {
const filePath = path.join(dir, file);
const { frontmatter, rawContent } = await readMDXFile(filePath);
return {
slug: file.replace(".mdx", ""),
frontmatter,
rawContent,
};
});
const data = await Promise.all(promises);
return data;
}
export async function getBlogPosts() {
return await getMDXData(path.join(process.cwd(), "content"));
}
And it's being used like this:
import { notFound } from "next/navigation";
import { MDXRemote } from "next-mdx-remote/rsc";
import { getBlogPosts } from "@/lib/mdx";
export default async function Post({ params }: { params: PageParams }) {
const post = (await (
await getBlogPosts()
).find((post) => post.slug === params.slug)) as {
slug: string;
frontmatter: FrontMatter;
rawContent: string;
};
if (!post) {
return notFound();
}
return (
<main className="mx-auto max-w-screen-md w-full px-4 md:px-0">
<div className="mt-16">
<h1 className="scroll-m-20 text-3xl font-extrabold tracking-tight lg:text-4xl">
{post.frontmatter.title}
</h1>
</div>
<div className="mt-12">
<MDXRemote
source={post.rawContent}
components={{ ...components }}
options={{
parseFrontmatter: true,
}}
/>
</div>
</main>
);
}
This approach will compile the MDX files twice, once to get frontmatter data and another one to get the actual content. It's not that big of a deal for a simple personal blog, but take it into account if you have thousands of files.
shadcn/ui for UI Components
Replacing HeadlessUI with shadcn/ui was another strategic move. shadcn/ui is known for being customizable, accessible and lightweight. You get the entire file leveraging Tailwind and Radix so there's no need to start from scratch.
An Unobtrusive View Counter
For the view counter, my goal was efficiency without intrusion. It's a fun, light-hearted metric, not meant for tracking or boasting. Here's how I integrated it into the project:
I started by adding @upstash/redis
. Upstash has a free tier that's more than enough for this use case. I added a redis.ts
file to my lib
folder:
import { Redis } from "@upstash/redis";
const formatter = Intl.NumberFormat("en", { notation: "compact" });
const formatStats = (stats: Record<string, unknown> | null) => {
const numStats = {
views: stats?.views ? Number(stats.views) : 0,
};
return {
views:
formatter.format(numStats.views) +
(numStats.views === 1 ? " view" : " views"),
};
};
const redis = Redis.fromEnv();
export enum PostStat {
Views = "views",
Likes = "likes",
Claps = "claps",
}
export async function incrementPostStat(slug: string, stat: PostStat) {
return await redis.hincrby(`post:${slug}`, stat, 1);
}
export async function getPostStats(slug: string) {
const stats = await redis.hgetall(`post:${slug}`);
return formatStats(stats);
}
As you can see there's some space to add "likes" and "claps" in the future.
Open Sourcing The Journey
For the first time I'm open sourcing my personal website. It feels right. I've gathered a lot of inspiration from Lee's blog, the tech stack is also built and mantained by open source communities. So I hope this article helps you in some way or another.
Setting Up Your Version
Ready to build your own version? Here's how you can get started:
-
Environment Setup: Visit Upstash to set up a free account. Create a Redis database and note the connection URL and token. Add these to a
.env
file in your project's root. -
Configuring
config.ts
: Tailor theconfig.ts
file to reflect your personal or brand identity. Here's an example: -
Organizing Content: Store your blog posts in the
content
folder, using the file name as the post slug. This setup supports frontmatter and allows for extensive customization.
UPSTASH_REDIS_REST_URL="YOUR_URL"
UPSTASH_REDIS_REST_TOKEN="YOUR_TOKEN"
export const config = {
domain: "https://gvizo.so",
name: "Guido Vizoso",
description: "Product Engineer & Frontend Team Lead",
faviconEmoji: "🔷",
};
Future Enhancements
The journey doesn't end here. I plan to refine components like Tweet and GithubRepo for better error handling and explore other areas for improvement.
Feedback and contributions are always welcome. Let's make this a collaborative effort to push the boundaries of what our personal websites can be!
Remember, this is more than just a blog update; it's an invitation to explore, learn, and create. Happy coding!