Hugh Do

2024 Website Refresh

January 25, 2024

React.js logo

A few years ago, I've built my first personal website using Gatsby.js. After finishing building it, I realized that I didn't have enough time to write nor did I have many ideas to write about. So, I decided to just leave it there to collect dust.

Now, since I've been learning a lot and gaining a ton of inspiration from other developers, I think it's time to refresh my website and start writing again. Addtionally, I want to have a place where I can share my photos I've taken with high quality. In this blog, I will talk about the tech stack that I've chosen to build my new website.

Aims

Here's what I wanted with my new website:

  1. Arrow
    Have a modern look.
  2. Arrow
    Minimal (but doesn't have to be too minimal): Use as few dependencies as possible. Easy to maintain, scale; easy to write and publish new content.
  3. Arrow
    Can display high quality photos.

Tech Stack

Here's the tech stack

Next.js

I've been using Next.js since forever so it's a no brainer for me to use it for the website rebuild. The only reason I used Gatsby.js for the first version of my website was because Next.js didn't have Static Site Generation (SSG) at that time. But that feature has been added to Next.js since version 9.3.

Especially, with the latest version of Next.js (v14), everything is React Server Components by default which can help reduce the bundle size significantly and improve the performance of the website.

OG Image and Sitemap generation

Next.js provides generateMetadata and generateSitemaps functions which can be used to generate website's metadata including OG Image and Sitemap dynamically.

I create a Route Handlers to dynamically generate the OG Image for each blog post page. The API basically just adds the provided title to a template image and returns the generated image.

Here's the code to generate the OG Image:

1import { ImageResponse } from 'next/og'
2import { NextRequest } from 'next/server'
3
4export const runtime = 'edge'
5
6export async function GET(req: NextRequest) {
7 const { searchParams } = req.nextUrl
8 const postTitle = searchParams.get('title')
9 const font = fetch(
10 new URL('../../../public/fonts/UncutSans-Regular.otf', import.meta.url)
11 ).then((res) => res.arrayBuffer())
12 const fontData = await font
13 const imgUrl =
14 process.env.NODE_ENV === 'development'
15 ? 'http://localhost:3000/og-bg.png'
16 : 'https://hughdo.dev/og-bg.png'
17
18 return new ImageResponse(
19 (
20 <div
21 style={{
22 height: '100%',
23 width: '100%',
24 display: 'flex',
25 flexDirection: 'column',
26 alignItems: 'flex-start',
27 justifyContent: 'center',
28 backgroundImage: `url(${imgUrl})`,
29 }}>
30 <div
31 style={{
32 marginLeft: 190,
33 marginRight: 190,
34 display: 'flex',
35 fontSize: 120,
36 fontFamily: 'Uncut Sans',
37 letterSpacing: '-0.05em',
38 fontStyle: 'normal',
39 color: 'white',
40 lineHeight: '160px',
41 whiteSpace: 'pre-wrap',
42 }}>
43 {postTitle}
44 </div>
45 </div>
46 ),
47 {
48 width: 1920,
49 height: 1080,
50 fonts: [
51 {
52 name: 'Uncut Sans',
53 data: fontData,
54 style: 'normal',
55 },
56 ],
57 }
58 )
59}
1import { ImageResponse } from 'next/og'
2import { NextRequest } from 'next/server'
3
4export const runtime = 'edge'
5
6export async function GET(req: NextRequest) {
7 const { searchParams } = req.nextUrl
8 const postTitle = searchParams.get('title')
9 const font = fetch(
10 new URL('../../../public/fonts/UncutSans-Regular.otf', import.meta.url)
11 ).then((res) => res.arrayBuffer())
12 const fontData = await font
13 const imgUrl =
14 process.env.NODE_ENV === 'development'
15 ? 'http://localhost:3000/og-bg.png'
16 : 'https://hughdo.dev/og-bg.png'
17
18 return new ImageResponse(
19 (
20 <div
21 style={{
22 height: '100%',
23 width: '100%',
24 display: 'flex',
25 flexDirection: 'column',
26 alignItems: 'flex-start',
27 justifyContent: 'center',
28 backgroundImage: `url(${imgUrl})`,
29 }}>
30 <div
31 style={{
32 marginLeft: 190,
33 marginRight: 190,
34 display: 'flex',
35 fontSize: 120,
36 fontFamily: 'Uncut Sans',
37 letterSpacing: '-0.05em',
38 fontStyle: 'normal',
39 color: 'white',
40 lineHeight: '160px',
41 whiteSpace: 'pre-wrap',
42 }}>
43 {postTitle}
44 </div>
45 </div>
46 ),
47 {
48 width: 1920,
49 height: 1080,
50 fonts: [
51 {
52 name: 'Uncut Sans',
53 data: fontData,
54 style: 'normal',
55 },
56 ],
57 }
58 )
59}

You can try it out by going to this URL: https://hughdo.dev/og?title=Hello%20World

Code hightlighting

I use Bright for code hightlighting. It's fast, lightweight and easy to use, customize. It supports React Server Components and all of the VSCode themes so you can use your favorite theme for code hightlighting.

It also supports Dark Mode out of the box. Here's how I render different themes for light and dark mode:

1import { FC, ReactNode } from "react";
2import { Code } from "bright";
3
4import MaterialThemePaleNight from "./MaterialThemePaleNight.json";
5import NightOwlLightNoItalics from "./NightOwlLightNoItalics.json";
6Code.theme = {
7 light: NightOwlLightNoItalics,
8 dark: MaterialThemePaleNight,
9 // render light or dark theme based on the data-theme attribute of the html tag
10 lightSelector: 'html[data-theme="light"]',
11};
12
13type CustomCodeProps = {
14 children: ReactNode;
15};
16
17const CustomCode: FC<CustomCodeProps> = (props) => {
18 return <Code lang="js" lineNumbers {...props} />;
19};
20
21export default CustomCode;
1import { FC, ReactNode } from "react";
2import { Code } from "bright";
3
4import MaterialThemePaleNight from "./MaterialThemePaleNight.json";
5import NightOwlLightNoItalics from "./NightOwlLightNoItalics.json";
6Code.theme = {
7 light: NightOwlLightNoItalics,
8 dark: MaterialThemePaleNight,
9 // render light or dark theme based on the data-theme attribute of the html tag
10 lightSelector: 'html[data-theme="light"]',
11};
12
13type CustomCodeProps = {
14 children: ReactNode;
15};
16
17const CustomCode: FC<CustomCodeProps> = (props) => {
18 return <Code lang="js" lineNumbers {...props} />;
19};
20
21export default CustomCode;

Tailwind CSS

Nowadays, there're a lot of ways available to style your website. You can use CSS-in-JS, CSS modules, CSS preprocessors, etc. I've used serveral CSS-in-JS libraries like Emotion or Styled Components before and I think they're great.

However, there're a few problems with CSS-in-JS libraries:

  1. Arrow
    Most of them are not compatible with the new Next.js App Router and are runtime CSS-in-JS libraries. This means they come with a cost of performance.
  2. Arrow
    The learning curve is quite steep. You have to learn a new syntax and a new way to style your components.
  3. Arrow
    You have to write a lot of CSS code to style your components.
  4. Arrow
    Naming your CSS classes is hard.

The latest CSS-in-JS library StyleX that powers all the Meta sites: Facebook, Instagram, WhatsApp and Threads solves the first problem. But still, it doesn't solve other problems.

With Tailwind CSS, you also need to spend some time to learn the ulitity classes and syntax. But the process is much easier and faster. Time for styling your components is reduced significantly. Tailwind uses a compilier to generate only classes that you use, other unused classes will be removed in the output CSS file. So, you don't have to worry about the bundle size.

To be honest, I didn't like Tailwind CSS at first because I think it's ugly. But after using it for a while, I've grown to love it. So if you have the same feeling as me, give it a try and you might like it.

Image Storage and CDN

I've taken a look at several image storage sevices like Cloudinary, Cloudflare Images and Imgix. The problem with Cloudinary is that It doesn't allow you to upload images larger than 10MB on free plan and most of my photos exceed that size. As for Cloudflare Images, It doesn't allow you to get the original image back, It will always return the processed images.

Imgix's free plan can serve 1000 unique images and offers 100GB of bandwidth per month which is more than enough for me. It also provides the option to get the original image so I decided to go with Imgix. However, with Imgix, you have to upload your images to your own storage like AWS S3 or Google Cloud Storage. I chose AWS S3 because It offers a free tier for 12 months and after that, It's still very cheap.

Radix Primitives

Radix Primitives is a React component library that provides you with the unstyled, accessible building blocks to build your own design system. I use this library for some of the components like Dialog, Button and Dropdown Menu. It's easy to use, accessible by default, and can be styled with Tailwind CSS.

One of the cool things about Radix Primitives is that you don't have install the whole library, just install the components that you need.

Database and Caching

I use Vercel PostgreSQL for the databse to store images' metadata like Url, description, camera-related information, etc.

For the database ORM, I use Drizzle ORM. It's a type-safe, fast ORM.

"Well, actually it's not that Drizzle is fast, Drizzle just doesn't slow you down."

I also use Vercel KV for caching the DB's query results. Because I don't frequently upload new images so there's no need to hit the database often.

Content Management

I use MDX to write my blog posts. MDX lets you use any React components in your markdown content. It means that you can write and use your own components like code playground to make your content more interactive and dynamic. For compiling MDX to HTML, I use Next MDX Remote.

Conclusion

There's still a lot of things that I'd to add to my website like:

  • Arrow
    Code Playground
  • Arrow
    Better image gallery
  • Arrow
    Better mobile nav menu
  • Arrow
    Fixing accessibility issues

But if I keep waiting for everything to be perfect, I'll never be able to publish any blog post. So I'll just publish this post and keep improving my website.

Finally, my website will be open source. Feel free to hack around and give me feedbacks.

Last updated
January 27, 2024