This is my blog, 宙と木 (Sora to Ki). Every time you change pages, an amber dot flies from this page to the next and lands on that page's own line. In the early hours of October 25, on day 572 of its life, it took on this look.

The site started as a portfolio I pieced together from scratch, and after that I added a page whenever I felt like it. About a year in, I started to feel it had a whiff of AI about it: a lot of it looked like something any template could produce. So in May and June this year I borrowed a few ideas from innei's blog, and later switched to a starfield background drawn with WebGPU (that post is here). My posts, murmurs, the anime I've watched, and the photos I've taken all live here, so I always wanted it to look like "mine," not some theme with a few tweaks.

To be honest, half a year ago I was fairly happy with this blog. But I had always revised it one page at a time and never thought about what the whole site looked like together. In April, when I reworked the Journey page, I removed the card backgrounds and replaced them with huge years floating in the background. It did look better, but something always felt missing. Later I realized what was missing was immersion: every card felt like a separate slide, with nothing tying them together.

I'm someone who zones out a lot, staring at the sky. I've also always felt there's much more inside me than this site could hold, I just didn't know how to express it. Then one day in October I put the music, bookshelf, and setup pages side by side and saw they looked exactly the same: a big title, a subtitle, a row of cards below. In that moment it felt like these pages were only there for show. If every page looks like this, what is there for readers to explore? I decided to tear it all down at once.

"And now, I'm going to move the Earth."

Orb: On the Movements of the Earth

That's a line from a work I love. In the story, people across several generations risk their lives to pass heliocentrism down. What I did is of course far smaller, but at the moment I made up my mind to turn the whole site over, that line is what came to mind.

In the end it took four days. I started thinking about direction on October 22 and launched in the early hours of the 25th. The merged PR touched 449 files, adding 41,744 lines and deleting 53,184. All fourteen pages were redrawn, each one turned into a celestial body or something actually used in space. This post is about those four days, and the 572 days before them. The technical details of the transitions and fonts are in the appendix at the end, so if you want to build something yourself, feel free to skip ahead.

The first version failed#

At first I didn't make any design mockups. I figured changing things directly on my machine would show the effect better, since a prototype feels very different from the real thing against the background. The first version kept the realistic textured Saturn, made planets explode into stardust when changing pages, had the Notes cards grow down to the bottom of the page to meet a horizon, and gave the Murmurs page a pixel-art style.

When I looked at the result, I was really unhappy with it. Saturn blew out when it moved, and the particles from the explosion didn't blend with the stars already in the background. It looked like Thanos's snap, or like an eraser rubbing things out. The star trails on Notes were so dense at first they made me dizzy, nearly covering the content. However I adjusted the layout, it had the feel of a blog from a previous era, a lot like the old Yahoo! Kimo.

I spent an afternoon on it, and every time I fixed one spot, another looked even more out of place. The problem was that I hadn't set a base tone first. Every page had its own effects, and the materials, colors, and motion were all talking past each other. Honestly that version wasn't ugly, it just wasn't what I wanted. So I didn't delete it. I archived it in the archive/sky-morph branch on GitHub and went back to think about direction from the start.

"Being incorrect doesn't mean being meaningless."

Orb: On the Movements of the Earth

This line comes up again and again in Orb. The first version was wrong, but if I hadn't gotten it wrong first, I wouldn't have known what I really wanted.

Only dots, lines, and a touch of amber#

When I build apps I already lean toward the feel of serif type, and the 宙 in the site name 宙と木 already means the universe (木 comes from my handle, 木村盆栽, Kimura Bonsai). So the direction came together quickly: a starry sky can be drawn with nothing but dots and lines. Stars are dots, and orbits, star trails, and comet tails are lines. Using lines to tie the pages together would fill in exactly the immersion I felt was missing.

The hardest thing to let go of was the Saturn on the home page. I had kept tuning that textured, modeled Saturn until April this year, going through several rounds on performance, image quality, and overall feel. Throwing it away entirely really hurt. In the end I kept it, but redrew it with dots and lines: white brush strokes painted onto an opaque sphere, with the rings made of lines too. It looked much better afterward, and I realized I didn't have to insist on realistic materials so much. Still, letting go is letting go, so I kept the old one on the old site as a keepsake.

The rules I settled on were few:

ItemDecision
ColorBlack #0A0A0C as paper, warm white #F1ECE3 as ink, amber #E9C17A only as the signal of "one dot"
MaterialNo cards, shadows, or frosted glass, and no dividers unless I can't avoid them
Chinese typeSource Han Serif (Noto Serif TC)
Latin typeOnly for large text set entirely in English, in Instrument Serif, never bold

I don't know much about color, but I was sure of one thing: with too many colors, the picture loses its subject. So the dot is the only amber thing on the whole site. This time I also learned my lesson and thought the direction through before writing code. I made every page as a clickable mockup in the browser first and revised it version by version, 15 versions in total. Only after every page was settled did I start touching the site.

Every page is a celestial body#

Once the direction was set, I asked the same question of every page: what would this be in space? I wrote every page's subtitle myself. I wanted the whole site to read with a bit of a literary feel, but without padding.

PageSubtitle
HomeSomeone who writes code and occasionally builds things.
NotesAs the stars wheel on, every flicker of curiosity leaves an arc.
MurmursCatching a stray noise that cuts through the silence.
WorkIn the silent deep, carving out marks one inch at a time.
CollectionWherever gravity reaches, fragments of light that once lit my way.
PhotosFaint signals that crossed the void, developed here line by line.
SetupTaking precision apart, giving obsession a place to rest.
FriendsDeep space is long and lonely, but look back and there is always starlight.
MessagesThe long night is silent, waiting for a pulse from far away.
LogBreaking free of gravity, I become the open sky.
AboutHold on.
This siteNothing else here: only the night, white ink, and a faint light.

Home: nothing on it#

The home page is that Saturn, and the five sections, Notes, Murmurs, Work, Collection, and About, are five moons orbiting it. I deliberately put no information on the front page. Normally there are only lines and dots. When your mouse gets close to a moon, its orbit lights up and shows that section's name and its latest entry. When nothing is happening, one of them occasionally lights up on its own, to remind you they can be clicked. I want people who come in to explore on their own, the way I explore myself in this universe.

I insisted on keeping Saturn in 3D because later I want to turn the home page into a 3D space you can roam around in.

Murmurs: a comet that flashes past#

To me, murmurs are moments that flash through my head, posted on a whim like on Threads, so I made them a comet. It flies along a big S-shaped arc. The newest murmur is the nucleus, older ones line up on either side along the tail, and very old ones are just a dot on the tail that only appears when you hover over it.

When you open a single murmur, the camera zooms into that section of the tail, like pulling in with a telescope. That section lights up, the rest dims, and the comments below are the debris shed from that stretch of tail. What I wanted was "you're looking at one part of the comet," not jumping to another page.

Notes: long-exposure star trails#

Notes is my favorite page. Each post is a star trail: the newer the post, the farther out it circles, and the longer the post, the longer its arc. The faint arcs in the background are star trails too, so the whole night sky looks like a long-exposure photo. When the page opens it's as if the shutter just opened: all the stars drag out their trails together, then the whole sky slowly turns around the celestial pole. Star trails are really traces left by the Earth's rotation: the stars aren't turning, it's the planet we're standing on that turns.

The part I'm happiest with is the moment you open a post: the star trail of the post you clicked gets pulled straight and drawn close, becoming the orbit on the right side of the post page.

That orbit is also the table of contents. Chapters are tick marks on the orbit, spaced according to each chapter's actual length. The part you've read is a thin solid line, the part you haven't is dotted, and the amber dot is you, reading. The Notes list is ring after ring of star trails, and opening a post is the camera pulling in on one of them.

Setup: blueprints I'm still not happy with#

The Setup page is the one I'm still not quite happy with, even after all the revisions. The first version was a zero-gravity assembly with spacecraft subsystem labels and live telemetry. It came out flat, and it didn't really have much to do with space. I always wanted to make it an engineering concept drawing, but drawing it all by hand line by line would take too long. In the end I used Gemini to generate exploded axonometric drawings, black lines on white, as the base, and then the code overlays numbered circles, leader lines, dimension lines, and a parts list. Hover over a number and that part on the drawing turns amber.

The first drawing, SHEET 0, is the computer back at my parents' home, bought around 2012. There's no photo of it, so the case is a dotted sketch drawn from memory. The dots wobble slightly, and the specs type out line by line like a power-on self-test. It's my childhood, so it comes first.

I'm still not happy with this version, and I'll probably change it again.

A few more pages#

The other pages each have a celestial body too:

  • Work is a spiral route growing outward from the center. Its starting point is my graduation project VoltiCar, a project that gamified carbon credits. The topic wasn't great and the team didn't work together smoothly, and looking back, it's kid stuff technically. But it was my first project where I used everything I knew at the time. I worked on it for over a year, often forgetting to eat and sleep. So it's the start of the route. Each project is a small line-drawn planet, with its size, terrain, rings, and moons grown from GitHub data.
  • Photos follows how space photos get sent back to Earth: the probe takes the picture and turns it into 0s and 1s, the big dishes of the Deep Space Network receive it, it develops line by line in black and white, and only at the end is it colored.
  • Friends has this site at its center, with friends as stars orbiting it. Click one to dock with them: two ships connect, and their latest posts unfold beside them.
  • Messages: each message is a received signal. As you type, your words turn into a waveform in real time, and when you send it, it flies into the antenna.
  • Log is a full-page launch scene. The altitude is my cumulative GitHub contributions. This page shows up again at the end of the post.
  • This site is a sextant pointed at the stars. Every toy in it uses real things from the site, taken apart step by step to explain how I thought about them. I hope someone plays with it and thinks, "oh, so you can do it this way too."

About: touchdown#

The About page is a free fall. The dot slowly falls from straight above and comes to rest as the period in "Hold on.", and as readers scroll down, they fall along with it.

The inspiration was a video I saw in the news: late at night in Portugal, a blue fireball streaked across the sky and lit up the whole night for several seconds. Astronomical events have always stunned me. A star falling to Earth is like a life: it gives off its most brilliant light and is gone in an instant. So this page skims past those naive, lost yesterdays and stops at "touchdown." But landing is where something new begins. Further down is underground, digging layer by layer into the things I've done, and at the deepest point is my favorite line.

"I think being moved matters more than how long you live."

Orb: On the Movements of the Earth

Collection: a signal redone four times#

The Collection page (Watching, Bookshelf, Listening) is the one I revised the most, because what I want to show here is taste: to let people see that I've watched a lot, and also what I love. The first version put the three sections side by side, which was too repetitive. The second switched between them with a stardust reshuffle, which felt too contrived. The third was three nebulae, with the camera flying between them as you scrolled. It still didn't feel as vast as space, and the round covers got mostly cut off.

After three wrong versions, I stopped thinking about "how to arrange the covers" and asked instead: in space, how would these things actually be used? The answer was signals. Each work is a ring of signal sent out from Earth the moment I finished it, and how far it is from Earth shows how long ago that was. Scrolling down, you pull back from Earth, past the Moon, the Webb telescope, and the planets at their real positions, Voyager 1, the edge of the heliosphere, and the Oort cloud, until you end up among the neighboring stars about 9 light-years away.

On the way out you pass Voyager 1. In 1990, from about 6 billion kilometers away, it turned around and took a picture of Earth, and in that photo Earth is less than a pixel. Looking at that picture, Carl Sagan wrote:

"Look again at that dot. That's here. That's home. That's us."

Carl Sagan, Pale Blue Dot

I want the Collection page to feel like that: the farther you pull back, the smaller Earth gets, and all the works that once lit my way are signals sent out from that little dot.

One dot, linking every page#

If I could keep only one thing from the whole redesign, I'd pick the transitions. I've always felt that how smooth transitions are and how well they connect decide whether a site has quality. Readers may not be able to say what's good about it, but a stutter, a flash, or a jump in position, they always feel.

Caring about "smooth" didn't start this time. In July, when I reworked how post pages load, I got stuck on the handoff between the skeleton screen and the content. In October, when I tuned the starfield's performance, my goal was for visitors to think, "how can it be 3D and still this smooth?" That's why the demo from the first day of the redesign was especially hard for me to accept: the page-change flight was dry and monotonous, entering and leaving went in the same direction, and the version I made the next day just used fades to force the two pages to feel connected.

Then I gave myself a rule: every page change has to extend out from a single dot. On the home page it's a moon on its orbit, on Murmurs it's the comet's nucleus, and every star trail on Notes has a star too. As long as every page has its own dot, a page change can become one dot flying from this page to the next.

The first version looked forced#

The first version was bare-bones: the dot flew along a fixed path, and when it arrived, the new page faded in. In practice the dot didn't line up with the lines on the page: the amber dot wasn't aligned with the comet's nucleus, and the line drawn by the transition was a little off from the comet tail.

After a few rounds of changes, the "entering Work" sequence turned out the smoothest, truly one unbroken motion.

I used it as the standard and reworked the other pages to match. Breaking down why it felt smooth, it came to three things:

The old page gathers itself into that dot

On the home page, Saturn folds into the moon that was clicked. On Murmurs, the comet tail shrinks back into the nucleus. Everything on the page gathers toward that point, and the page only changes after it's done.

The camera tracks like a film

This is what I care about most. The camera first moves onto the amber dot and centers it completely. Once the dot sets off, the camera follows, keeping the dot right in the middle of the frame. Near the end, the camera stops while the dot is still moving, which is the only way to catch the trail behind it. It's like a film that zooms in and tracks a subject, and only when the camera stops do you see the path it traveled.

The last stretch follows the page's own line

The dot finally lands along a line the next page already has (a star trail, a comet tail, a route). The tail behind the dot fades over time, so when it lands, the only lines left on screen are that page's own.

How each page catches it#

Once those three things were settled, every page just had to answer two questions: which line does the dot follow to land, and how does the page gather back into a dot when you leave? This is the part I find the most fun: the same dot means something different on every page.

PageHow the dot landsWhen leaving
HomeFlies to the center of Saturn, and Saturn opens out from that pointThe whole system folds into the moon that was clicked
NotesCuts into a star trail, travels a long way around the celestial pole, and stops on that post's starWhen you open a post, its star trail is pulled into the post's table of contents
MurmursLands along the comet tail to become the nucleusThe tail shrinks back into the nucleus
WorkLands at the starting point and draws the whole route as a pen tipThe route erases from the starting point and folds into the moon of the newest project
CollectionDives from deep space back to EarthThe camera pulls back to Earth, and the dot launches from Taiwan with a ring of wavefront
PhotosThe dot is the signal, falling from the probe into the antennaThe whole page folds back into 0s and 1s, and the dot launches from the probe
MessagesThe starting point pulses once, and a beam of amber light runs along the channel into the antennaThe light runs backward from the antenna to the starting point
FriendsLands at the center, and orbits grow outward one by oneThe orbits fold back into the center
LogLands on the launch pad, and the rocket lifts offThe rocket flies off along its body, and the dot follows the nose
AboutSlowly falls from straight above into the periodLeaves carrying its falling speed
This siteLands at the front of the telescope tube, and the line of sight pulls to the first starFlies off from the star being measured

Visitors don't always come in through the home page, so the opening uses the same system: on your first visit there's a night sky with a circle drawn in the middle. When the page finishes loading, the circle fills, and the dot in the middle flies from the center of the screen to the page.

Once I'd made up my mind, I didn't want to stop for those four days. Right now my favorites are Photos, Collection, and Notes, especially the handoff from a Notes star trail into the post page. There's still a little stutter, and the feel of changing pages rapidly isn't where I want it yet, but they're the closest so far to what I mean by "smooth."

What isn't done yet#

So far it might sound like everything went smoothly, but there are a few parts I know very well aren't done yet.

Phones and the iPhone Duo#

The whole design was thought out on wide screens, and on phones it's merely "viewable." The 3D Saturn on the home page shrinks way down on a phone, and the moons' names only appear on hover, which phones don't have. The spiral on the Work page turns vertical on a phone, and the project names crowd together. The iPhone Duo, released this September, is even trickier: unfolded, its inner screen is 7.6 inches with a nearly square ratio, right between a phone and a desktop, and the current layout doesn't fit either way.

I don't have an answer for this yet. On a lot of pages the visuals themselves are the content, so it's not something you can solve by stacking cards in one column. I'll probably have to rethink each page: "on a small screen, how should this celestial body be seen?"

Readability#

After launch, someone on X commented that the readability was a bit poor: the text was hard to read, and it wasn't clear where to click. Honestly, I'd thought the same when I first designed it. The navigation bar is normally collapsed and only expands on hover. When you land on the home page there's just Saturn and a few lines, with no button telling you "click here."

That's a deliberate choice. I want people who come in to feel, first of all, the impact of a full-screen view and the sense of finding their own way through the universe, but those two things are hard to balance with "easy to read, easy to find." If I made the text bold and gave the post content more color, as colorful as the old MDX blocks, it would definitely read more clearly, but the style of the whole site would start to clash. I'm still adjusting bit by bit, but I haven't found the line for where it should end up overall.

The day I was seen#

The day after launch, I saw a wave of traffic from X in the admin panel. When I clicked through, I found that innei had posted a tweet introducing the site, and had also left a comment under one of my murmurs. I've always loved his blog: its Japanese paper style is beautiful, and the interactions between elements in his posts and his layout engine are really detailed. I also admire his spirit as a developer. I'd assumed he probably wouldn't look at a small site like mine, so in that moment it felt unreal. I was happy and flattered. But right after, I started thinking: is there something I still need to change, something not good enough? And that made me want to hide it from people a little again.

I had learned from what he did, and now he's seen what I made. It feels a bit like the passing-on in Orb: what one person leaves behind is picked up by the next and carried even farther.

"If I have seen further it is by standing on the shoulders of Giants."

Isaac Newton, letter to Robert Hooke, 1675

Another site owner found me after seeing my link exchange request in innei's group. He said the site looked beautiful and mentioned that the Messages page couldn't send. I fixed that bug the same day. I've always felt this site is a treasure hidden a little too deep: lots of details, but not necessarily seen by anyone. Since this redesign, people have started finding their way here from elsewhere, clicking around, and even coming back to tell me what's hard to read.

Day 572#

The day the redesign launched was this site's 572nd day. The Log page draws exactly this stretch of time: going into the distance is time, and the altitude is cumulative GitHub contributions, now at 5,356.

Looking back at that line, it started as just a Hero page plus a portfolio. The blog only came in August 2025, my own domain in October, and in January this year the whole site moved to my own HomeLab. After that I tore it down and rebuilt it almost every month: in April I added multiple languages and did my first big redesign, in June I moved the whole site from JavaScript to TypeScript and migrated the frontend to TanStack Start, in July I swapped the Express backend for Rust and made the WebGPU space background the default, and then came this. WebGPU, a Rust backend, a self-hosted server, translations into five languages: a lot of these I touched for the first time because of this site, and mostly learned by stepping into pitfalls. Most of those stories became posts in Notes.

I didn't delete the old site. It's frozen at archive.koimsurai.com. It's no longer what I want, but a big part of why the new site runs this smoothly and why these four days went so fast is the architecture it built up over more than a year.

The hardest part of these 572 days wasn't actually writing code. It was coming up with topics for posts. I've never thought about giving up on this site, but I often wonder: why am I doing all this? It always seems like I'm the only one doing it. So this time, having people find their way here from elsewhere and click around means more to me than anything.

This redesign is more like the first time I've taken everything I learned over these 572 days and organized it into a complete form with my own aesthetic. It's not perfect yet. The mobile version, readability, and performance all still need a lot of work, but this line will keep climbing.

Appendix: for anyone who wants to build their own#

Below are the implementation details of the transitions, fonts, and the old site. They have nothing to do with the story above, so read on only if you're interested.

Gather, fly, grow#

The transition is split into three phases: gather, fly, grow. The page change is held back first and only allowed through once the old page has finished gathering. During the flight, the new page is already mounted and slides in with the camera from the side the dot is heading toward. Only after the dot lands does the new page start growing its own things.

Click a link / go backhistory blocker holds this navigationleave()The point it gathers into, initial velocity, a Promise for when it's doneLet it through only once gatheredMount the new page (hidden outside the camera at first)anchor(), arrive() give the anchor and the last stretch of pathFocus, follow, brake, the dot lands along approachThe dot lands, the page starts growingReaderRouterTransition layerOld pageNew pageReaderRouterTransition layerOld pageNew page

The transition layer (TransitionLayer) handles holding page changes, drawing the dot and its tail, and moving the camera. Each page only needs one hook to report four things: where the anchor is, which path the dot takes at the end, how to gather, and what to do on landing. This is the About page's version, where the dot falls straight down into the period of "Hold on.":

tsx
usePageTransition({
  // The dot flies in and comes to rest as the period of the title
  anchor: () => stopPoint(titleStopRef.current),
  // Last stretch: fall straight down from above the period, the camera follows down, the page rises from below
  arrive: () => {
    const p = stopPoint(titleStopRef.current);
    if (!p) return null;
    const drop: Pt[] = [];
    for (let d = FALL_IN_PX; d >= 0; d -= 6) drop.push({ x: p.x, y: p.y - d });
    return { approach: drop, join: Math.round((FALL_IN_PX - FALL_SHOWN_PX) / 6), pace: FALL_PACE };
  },
  // Gather: the text fades out, and the amber dot leaves carrying its falling speed
  leave: () => {
    const point = dotRef.current;
    if (!point) return null;
    pageRef.current?.classList.add('is-leaving');
    return { point, velocity: { x: 0, y: 0.8 }, done: after(300) };
  },
});

approach is the last stretch of path the dot takes, and join marks the point where the camera brakes. The part before that can be off screen. A pace below 1 slows it down. The About page is a free fall, so I let it fall slower than the other pages. If a page doesn't provide leave, the transition layer uses a circular mask to gather the whole page into the anchor, so a new page doesn't have to have its transition finished from day one. When going from Notes into a post, that star trail is handed to the transition layer, which keeps drawing it and only fades it once the post page has grown in, so the line between the two pages never breaks. Page changes are held with TanStack Router's router.history.block. Back and forward (popstate) also go through the same blocker, so changing pages with the mouse side buttons gets the transition too.

Edge cases#

Once the flight itself was smooth, most of my time went into edge cases. The most obvious was changing pages rapidly: the half-drawn line and the hovering dot would be left behind on the next page.

  • Rapid page changes: if the page changes again mid-transition, the old run finishes immediately and this one doesn't play. A page's "gather" waits at most 0.8 seconds, and any transition running over 8 seconds is treated as broken and shut down.
  • The background starfield snapped back to its original position on every page change, making it look like the whole sky shifted. Now the camera's offset accumulates instead of resetting to zero.
  • Changing pages with the mouse side buttons fell back to the default flight. That's because when a page decided "where to gather," it looked at the clicked element, and with side buttons nothing is clicked. Now it decides based on the destination ctx.to.
  • Going from one post to another doesn't play a transition. The post page swaps in place: the left column stays, and only the title, body, and table of contents change.
  • Every glow and bright line has to fade out. Going dark instantly looks like a power cut, so across the site everything fades slowly back to the page's original brightness.
  • Opening: on a slow connection it waits at most 3.5 seconds before flying, so no one gets stuck on a loading screen. Readers who only switch languages, go in or out of the admin panel, or have "reduce motion" turned on in their system settings won't see transitions.

Load only the glyphs each page needs#

The variable-weight version of Traditional Chinese Source Han Serif is 16.8 MB on its own, there's one each for Simplified Chinese, Japanese, and Korean, and the post font switcher adds a few more: a Hei sans-serif, LXGW WenKai, and MiSans. There's no way to send whole files to readers. My approach is to subset per page: the browser uses MutationObserver to collect which characters appear on screen and requests /fonts/<style>/<weight>/<key>.woff2?c=<chars>&f=<locale>, and the server uses subset-font (HarfBuzz's wasm under the hood) to cut a woff2 containing only those characters. The URL is determined by the character set, so it can be cached forever. Testing on the body of my previous post, FurSpark, the full text uses 836 distinct characters, and the 400-weight woff2 cut from the 16.8 MB original is 185 KB.

The core of the server-side subsetting
ts
import subsetFont from 'subset-font';
// subset-font only finds HarfBuzz's wasm at runtime with require.resolve, which bundlers can't trace,
// so import it explicitly once and the whole harfbuzzjs package (wasm included) gets into the output
import { Blob as HbBlob, Face as HbFace } from 'harfbuzzjs';

// Each subset copies the whole original file into wasm, about 0.7 s for a long post: one at a time, queued
let queue: Promise<unknown> = Promise.resolve();
const serial = <T>(job: () => Promise<T>): Promise<T> => {
  const run = queue.then(job, job);
  queue = run.catch(() => undefined);
  return run;
};

async function build(file: string, weight: 400 | 600, cps: number[]) {
  const font = await sourceOf(file); // Only the few most recently used originals stay in memory
  return subsetFont(font, String.fromCodePoint(...cps), {
    targetFormat: 'woff2',
    variationAxes: { wght: weight }, // Variable-weight original: extract the weight we need
  });
}

Two pitfalls are worth noting. The first is bundling: subset-font only looks up HarfBuzz's .wasm at runtime with require.resolve, and Nitro's tracing tool can't tell it's a dependency, so I only found out after deploying that the file was missing. The fix was to mark it as an external dependency (traceDeps) and import harfbuzzjs explicitly once. The second is missing glyphs: Simplified characters occasionally appear on Traditional Chinese pages (usually in comments). Now a request can carry an extra x=sc, and the server first checks which characters the Traditional Chinese original is missing, then cuts only those from the Simplified Chinese file.

How I kept the old site#

Before launch, I froze the old version as the git tag v1-final and a Docker image, and ran it in a separate container at archive.koimsurai.com, sharing the same backend as the new site.

The risk of sharing a backend is that the old site's forms could still write data, so I locked it to read-only at the nginx layer: sign-in returns empty, the API only allows reads, the admin panel redirects to the main site, and search engines aren't allowed to index it.

The old site's nginx config
nginx
server {
    server_name archive.koimsurai.com;
    add_header X-Robots-Tag "noindex, nofollow" always;

    # Keep search engines off the old site so it doesn't compete with the new one for the same posts
    location = /robots.txt {
        default_type text/plain;
        return 200 "User-agent: *\nDisallow: /\n";
    }
    # Admin redirects to the main site
    location /admin { return 302 https://koimsurai.com/admin; }
    # Sign-in returns empty
    location ^~ /api/auth/ { return 204; }
    # API only allows reads
    location /api/ {
        if ($request_method !~ ^(GET|HEAD)$) { return 204; }
        proxy_pass http://127.0.0.1:<backend-port>;
    }
    location / {
        proxy_pass http://127.0.0.1:<archive-port>;
    }
}

Performance and after launch#

The rule for performance fixes was that the visuals must not change. Opening a post from Notes used to hitch for a moment because the whole body mounted in the same frame. Now it mounts in batches when you navigate in from within the site. The hitch when reloading the Collection page came from the background starfield's worker building the WebGPU device, so now it waits until the entrance animation finishes and the main thread is idle before building it.

50–67ms
The frame when opening a post from Notes
was 183–217ms
0ms
The hitch when reloading Collection
was 67–85ms

Still unsolved is the outermost part of the Collection page: when you scroll all the way out, the dashed signal rings have to be re-split into forty to sixty thousand segments every frame, dropping from 65 fps to around 45 fps. The network diagram and telemetry on the Setup page also follow one rule: keep the structure, remove sensitive model numbers. Telemetry only shows temperature, load, memory, and power draw, with numbers rounded and cached for 30 seconds. In the two days after launch I opened twenty-some more PRs, filling in what got missed in the migration: the context menu, search panel, lightbox, newsletter, and admin panel. Every page's share image was also redrawn on the backend with Rust's tiny-skia and output as APNG.


As I write this, the site keeps growing. The redesign isn't the end, just another node on that line. To close, here's my favorite line once again:

Stay hungry, stay foolish.

參考連結