favour.work, The Third Edition

Published on: Friday, August 22, 2025

Why start all over?

Initially this site’s primary role was to serve as a digital business card, where anyone could access anything work related. But over time, after seeing more personalized portfolio sites (e.g. Sneaky Crow and Kyaw), I felt it was time to make my corner of the internet mine.

favour.work V2 Version 2 of favour.work, nothing too crazy. It just tells you who I am and links to my resume, LinkedIn and Github.

My main goals for Version 3 of my website were to:

  1. Showcase my personal projects and work experience.
  2. Have a blog, where I can give visitors a look into my thoughts on programming + tech.
  3. Include a section (nothing too big) I can share non-work related things I’ve been interested in. This was heavily inspired by the Perfectly Imperfect Newsletter.

With these goals in mind I began developing my new website. However, instead of using the same tech stack I’ve used in the past, I decided to switch to Astro.

Vue Astro

Web development has been something I’ve been interested in since I’ve been in highschool, making websites on Wix. As I got better at programming in college, I began making full-stack web applications with the same tech stack:

I used this stack so much, I began calling it the FAVstack (FastAPI + Vue / Flask Axios Vue). It was easy to reach for it whenever I needed to make any web solution, and my personal website was no exception. Although the stack worked perfectly fine for what I needed, I was still keeping up with the latest tools in web development and thought it was time to move on from Vue.

Astro got on my radar a couple of years ago, when Fireship released a breakdown of Astro’s 1.0 release. I wanted to use it for V2, but chose not to because I wouldn’t use any of the features that made it worthwhile. Since I wanted to completely overhaul favour.work to favor (haha get it) more content-heavy work, I felt it was a good time to use Astro.

So, What’s New w/ V3?

My favorite Astro feature was it’s Content Collections API, because it made it easy to set up my blog. Every .mdx blog file has this set up:

---
title: 
date: 
tags: 
slug: 
---
[Write content here.]

All of the blog files live in my ./content/blog file, which all the data stored within the three dashes (---) and the content are stored in a collection. I defined the blog collection in a content.config.ts file like this:

import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

const blog = defineCollection({
    loader: glob({ pattern: '**/*.(md|mdx)', base: './src/content/blog'}) ,
    schema: z.object({
        title: z.string(),
        date: z.string().date(),
        tags: z.array(z.string()),
        description: z.string(),
        slug: z.string()
    })
});

export const collections = { blog };

With this definition, I am able to use this data across Astro components. Here’s how I render all the blog posts:

---
import type { GetStaticPaths } from "astro";
import { getCollection, getEntry, render } from "astro:content";
import BlogLayout from "../../layouts/BlogLayout.astro"

//First, I use GetStaticPaths() and getCollection to generate the blog url paths 

export const getStaticPaths = (async () => {
    const blogs = await getCollection('blog');
    return blogs.map(blog => ({
//This maps the slug value in each .mdx file to the url parameter in this format: favour.work/blog/{slug}        
        params: { slug: blog.slug }
    }));
}) satisfies GetStaticPaths;

//Then, I use getEntry() to retrieve the data and content, if the slug matches the slug value in the .mdx file
const entry = await getEntry('blog', Astro.params.slug)
if (!entry) {

  throw new Error('Could not find blog post');
}

//Finally, I use render() to render the .mdx file into the <Content /> component
const { Content } = await render(entry);
---
<BlogLayout>
    <article class="[I used Tailwind Typography for styling the .mdx]">
        <h1>{entry.data.title}</h1>
        <p class="italic">Published on: {postDate.toLocaleDateString('en-US', dateOptions)}</p>
        <Content />
    </article>
</BlogLayout>

I used defineCollection() primarily for my blog. I initially wanted to use it for all content, but for some reason, Astro was not reading my JSON files. So I had to jerry rig it. I stored both the recommendations (“Stuff I’ve been into lately” section on the home page) and my projects in JSON files in my /content folder.

In order to parse the JSON files in the same manner as defineCollection(), I created Zod schemas for both JSONs.

import { z } from 'astro:content'

// ./interfaces/recommendations.ts
export const recSchema = z.array(z.object({
	img_url: z.string(),
	title: z.string(),
	thoughts: z.string(),
	show: z.boolean(),
	current_ep: z.number().optional(),
	total_eps: z.number().optional()
}))

// ./interfaces/projects.ts
export const projectSchema = z.array(z.object({
    name: z.string(),
    link: z.string(),
    description: z.string(),
    tools: z.array(z.string()),
    complete: z.boolean()
}))

I was then able to use those schemas in my components like this (I’m only going to show the recommendations component, but it’s the same idea for the projects component):

---
import {recSchema} from '../interfaces/recommendations'
import recommendations_data from '../content/recommendations/data.json'


const fun_stuff = await recSchema.parseAsync(recommendations_data)
---

<section class="[not important]">
    <h1 class="[not important]">Stuff I've been into lately...</h1>
    <section class="[not important]">
        <!-- Recommendation Cards -->
        {fun_stuff.map((item) => (
            <section class="[not important]">
                <img src={ item.img_url } class="[not important]">
                <aside class="[not important]">
                    <section>
                        <h1 class="[not important]">{item.title}</h1>
                        <p class="[not important]">{item.thoughts}</p>
                    </section>
                    {item.current_ep! &&
                    <section>
                        <div class="[not important]" style={`width: ${(item.current_ep / item.total_eps) * 100}%`}></div>
                        <h2 class="[not important]">Progress ({item.current_ep}/{item.total_eps})</h2>
                    </section>
                    }
                </aside>
            </section>
        ))}
    </section>
</section>

Lastly, a previously overlooked feature I added was a dark mode. For the light mode I decided to use the color scheme of V1 of the website, and kept the V2’s color scheme for the dark mode.

Light Mode (Mobile)Dark Mode (Mobile)
V3 Light ModeV3 Light Mode

Side note, I had an issue with FOUC (Flash of Unstyled Content) but I just had to turn my script tags into inline components. Spent way more time on this than I needed to.

Conclusion

All in all, I’m satisfied with how V3 turned out and I think Astro will be my default for web development moving forward. I wanted to add more features, like a Letterboxd card in the “Stuff I’ve Been Into Lately” section on the home page that shows the title and rating of the last film I watched, but I chose not to because it would clutter the page.

I think V3 is here to stay for a while, but who knows, I’m constantly learning.