About this site

This is Bojin Li's personal website.

History

Year

Stack

2019

GitHub Pages + Jekyll + Disqus, Huxpro theme

2020

Vercel + Jekyll + Disqus, Huxpro theme

2021

Vercel + React 17 + Tailwind CSS + Vite + TypeScript

2022

Vercel + Next.js 12 + Notion API + Giscus + p5 + three.js

2024

Vercel + Next.js 14/15 + Tailwind CSS + shadcn/ui

2026 (current version, Haku)

Cloudflare Workers + TanStack Start + Vite + Tailwind CSS + Durable Objects

About Haku

Haku takes its cues from Yohaku. It carries forward Yohaku's focus on reading, including the feel of whitespace, paper, and subtle animations, with the same polished rich-text article rendering. As for the name, I kept a piece of Yohaku, and Haku also happens to be the Japanese reading of "帛", a character in my name.

Yohaku is built on Next.js, while this site runs on Cloudflare Workers and needs as much optimization as possible for mainland China's particular network conditions. So I chose TanStack Start, TanStack Router, and Vite, and added Durable Objects. This lets me work closer to the underlying runtime when optimizing SSR on Workers, while avoiding Next.js's bloat and the uncertainty of running it on platforms other than Vercel.

On the backend, many of the features that come with Yohaku aren't needed here, while Haku has plenty of its own customizations and optimizations. So I wrote a new CMS with Yohaku as a reference, keeping its protocol as close as possible to mx-space, the backend used with Yohaku.

Overall architecture

The monorepo contains three applications: Web, Core, and Admin.

Application

Responsibilities

Technology

Deployment

Web

Public site, including the homepage, articles, and standalone pages

React, TanStack Start / Router, Vite, Tailwind CSS

Cloudflare Workers

Core

Backend services for content, media, identity, comments, and more

NestJS, Fastify, Drizzle, PostgreSQL, Redis

Self-hosted server, managed by Dokploy

Admin

Content management interface

React, Vite, CodeMirror, Haklex / Lexical

Self-hosted server, managed by Dokploy

Mermaid diagram

The three applications communicate through shared TypeScript and Zod contracts, which also validate data at request and response boundaries.

Server-side rendering

Haku uses server-side rendering. When you first open a page, the Worker fetches the data needed for that route and language, generates HTML with the content already in it, and streams it to the browser. This lets you see the content before the scripts have finished loading.

The browser then hydrates the page, taking over what the server has already generated so that interactions such as navigation, menus, and comments can work. After that, TanStack Router handles navigation within the site, fetching only the data needed for the next page without reloading the entire document each time.

Mermaid diagram

To keep server rendering and browser hydration in sync, each server request has its own Router, QueryClient, and Jotai store, and the browser has its own instances too. This keeps different readers' data separate. Entrance animations are also handled alongside routing, with separate behavior for initial page loads, normal navigation, and back/forward restoration. Prefetching or background refreshes won't make the page replay its animations for no reason.

Caching

Site settings, navigation, and the various summaries on the homepage are the same for every reader, so they can be shared. They're stored as public snapshots in Cloudflare Durable Objects. Concurrent reads of the same data are combined into a single read. Once a snapshot expires, the saved content is returned first while an update runs in the background (stale-while-revalidate), reducing the wait for readers. Publishing new content also marks old snapshots as stale. Cache updates include a version check, so even if an earlier request arrives late, it can't overwrite newer data with an old result.

Article bodies aren't part of this shared cache. They're read from published content each time, and both HTML and responses from server functions are kept private.

Activity and statistics shown on the homepage follow a similar approach. The backend aggregates them periodically, so opening the homepage doesn't require live requests to external services. Occasional slowdowns in those services won't delay the article content.

Loading speed optimizations

Haku uses the following measures to improve loading speed.

  • Site startup data is kept as small as possible, sending only navigation and information needed by the current page, without content from unrelated pages.
  • Route loaders work with the query cache to reduce duplicate reads during a single navigation.
  • Heavy dependencies for rich text, maps, and 3D are split by use case and loaded only when needed.
  • The fonts, languages, and rendering modules used in article bodies are prepared in advance to avoid mismatches during refreshes and hydration.
  • Background and 3D animations are scheduled to pause or fall back to a static version when the page is hidden or the reader has enabled reduced motion, so they take as few resources as possible away from reading.

SSR and the initial render have also been tested repeatedly with network throttling in Chrome DevTools. Sentry helps me track down errors and performance issues in production.

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...