Skip to main content
hazuki
moss on the old courtyard wall · sunlight through the leaves, 4 pm · a forgotten greenhouse, still breathing · sage green, faded by summers · wooden window, one potted fern · expired film, overgrown garden · ivy on the greenhouse glass · an old photograph, green at the edges · latest note: rebuilding my site with astro 7 · fresh roll on /tuesday: forgotten places · hanoi, 21°c, drizzle on the windowpane · 06:23 · new york, 13°c, grey clouds over the rooftops · 19:23 ❧
v0.1.0

Rebuilding my site with Astro 7

Why I dropped my old blog, picked Astro 7 served statically from Cloudflare Workers, and what I learned on the way.

My old site got heavier every year: one more UI library, one more tracker, until opening it meant waiting a few seconds before any text appeared. This time I wanted the opposite: less stuff, slower pace, and almost no JavaScript in the browser.

Why Astro

Astro generates static HTML and only ships JavaScript when I actually ask for it. For a site that is mostly text and photos, that is enough. Content lives in src/content and is validated by Zod at build time, so a missing alt line fails the build instead of slipping out.

The short list of what I kept:

  • Markdown and MDX for posts, with footnotes and callouts.
  • Two languages, Vietnamese as the default with no URL prefix.
  • Self-hosted fonts, tested hard against letters such as ằ, ẳ, ỡ, ự.
  • Search with Pagefind, loaded only on the /search page.

Picking a language at the edge

Visitors to / are redirected based on their browser language, decided in the Worker rather than in the page. The core is one small function:

type Lang = "vi" | "en";

export function resolveLang(cookie: string | null, acceptLanguage: string | null): Lang {
  const fromCookie = cookie?.match(/(?:^|;\s*)lang=(vi|en)/)?.[1];
  if (fromCookie) return fromCookie as Lang;
  return acceptLanguage?.toLowerCase().startsWith("en") ? "en" : "vi";
}

NOTE

The cookie always wins over Accept-Language, so anyone who picked a language once will not have it changed for them.

The static files part

Cloudflare Workers serves the dist folder as static assets. Only the root path goes through the Worker; everything else comes straight from the CDN:

{
  "assets": {
    "directory": "./dist",
    "binding": "ASSETS",
    // only "/" runs the Worker first, the rest is static
    "run_worker_first": ["/"],
    "not_found_handling": "404-page"
  }
}

WARNING

If you set run_worker_first to true, every static file request costs a Worker invocation. List only the paths you really need.

What I almost broke

At first I planned to add an animation layer to every page. After a few days I saw it served nobody, and it dragged the performance score down.1

CAUTION

Do not upload original photos with EXIF to a public site. The attached GPS data can reveal exactly where you took them.

Wrapping up

This site is not finished, and probably never will be. But it is fast, light, and I understand every line of it.

Footnotes

  1. All animation now runs only when the operating system reduced-motion setting is off. ↩