My debut story that also goes over how I built this portfolio.
If you've been on the internet lately, you must have noticed that it's getting a lot harder to find content or opinions that sounds or feels human. Maybe it's just me, maybe I'm trapped in an echo chamber, maybe I make the mistake of going on Twitter (Currently X) too often. Whatever it is, I can say one thing for sure: the software world feels a lot different than it did just half a decade ago. Everyone and everything seem to care only about moving as fast as possible. So, I've decided to silence the noise here in my little corner of the internet, build the things I find fascinating and share my experiences. This is the first of many software stories to come, and I hope you'd stick around.

I thought about what would be a good topic to write about for my debut and I settled on discussing how I went about developing the web application you're reading this post on.
Not too long ago, I rebuilt my portfolio and based it on the Portfolio template from MagicUI. While it served me well enough for a bit, I always felt like that UI felt a bit too bland. I understand the beauty in simplicity but the vibe I got whenever I looked at my portfolio was that it felt lazy. It was always at the back of my mind to improve it but I never got around to it until I stumbled on the beautiful portfolio from Nelson Lai.
It ticked every single box for what I had envisioned my portfolio to be, simple yet visually pleasing. It didn't have overly-bizarre scrolling animations and the elements didn't jump at you from random locations. I quickly went to work. Jumping into the source code, I initially felt overwhelmed, not because I didn't understand it but because it had a lot I didn't need or could leverage something else for. So I cloned the repo locally, opened a new migration branch and decided to start from scratch.
My opinion towards web design has shifted significantly over the last couple years. Roughly a decade ago, I tried picking up React and despised every bit of it but then again, the "React" I learnt and the one I use now are very different. I had to learn class-based React using JavaScript… horrible times. Nowadays, no one even bothers comparing JavaScript to TypeScript. Then React Hooks also came and the developer experience went up again. Then came Next.js which I still have a love-hate relationship with. I didn't really enjoy having to relearn the framework almost every year but now it's a lot stabler — at least I hope it is — and doesn't release a lot of breaking changes anymore.
I know "better alternatives" exist (Tanstack Start, Svelte, Vue) but at this point, I'm too unmotivated and uninterested to spend my time learning more frontend frameworks that I probably won't use professionally. So, regrettably I reach for the same poison tech stack when building any serious professional and recreational web project. And the main reason, I'll probably never pick a new one is simple: Shadcn. I love it so much. I'd even go as far as saying it solved Web Dev for me. If Shadcn doesn't have it, a registry probably does. You have to build a lot less stuff from scratch which is a huge productivity boost for me.
So in the end, I stuck with Ol' Reliable:
My last iteration leveraged Nextra and while I tend to prefer opinionated languages and frameworks, I also like having the flexibility to customize. With Nextra, it felt like I was fighting the framework. Simple things I would expect to work out of the box or easily changed would turn out to be infeasible. I found Fumadocs to be a lot more enjoyable to work with and I was able to bring over the Nextra Callout widget I liked over. In the end I don't miss Nextra and would rely on Fumadocs moving forward for my projects.
Whenever I want to design a system, I like to list out the problems I'd like to solve:
Getting a nice looking UI was solved right out the door by basing my new portfolio on Nelson Lai's template and complimenting the missing pieces with components from Shadcn and MagicUI or building whatever was remaining myself. It was a huge time saver for me and you'd be able to somewhat replicate the entire landing page with this knowledge alone. For example, this is the source code for the <ProjectCard /> and <Stories /> components:
type ProjectProps = {
project: Project;
};
export function ProjectCard({ project }: ProjectProps) {
let { title, icon, brand, urls, tags, description } = project;
const [hovered, setHovered] = useState(false);
const isMobile = useIsMobile();
const { site } = urls;
brand = brand ?? "#A6A6A6";
icon = icon ?? "/placeholder.png";
if (isMobile && !hovered) {
setHovered(true);
}
return (
<Link
href={`/projects/${title.toLowerCase()}`}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<MagicCard
gradientTo={brand}
gradientFrom={brand}
gradientColor={"transparent"}
className="relative overflow-hidden rounded-xl px-3.5 h-full"
>
<div className="flex items-center gap-1.5 py-2.5">
<div className="flex items-center gap-1.5">
<span
className={`size-2.5 rounded-full ${hovered ? "bg-[#ff5f57]" : "bg-neutral-700"} transition-colors duration-300`}
/>
<span
className={`size-2.5 rounded-full ${hovered ? "bg-[#febc2e]" : "bg-neutral-700"} transition-colors duration-300`}
/>
<span
className={`size-2.5 rounded-full ${hovered ? "bg-[#28c840]" : "bg-neutral-700"} transition-colors duration-300`}
/>
</div>
{site && (
<span
className={`ml-auto text-[10px] uppercase tracking-widest select-none ${hovered ? `text-accent-foreground` : "text-neutral-700"}`}
>
{new URL(site).hostname}
</span>
)}
</div>
<div className="relative z-2 py-4 space-y-4">
{/* Title */}
<div className="flex items-center gap-3.5">
<BlurImage
width={48}
height={48}
src={icon}
alt={title}
lazy={true}
className="h-12 w-12 rounded-full"
/>
<div className="flex-1 min-w-0">
<h3 className="text-xl font-mono tracking-tight leading-none m-0 text-accent-foreground">
{title}
</h3>
</div>
</div>
{/* Description */}
<div className="flex flex-col h-full justify-between">
<div className="relative z-2 space-y-4">
<p
className={`text-xs leading-loose text-justify ${hovered ? "text-accent-foreground" : "text-neutral-500"} font-mono`}
>
{description}
</p>
</div>
<div className="flex flex-wrap gap-2 border-neutral-900 pt-4">
{tags.map((tag: string) => (
<Badge
key={tag}
variant="outline"
className={
hovered ? "text-accent-foreground" : "text-neutral-500"
}
>
{tag}
</Badge>
))}
</div>
</div>
</div>
</MagicCard>
</Link>
);
}The <BlurImage /> component comes from Nelson Lai and the MagicCard /> comes from MagicUI. I also added the search component although it's just a simple abstraction over Shadcn's <InputGroup />. Everything else either comes from Next.js, Shadcn or React. I couldn't possibly hope to include the entire source code in this post. I'm just trying to steer you in the right direction if you're interested in getting something similar to mine.
This was the one thing I couldn't get working out of Nextra. I wanted to have different layouts with different content sources show up on different routes for the application. I just couldn't get it working nor could I find an example on how to — I honestly still have no clue if Nextra even supports it from their docs. It's pretty funny that a documentation framework doesn't have great documentation. Anyways I was able to get what I wanted with Fumadocs in a single afternoon.
I first had to specify content source in the source.config.ts file and set up what route I'd load the content into in my lib/source.ts file:
export const docs = defineDocs({ dir: "content/docs" });
export const stories = defineDocs({
dir: "content/stories",
docs: {
schema: pageSchema.extend({
description: z.string(),
date: z.string().or(z.date()),
tags: z.array(z.string()),
banner: z.string(),
published: z.boolean(),
}),
},
meta: {
schema: metaSchema.extend({
title: z.string().optional(),
description: z.string().optional(),
}),
},
});This not only gives me complete flexibility over what layout I can use for each route but also gives me typed frontmatter data validated by Zod while it remained untyped in Nextra.
Anyways, the existence of the /stories route is easy to deduce but you might be asking yourself already why I'd also need a /docs route. The answer is that I've thought about how I'd write documentation for my personal projects and I didn't like having to rebuild a site for each project and so I decided to turn my portfolio into a CMS for them.
This looks good on paper but trying to get it working as expected wasn't straightforward.
Since projects that have documentation would have a docs directory that has valid MDX and meta.json files, my initial idea was to query this data from GitHub at request time, build the page tree and render the page on the fly. This would ensure that the rendered docs never go stale but then not only is this not very performant but I would probably also run into rate limiting from GitHub and so I had to look for other means.
As the first approach wasn't the best, my next idea was to instead sparse clone only docs and media directories of each project. That way I can statically build them giving me great performance but also introducing the potential for stale docs: updated docs source code doesn't immediately reflect on docs site. I considered creating a workflow in each project that sends a request to my Vercel deploy URL to trigger a redeployment if any git changes is detected but it feel a bit too clever for me for something simple. I decided to just manually perform the sparse clone locally and commit the changes by hand. Gemini was able to give me a bash script that does exactly what I wanted:
#!/bin/zsh
set -e
REPOS=(
"username/repo"
)
mkdir -p vendor
mkdir -p content/docs
mkdir -p public/projects
for REPO in "${REPOS[@]}"; do
PROJECT_NAME=$(basename "$REPO")
VENDOR_DIR="vendor/$PROJECT_NAME"
DOCS_TARGET_DIR="content/docs/$PROJECT_NAME"
MEDIA_TARGET_DIR="public/projects/$PROJECT_NAME"
echo "========================================"
echo "Processing $PROJECT_NAME..."
echo "========================================"
if [ ! -d "$VENDOR_DIR" ]; then
echo "Initializing sparse clone for $REPO..."
git clone --depth 1 --filter=blob:none --sparse --no-recurse-submodules "https://github.com/$REPO.git" "$VENDOR_DIR"
cd "$VENDOR_DIR"
git config submodule.recurse false
git sparse-checkout set docs media
cd - > /dev/null
else
echo "Directory exists. Pulling latest sparse updates..."
cd "$VENDOR_DIR" && git pull --no-recurse-submodules && cd - > /dev/null
fi
# Handle Docs
if [ -d "$VENDOR_DIR/docs" ]; then
echo "Copying docs contents into $DOCS_TARGET_DIR..."
rm -rf "$DOCS_TARGET_DIR"
mkdir -p "$DOCS_TARGET_DIR"
cp -R "$VENDOR_DIR/docs/." "$DOCS_TARGET_DIR/"
echo "Successfully synced $PROJECT_NAME documentation!"
else
echo "Warning: 'docs' folder not found in remote repository."
fi
# Handle Media
if [ -d "$VENDOR_DIR/media" ]; then
echo "Copying media contents into $MEDIA_TARGET_DIR..."
rm -rf "$MEDIA_TARGET_DIR"
mkdir -p "$MEDIA_TARGET_DIR"
cp -R "$VENDOR_DIR/media/." "$MEDIA_TARGET_DIR/"
echo "Successfully synced $PROJECT_NAME media!"
else
echo "Warning: 'media' folder not found in remote repository."
fi
done
This one was pretty easy, I just based it off of the Changelog Template from MagicUI.
This article isn't comprehensive and never intending to be. I simply wanted to walk you through a bit of the architectural choices that went into this web application. As I also don't have plan on making the source code public — at least not for the meantime — I wanted to steer you in the right direction. With just a little bit of time and effort, you'd be able to get something similar if you wanted. Thank you for reading!