Building my personal website
After years of procrastination, it's ready! A primer on how I did it and the tools I used.

Building my personal website has been on my to-do list ever since I became a software engineer. I've always been somewhat motivated to work on it, but other priorities always got in the way. However, I recently took a leave of absence from graduate school and now, for the first time in my adult life, I find myself neither working nor studying. So, I finally decided—no more excuses—it's time to build my website.
Having left my previous development job in the summer of 2023 and having taken a hiatus from programming to concentrate on graduate school, building this website was also a good opportunity for me to learn and re-educate myself on some of the recent frameworks and trends in web development, such as the "game-changing" new Gen AI tools I keep hearing about. This post will be a little bit technical for the average person, but I hope it will be an interesting read nonetheless and that it can serve as a useful guide for any engineers or vibe coders looking to build their own website using a web application framework rather than a website builder.
Choosing a framework and template
There are many different ways to build a website. Those without web development experience can opt for a website builder (like Wix or Squarespace) to interactively design and manage a site, or they can opt for a more basic static site generator (like Jekyll or Hugo) to create a site from templatized plain text. As a developer, I wanted to build a slightly more complex website using modern web tools. A quick Google search revealed a few different options: Next.js, Gatsby, Nuxt, and Astro. To be perfectly fair, all of these options could have worked for my website. They all support static site generation (SSG) and server-side rendering (SSR), but in the end, I chose Next.js over a few key reasons:
-
Next.js was the most popular, meaning it would likely have the most documentation and community support. While I am a fairly seasoned developer, I am still new to all of these technologies, and the level of maturity of the ecosystem is an important factor in my decision.
-
Next.js is backed by Vercel, a company that also offers a front-end-as-a-service solution, giving me an easy way to deploy my website.
-
I am already comfortable using React (albeit less comfortable with the "post-hooks" versions of React) to build frontends. Next.js integrates extremely well with React to build expressive applications, more so than Gatsby or Astro.
I started with a combination of two templates provided on the Vercel website. I cloned the initial repository from the Next.js Boilerplate. Then, I heavily borrowed code from the Portfolio Starter Kit because it contained examples of useful features that I would want to include in my website or development process, such as MDX support, SEO optimization, and Tailwind CSS.
Setting up the infrastructure
Even though developing the website itself is the fun and creative part of the work, actually launching the website involves some technical plumbing as a pre-requisite. The steps are relatively straightforward as documented here and on other sites, but I'll describe some personal and unique challenges I faced and decisions I made.
Registering a domain
Unfortunately, the domain of leqi.com
was already taken because my name is
painfully common (there was another Le Qi at my own university, also studying
electrical engineering, but a year above me…). Anyways, a WHOIS lookup of the
domain revealed that the domain owner was a Chinese IT solutions company (I
think?) that was not interested in transferring the domain. I don't have a
middle name either and couldn't use that as part of the domain, so I had to get
creative with some possible choices. After scouring the internet and
brainstorming with ChatGPT, I came up with the following options:
leqi.dev
leqi.me
therealleqi.com
iamleqi.com
In the end, I chose leqi.me
. I thought having my full name leqi
as the
second-level domain was more important than having a .com
top-level domain
(TLD). And ordinarily, I would have opted for a .dev
TLD, but I didn't want my
brand to just reflect my work as a developer (waiting patiently for my music
career to take off). The .me
TLD has already gained a bit of traction as one
used for personal sites—shoutout to Montenegro for making it
happen.
I purchased and registered the domain on Cloudflare, a company known for its reliable content delivery network (CDN), domain name system (DNS), and general network security services. To be honest, I didn't give the registrar decision much thought; I just made sure it didn't cost me an arm and a leg.
Small tip: if you or your kid(s) might want a personal website someday, buy the domain early—before someone else, like a publicly traded Asian corporation, does.
Hosting the website
As described earlier, I deployed the project on Vercel. The experience has thus far been excellent - the Git and CI/CD integration means that committed changes to code on the main branch will automatically deploy a new version of the website, issues with builds get caught before they are deployed, and rollbacks to previous versions can be easily triggered. While I have yet to appreciate the built-in analytics or the global CDN performance benefits, I'm sure they are just as excellent. Deploying on Vercel has been a far more convenient and user-friendly experience than deploying on AWS. It's perfect for personal projects, though it can get expensive for a site that serves considerable traffic.
Configuring DNS
I configured the domain nameservers on Cloudflare. For those unfamiliar with
DNS, think of it in its most basic form as the giant, abstract phone book of the
internet. To find out which servers host my actual leqi.me
website, I need to
add my records to the global phone book so that your browser and my browser can
do a phone book lookup and find out that my website is hosted on Vercel's
servers.
Configuring the DNS means adding two records as recommended by Vercel:
-
An A record that explicitly points
leqi.me
to the IP address for Vercel's deployments. -
A CNAME record that points
www.leqi.me
toleqi.me
so you can type both address variants into the address bar and arrive at my website.
Hosting the mail server
Though not technically part of the website, the ability to receive and send
emails from a personal @leqi.me
address enhances the personal brand. I knew
nothing about how mail servers worked, so I went pretty far down the rabbit
hole.
I already knew that I did not want to set up my own mail server. Doing so is a technically complicated endeavor filled with pitfalls. I researched a few options online, including Proton Mail, Zoho Mail, Migadu, Postale and a few others, but I eventually decided to go with MXroute primarily for three reasons:
-
MXroute has one of the most competitive pricing plans out there, and the only one I could find with a lifetime plan. My subscription fatigue is palpable, and for a modest fee, I had unfettered access to 10GB of combined storage and unlimited domains and email accounts for life.
-
MXroute is very bare-bones and requires some domain knowledge about DNS. Thankfully, the level of technical ability required is not too high to be frustrating or overwhelming but not too low to be useless for learning.
-
The reviews for MXroute have been compelling, with many users citing the high level of reliability, satisfactory spam filtering functionality, and absurdly low prices.
From the MXroute portal, I had to register the leqi.me
domain and create an
email account. I followed the directions found
here and
here—the steps were very
straightforward and self-explanatory.
Remember DNS? In addition to letting Chrome, Safari, or any browser know where to find my website, I also have to let Gmail, Outlook, or any mail client know that my mailbox is hosted on MXroute. To do so, I need to add a few more DNS records to the nameservers on Cloudflare:
-
Two MX records that point
@leqi.me
mail to the MXroute servers (two servers for redundancy). -
A DKIM record that helps receivers authenticate mail sent from
@leqi.me
. -
An SPF record that whitelists the MXroute machines as the only legitimate senders of mail from a
@leqi.me
domain. -
A DMARC record that reports possible spam mail that fails DKIM and SPF validation to Cloudflare.
-
Additional CNAME records that allow Gmail and Outlook clients to send and receive mail using addresses from the
@leqi.me
domain.
This was, somewhat surprisingly, the part of the project that I had to learn the most technically to accomplish. I had a lot of help from the instructions in the links here, here and here.
Building the blog
The template project already provides support for MDX, a markup language that is an amalgamation of Markdown and JSX. It combines the simplicity and readability of Markdown with the expressiveness and dynamism of JSX by allowing React components to be embedded within near-plain text.
In simpler terms, I can write most of this blog in the same way I would write a Google Docs or Notion document (with auto-formatted headers and hyperlinks), but I can still interject custom, funky components with their own unique styles and behavior.
Making aesthetic choices
Getting inspiration
For inspiration, I scoured the web and took a look at the websites of other developers. Many of those sites are compiled on this website. Weaving through over 100 websites gave me some intuition of what my website should eventually look like in terms of both content and aesthetics. I will say that most of the websites on that list were visually stunning, implemented with masterful design and craft leagues above my own. I mean, just look at this masterpiece or this work of art. Nevertheless, many small features, including the transitioning sun to moon icon, the translucent header nav, and the link transitions were inspired by elements that I found on some of these websites.
Choosing a color scheme
Coolors is a fantastic tool to generate a palette of colors. If you have a starting color in mind, you can "lock in" that color and then randomly generate combinations of colors that are color complements. I started out with a royal blue (shoutout to my alma mater, Duke University), generated a shade of orange (my favorite color) that I found appealing, and rounded out the set with a black and a white. See my palette here.
To generate the different hues and tints from the main palette colors, I used tints.dev. These other tints are used for hover states, highlights, foreground contrasts, etc. Coloring the website was then just an exercise in cycling the site through different color combinations and choosing shades that contrast and complement each other well. Contrast checkers were useful to ensure that the colors used are generally accessible to all. Once I was fully satisfied with the colors, I copied them from tints.dev into my Tailwind configuration file, and I was all set to use them in my website.
Implementing dark mode
For the most part, the default next-themes
package that comes with Next.js is
enough to handle dark mode and there are tons of blog posts out there explaining
how to implement this feature. I do want to note that there's a subtle issue
that arises when persisting the user's preference. The tl;dr is that by default,
whether the user prefers dark or light mode is stored in the browser (in local
storage), but the page loads before it knows your preferred theme. The timing
mismatch leads to unwanted
flicker as
the website defaults to the system preference to show a sun or a moon before
loading the user preference, and then course corrects after learning this
preference. This blog
post
details the problem and the steps to resolve it. But though I've implemented the
solution, if you try re-loading the website, you'll notice the absolute smallest
flicker (barely noticeable unless you're actively looking) at the sun/moon icon
in the navigation bar.
Most importantly, how cute is the sun to moon transition? I take absolutely no
credit for it - all the hard work goes to the contributors of the
react-toggle-dark-mode
package. I simply
re-used the component and added my own colors. By far the coolest component in
the whole website.
Creating a logo
I am by no means a graphic designer, so please don't judge me for my first attempt at logo design. Canva has some great options to create a basic, yet elegant logo in minutes. I used a basic template and a fancy-ish font to create a minimalistic (but hopefully aesthetically pleasing) design with my name. I created multiple versions of the logo with different background types (transparent and opaque) and color schemes (light and dark). Canva allows you to export the logo in SVG, a format that is infinitely scalable in size, a fact which is important for making favicons, as described below.
Implementing favicons
Favicons were another surprisingly complex feature to implement. I spent about a week obsessing and learning the ins and outs of these tiny icons in an eye-opening/depressing amount of detail. My guru was this blog post that exhaustively outlined the process of generating a set of icons that would work for all websites and devices. The process involved:
-
Generating images of my logo and implementing them in different sizes and formats (PNG, ICO, SVG, etc.). This is required to make the logo work for all browsers (Safari always does its own thing), all devices (iPhone and Android have different requirements), and for both light and dark modes. I used this Favicon generator to create different files, and I used GIMP (and a lot of ChatGPT to figure out how to use GIMP) to generate my logo in different resolutions.
-
Adding the files to the project and configuring the project to use the generated images for the appropriate devices.
Styling link transitions
I was inspired by some websites to implement a flashy transition animation when hovering over a link. There's this great article here that details some novel ways to add some style to link transitions. I ended up using a combination of the "Growing Background Link Hover Effect" and the "Sliding Highlight Link Hover Effect," though I had to adjust the CSS heavily to accommodate transitions and seamless change in both light and dark mode.
Conclusion
For years, making this website was something that hovered in the background as one of those things I should get around to doing, like cleaning the attic or learning how to cook something besides noodles. Taking a break from grad school gave me the time—and frankly, the mental space—to just sit down and do it. And I'm happy with how it turned out. It really only took about two full months of on-and-off development to implement. And along the way, I've learned more about frameworks like Next.js and Tailwind and about AI tools like Windsurf and Cursor. As I find new excuses to try out the latest dev tools, the website will continue to evolve.
This is my first blog post, but it won't be the last. While this post is fairly technical (and somewhat long), future posts and updates won't always be. I'm excited to have an official location to share my thoughts, memoirs, and random musings on a wide gamut of things, from tech, business, the outdoors, and more. After all, why deprive the general public of the Kafkaesque chaos that stirs within my mind? Stay tuned!