Screenshot of the landing page of this site.
Published Nov 11, 2022
857

Writing A Blog

This is the third iteration of my personal website. Version one was a bare-bones single page app with my profile picture surrounded by a bunch of useless links to services I rarely use, and the second version consisted of almost the same thing, just stretched out a bit with parallax effects. I don't consider myself to be the greatest of writers, but nonetheless I have decided to start a blog, and to at least attempt to display some (hopefully) meaningful content here once and for all.

Oftentimes, I stumble across a technical post detailing a fix to a problem I was having, and I get whatever I was dealing with working properly. Grateful, I decide to take a look at other posts by the same author, and usually I end up finding the majority of everything else rather informative. So, why not have myself be a contributor to the process? The way I see it, the more information out there on the internet for free, especially in the realm of computer science, the better. My goal with this blog is not only to be informative and potentially save people time, but for you to also get a sense of my thought process when it comes to software development, and not just the solutions I come to but the journey as a whole.

Inspiration

I have been thinking about rewriting this site for the third time for a while now, and I owe a lot of credit to quite a few people and their existing blogs, namely Dov Alperin, as I found myself one day on his personal website. Specifically this blog post, where he explained the process of rewriting his blog from Next.js to Remix. Impressed with both the performance of the site and his explanation of the architecture behind it, I decided it was time for me to get started.

The first thing I thought of was that I needed to decide on an overall aesthetic. Whenever I'm starting a new project that involves UI somehow, I always have in mind some new concept I want to try out. In this case, it was glassmorphism, and seeing how the backdrop-filter API was somewhat recently stabilized in Firefox, I decided I would give it a go. Incorporating glassmorphism everywhere isn't possible obviously, and so the overall look of this site is mostly dependent on the colors. I've been using TailwindCSS for a while now, and I figured their default color-xxx values would work fine.

You can't help but be inspired in the design of other blogs you read, and that's why I see this site as a visual combination of the following:

  • Josh W Comeau's Blog - I have taken a lot of cues on colors from Josh here, especially with the yellow text on the dark backgrounds, and the use of indigo. I also really liked the idea of arrows for bullet points, and you'll see a lot of right-facing arrows on my site.
  • PlanetScale's Blog - Their blog has some very nice details, for example the accented border to the left of the title section. The full code snippets on my site are entirely based on what PlanetScale has done as well.
  • GitBook - I always liked the way GitBook structures data, even though it's for documentation. Both the anchor navigation on desktop and the 'next' and 'previous' post buttons are based on GitBook.

Technology

This site is written in TypeScript and React. A fundamental value of mine is if you are working with the web, you should never ignore fundamentals like HTML, CSS, and JavaScript. There is something unsettling to me about using technologies like Vaadin and building components in Java. Sure those things work, but I believe that if you're specializing in full web applications, the further and further you abstract yourself from JavaScript the worse off you'll be. I've found that React is enough of an abstraction yet still close enough to vanilla JS where I can be the most productive. TL;DR you ask me for a website? TypeScript.

Choosing Remix

In JavaScript land, the frameworks have their own metaframeworks, and in the case of React, Next.js has dominated that arena for quite some time now since its initial launch. When Remix first came along, I was pretty intrigued but never got the opportunity to try it out immediately, and so that's what this site is.

So far, my experience with Remix has been pretty great. This site is not all that complex, as of writing this the codebase is ~4000 lines, so I'm sure it wouldn't perform all that much worse in production had it been written with Next.js. What is completely different however is the developer experience. Remix claims that build times do not increase linearly with your data, unlike with Next.js, and I can confirm that similarly complex projects I've built with Next.js take longer.

I'm also fond of Remix's multitude of routing options when it comes to a file-based routing system. I like how the directories work with the layouts, and how if I don't want a nested layout to apply, I simply rename a file to the convention of nested.route.tsx and place it in the root. Pretty useful stuff!

Cloud Provider

Updated 11/19/22

I've migrated from Vercel over to Fly.io for hosting this site. Vercel was fine, but having everything rely on AWS lambda execution cold starts was a deal-breaker for me. Visiting the home page could take up to 6 seconds on Vercel's network, having to connect to both redis and sanity. I needed something a little bit more fixed, and so I've moved the site to fly.io. Vercel still remains my go-to when it comes to most frontend work, as their ease-of-use when it comes to deployments and previews is phenomenal.

Sanity and Open-Source

Sanity is the CMS that I am using, and I first heard about it digging through Dov's blog. I've never used a CMS before, but I knew that I wanted one, as the idea of using something like MDX and have my site's content be part of the source tree didn't sit right with me. This site is open source, and I don't like the possibility of anyone being able to see exactly the revisions I am making as commits. There's also the fact that I want to have drafts not be visible to the public, often for long periods of time. I need to be able update the site's infrastructure separately while keeping the drafts unknown to the public.

Josh actually wrote a post about this, arguing that same thing except that his blog is actually closed source because of this reason, since he relies on MDX for custom components and code within individual posts, and can't have his drafts be public. Completely understandable. He also mentioned that making the site open source would mean that people would see his sloppy code and he would feel obliged to maintain it. In my case, I don't need custom-defined behavior and have a much simpler site—I'm fine with having the source be accessible to anyone, but my content needs to remain private.

Technical Details

Your path on this domain right now should be /blog/writing-a-blog, and when you make a request Remix will route you to the __index.blog.$.tsx route. From here, the server will run the loader function, which will first check Redis to see if the post exists in the cache, and returns that data if it does. If not, the server will get the unauthenticated Sanity client and first run a query to find the corresponding post for the slug, return 404 if it doesn't exist, and then run another query to get the 'next' and 'previous' posts. From there, we also fetch from the database to get a total aggregation of all of the hits on the post, and store that data in Redis as well.

Slotted Hit Counters

I've looked at how other sites handle hit counters, and I'm not a big fan of creating a new row in a table for each new hit. PlanetScale wrote an article about this problem, and so I'm using slotted counters to keep track of hits. That means that there are at most 100 rows created for a single post, and lockups are going to be much more rare, a neat compromise between efficiency and data size.

Then, if there isn't an existing session for the user, one is created and the hit counter query is run to register a hit on the post. That query looks something like this, although it's handled by the ORM, Prisma:

MySQL
INSERT INTO SlottedPostCounter(slug, slot, count)
VALUES ('my-blog-post', RAND() * 100, 1)
ON DUPLICATE KEY UPDATE count = count + 1;

Client and Anchors

Now all of the data is ready to be sent over to the client. The bulk of the content is handled by the @portabletext/react package I'm using, and I've simply replaced and customized a few of the components to my liking. Most notably, the code snippets and anchors. When the content first loads on the browser, the h2 and h3 blocks are immediately parsed out with this code:

TSX
React.useEffect(() => {
  setLinks(
    ((post.body as any[]) || [])
      .filter(
        (x) => x._type == "block" && (x.style == "h2" || x.style == "h3")
      )
      .map((x) => ({
        name: x.children[0].text,
        slug: slugify(x.children[0].text).toLowerCase(),
        indent: x.style == "h3",
      }))
  );
}, [post]);

From here, it is easy enough to generate a map of all of the anchors we'll need for the post, and get the y positions of every single h2 and h3 element on the page. An observer is then created that listens for scroll events and updates the right-side navigation accordingly, highlighting the active header the user has scrolled to.

Landing Page

The animating blog on the landing page was built using Spline. It's super easy to use actually, all I'm doing on the client is just lazy-loading the component and pointing it to a URL:

TSX
const Spline = React.lazy(() => import("@splinetool/react-spline"));

export default function Index() {
  return (
    // ...
    <Suspense fallback="">
      <Spline
        scene="https://prod.spline.design/taXw95jMHuEO12Wp/scene.splinecode"
        className="absolute"
      />
    </Suspense>  
  )
}

In terms of how the blob was made within Spline, I just followed this YouTube tutorial. I used the tailwind colors I liked and I believe I added an additional group as the parent of the blob so that I can animate the group to a scale-in animation when it first plays. The performance of it is alright, on desktop it's virtually seamless except for on Firefox which I believe tends to be stricter in the resources it allocates, causing more lag. My slightly lower-end phone drops a few frames when the blurred cards are scrolled over the blob, but that's about it.

Issues I've Had

One of the biggest problems I've dealt with were the unsightly layout shifts and flashes that would occur whenever a user refreshed the page. You might have noticed that the primary image at the top of this page initially renders as a blurred, low quality image preview. This is intentional, but something that occurred when building this site was that test blog posts with arbitrary primary images would cause a layout shift a second after the user loaded the page. The dimensions of the preview were slightly off from the actual image. My speculation is that it's just the natural limitations of base64 encoding resorting to values of a certain proportion, and this was fixed after I started using images of simpler dimensions like 2:1.

Another thing with the primary image is that since I have links at the end to other posts within a post, the LazyImage component doesn't fully unmount when transitioning pages. With the old implementation I had, this would mean that for a split second of seeing the content of a new post after clicking off from another one, you would see the old primary image. It's these tiny little annoyances and flashes that affect the overall smoothness of the site that seem to bother me the most. Since it takes time to fetch the full image, and the preview is base64 and loads instantly, the solution was to 'unrender' and use the low quality preview as soon as there was any page transition.

Conclusion

I plan on posting something random here every once in a while, or whenever I work on some project where I get the feeling that I need to document my progress publicly. This first blog post is more of the latter, and honestly I'm pretty happy with the way things have turned out so far. If there's anything more you want to see, this site is open source and everything is available on GitHub.

Previous Post

The Pitfalls of NVIDIA on Linux