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.
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:
- Showcase my personal projects and work experience.
- Have a blog, where I can give visitors a look into my thoughts on programming + tech.
- 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:
- Vue, for the front-end
- Express or Flask (later on FastAPI), for the back-end and API development
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) |
---|---|
![]() | ![]() |
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.