Skip to content
<- Back

[WIP] Welcome to my first blog post! (and how it was written)

Hi, and welcome! I’ve always enjoyed coming up with technical experiments, discussing and sharing what I did with teammates and friends. I’ve done it a lot at work, it’s always enjoyable seeing the results make a difference and receiving feedback on what can be better. However, one of the suggestions I’ve received is that I should “own” such accomplishments more, and that stuck with me. It was an excellent suggestion, and until that point it had never occurred to me I should try to put my thoughts into a more permanent and digestible format so others (and even myself!) can refer to it… okay, that’s not entirely true. I’ve had in my mind, for a long time, the idea of building a website, but the idea seemed daunting: should I use WordPress? How would it work? Sigh, I need to buy a domain. I’m not good enough at web development. I’m an awful designer and CSS feels like black magic. I’m not a good writer either.

Background

When I was close to graduating from university all that was left was an internship. So I did a short stint at a local startup. I had no prior experience with web or mobile development, and their stack comprised of:

  1. A dashboard written in pure HTML, JavaScript and CSS
  2. Two multiplatform mobile apps written in React Native
  3. A few embedded system firmwares for the ESP8266, written in C++, using the MQTT protocol for uploading data into a Firebase database

I had no trouble picking up 3., as I had a reasonable amount of C++ and systems programming experience under my belt from writing CHIP-8 and NES emulators. I had heard of React Native before, or rather just that React was the hottness at the time, but learning the frameworks and modernising the mobile apps to modern React Native using the latest libraries and TypeScript was engaging and fun. When it was time to do maintenance on the dashboard I felt it was going to be incredibly daunting and challenging, because the website’s code had a ton of raw HTML injected by the JavaScript code, and those patterns went completely against what I expected coming from React Native.

I had a plan though. I was young and naive, so I did the most reasonable thing anyone in my shoes would also do: I learnt about bundling with Webpack, injecting small islands of React code (in the most dynamic and reactive parts), until I realised I was ready to rewrite the whole thing using React and the Vite framework. And so I did. I reused the mix of Bootstrap with custom CSS, sprinkled a little bit of Tailwind CSS, rewrote the whole code in TypeScript, and did my best to apply the best modern React and state managing patterns.

The results were staggering: no more 4-5 second delay when navigating between pages due to the whole JavaScript code having to start from the scratch, instant navigations, several bugs, both tracked and untracked, were fixed, and we were able to share a fair amount of code with the mobile apps.

So, that’s what I’ve known for years. Even after landing a new job I kept up with the web dev scene: I learnt about Next.js, static generation, server rendering, single/multi page applications (SPA/MPA), trade-offs, and a whole bunch of other stuff (i.e., I don’t remember). I could always write the code, but designing interfaces off the top of my head was the real final boss.

The Stack

First experiment

Fast forward a few years, I yolo-ed and bought this domain. I was dedicated to writing the website from the scratch. I knew Next.js and I knew it supported MDX. The stack was supposed to be a no-brainer:

  1. Framework: Next.js
  2. UI framework: Mantine
  3. Articles: MDX

What could go wrong? Well, first of all, I’m still no designer, so I was having a lot of trouble getting to a design I wanted, especially after looking into dozens of portfolios and blogs, and I was never happy with whatever I’d came up with. Second, making Next.js generate static pages in { output: "static" } mode was difficult if I wanted to maintain a good developer experience. DX is incredibly important to me because I enjoy building solid foundations to avoid having headaches or too much manual labour when iterating over it.

Unfortunately, I lost the code I had to glue the Markdown file discovery with Next.js’ generateStaticParams(). It originally used Node.js file system functions with globs with remark-frontmatter/remark-mdx-frontmatter/gray-matter (one of those, but each one of those during different times!) to parse and export the frontmatter (basically, the metadata of the Markdown file, including title, slug, description, publication date, etc…)

The page itself looked a bit like this:

type Props = PageProps<"/blog/[slug]">;

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const post = getBlogPost(slug);

  if (!post) {
    notFound();
  }

  return {
    title: post.title,
  };
}

export default async function Page({ params }: Props) {
  const { slug } = await params;
  const post = getBlogPost(slug);

  if (!post) {
    notFound();
  }

  const { Content } = post;

  return (
    <Content />
  );
}

export function generateStaticParams() {
  return getBlogPostsSlugs();
}

Not bad. But, behind getBlogPostsSlugs(), there was a lot of annoyances and boilerplace. Oh, and there was no styling yet. I couldn’t get Mantine to work well (100% my fault).

Second experiment

Clearly, I was unhappy with how things were working. Therefore, I did a lot of research and came up with this:

  1. Framework: Next.js
  2. UI “framework”: Tailwind CSS + Tailwind CSS Typography + shadcn
  3. Articles: MDX
  4. Content collection: Content Collections (content-collections), from content-collections

Content Collections was a rare find, because Contentlayer was traditionally recommended as the data layer, but it’s unmaintained so Velite came highly recommended, however, it had some issues with Turbopack and it didn’t feel ergonomic to use. Maybe it’s better now though. The content-collections.ts code looked like this:

import { createDefaultImport, defineCollection, defineConfig } from "@content-collections/core";
import type { MDXContent } from "mdx/types";
import type { StaticImageData } from "next/image";
import { z } from "zod";

const posts = defineCollection({
  name: "posts",
  directory: "src/content/blog",
  include: "**/*.{md,mdx}",
  parser: "frontmatter-only", // Let Next.js' MDX processor do all the content and plugin work
  schema: z.object({
    slug: z.string(),
    title: z.string(),
    description: z.string(),
    date: z.coerce.date(),
    image: z.string(),
    tags: z.array(z.string()).default([]),
    dev: z.boolean().default(false),
  }),
  transform: ({ _meta, ...post }) => {
    const Content = createDefaultImport<MDXContent>(`@/content/blog/${_meta.filePath}`);
    const image = createDefaultImport<StaticImageData>(`@/content/blog/${post.image}`);

    return {
      data: {
        ...post,
        image,
      },
      Content,
    };
  },
});

export default defineConfig({
  content: [posts],
});

Pretty cool, huh? It’d handle finding the files, hot-reload, and frontmatter parsing and validation. I removed a ton of Node.js code.

I chose Tailwind CSS over Mantine because it’s still the most popular tool for writing UIs, has an excellent documentation, provides the Typography plugin, and shadcn has become even bigger than Tailwind itself. This meant I would have an easier time with this stack since it’d be a lot easier to find examples and prior work.

Great! Everyone is happy, the sun is shining, all the birds are singing. This is looking solid (not to be mistaken with Solid). And then it was time to give the blog posts some flair and look good. Any blog post from a website that’s going to talk about code is going to need some syntax highlighting.

Then things started falling apart. There are lots of methods to support syntax hightlighting, both client and server side. I required it to be server side since there was no need to let the client do any work and it could be pre-rendered during build time. After a lot of research around Shiki, Prism, and the Remark/Rehype plugin system, I opted to use @shikijs/rehype, a Rehype plugin for Shiki. This worked great! But, again, any website like this one is legally and morally obligated to support dark theme. I used next-themes to handle the themes. However, the code blocks would never adjust to the correct theme. After some research and reading the docs (very important), I came up with this:

const rehypeShikiOptions: RehypeShikiOptions = {
  themes: {
    light: "github-light-default",
    dark: "github-dark-default",
  },
  defaultColor: "light-dark()", // This did the trick
};

But, of course, I also needed to support the usual plugins:

const withMDX = createMDX({
  extension: /\.mdx?$/, // md and mdx
  options: {
    format: "mdx", // Keep html tags in md files
    remarkPlugins: [
      // The two frontmatter plugins were still required
      "remark-frontmatter",
      "remark-mdx-frontmatter",
      "remark-gfm",
      "remark-smartypants",
    ],
    rehypePlugins: [
      "rehype-slug",
      "rehype-mdx-import-media",
      ["@shikijs/rehype", rehypeShikiOptions],
    ],
  },
});

This took me a lot of work to come up with. Sure, I want to be meticulous and make the choices count, but I wanted to use as many plugins as possible to cover most things. For example, remark-gfm (GitHub Flavored Markdown) gives you a more GitHub-like flavour, remark-smartypants adds support to smart quotes, rehype-slug adds id attributes to headings so it’s possible to use anchor tags to navigate or share a link to them, and rehype-mdx-import-media helps with media imports.

Phew. Can’t believe it took me this long to learn how much stuff you need to learn to use it. Maybe it would’ve been better to just use a meta-framework or tool like Zola. This came at a time I kept seeing the name Astro come up. It supported everything, the content collections layer, syntax highlighting, all the usual plugins pre-configured, and a method to collect headings to support navigations or table of contents. Some of them didn’t even have plugins remark/rehype plugins that worked with the current stack! That brings us to…

VR experiment (third experiment)

What comes after 2? Definitely not Half-Life 3, it’s actually Half-Life Alyx, the VR game. OK, enough.

At this point, you may be wondering:

Why don’t you just roll with these, it’s perfectly fine, that’s what most people use!

Yeah, I get it. Fortunately, experimenting with the goal of perfection in mind is part of my DNA. I’ll never achieve perfection, ever, but it’s for sure a goal. If I can get even 5% closer to it that’s already a huge improvement.

I had done a ton of research about Astro, peeked through people’s code bases, and found the pitch very interesting:

I was sold on it. What took me days of research to come up with several plugins, while still missing some, and not without some flakiness and issues, was all handled automagically my Astro.

Tags:

webdev

Astro

Next.js