#meta
25 posts
Putting a Three.js island inside a static site
The isle page downloads only its own script. The scene is computed at runtime, Three.js stays in that page's chunk, and the first HTML response carries an empty stage.
Volume without a modelling tool: triangulating an island by hand
The rock on this island is not exported from a modelling tool. It is five irregular rings stitched together at runtime, and vertex colours plus flat shading are enough to make it read as stone.
Twelve hundred instances: squeezing a garden into a few draw calls
Six hundred blades of grass, a hundred and twenty flowers, eighty-five mossy stones, forty-two shrubs. Density comes from InstancedMesh, not from cutting the count.
Lighting one scene: three lamps and a shadow map computed once
A hemisphere light for ambience, a key light for direction and shadow, a rim light to pull the island off the background — and seven point lights that only add glow.
Water from two shaders: a pool, a stream and a waterfall
There is no water simulation on the island and no reflections. Two ShaderMaterials cover all of it, one lying flat and one falling, sharing a single time and night uniform.
The loading budget for 3D: split the chunk, load on visibility, know when to stop
A 3D page pays three costs — download, first-frame compilation, per-frame drawing. Each needs its own budget, and any one of them can turn good-looking into janky.
Graceful shutdown: the exit sequence decides between 502s and clean responses
Exiting immediately on a termination signal cuts in-flight requests. The order is stop accepting, drain what is running, release resources, then exit.
HTTP caching: Cache-Control and ETag each own half
Cache-Control decides whether a request happens; ETag decides whether the response body can be skipped. Mixing them is why cache bugs persist: changing one never fixes the other half.
iframe sandbox: it narrows capability, it does not harden
The sandbox attribute starts by removing everything and allow-* adds pieces back. Getting the direction backwards is common: scripts stop running because that is the default.
Semantic versioning: a major bump promises no silent behavior change
MAJOR does not mean big. It means breaking. Using it for refactors and hiding breaks in minor releases turns your downstream automatic upgrades into outages.
What technical debt interest actually looks like
Debt is not ugly code; it is the extra cost every future change pays. The test is measurable: files touched per edit, test runtime, places that must change together.
URL encoding: spaces, plus signs and double encoding
A plus is a space in the query string and a literal plus in the path; the two positions encode the same character differently; encoding twice yields a plausible value that never matches.
Consistent hashing: why adding a machine does not reshuffle every key
Modulo sharding moves almost all data when the machine count changes. Put the hash space on a ring and let each key follow it clockwise to the first node, and only one arc moves. Virtual nodes are the price.
Cache headers for a static site: hashed files forever, HTML never
A cache policy depends on whether the filename changes with the content, not on file size. Long-caching HTML hands users an old entry point that points at deleted chunks.
When WebAssembly earns its place: less often than the pitch suggests
WASM pays off with heavy computation, bulk data and few boundary crossings. Rewriting an O(n) JavaScript loop as WASM usually buys slower loading and a harder stack to debug.
Manual i18n: what hand-rolling three languages costs
I hand-rolled this site's i18n instead of using Astro's built-in config. Here is why, and exactly where the cost shows up — two 404 incidents and a blog that was half translated.
Full-text search on a static site: index at build time, no backend at runtime
A static site does not need Algolia to have search. Generate the index during the build, run the query entirely in the browser, and pay a few seconds of build time.
1059 assertions, no test framework: why pure logic must import nothing
My test suite is `node scripts/selftest.ts` — no framework, no dependencies. The price is that every module it touches must have zero relative imports, and that price bought a codebase shape.
One source for head: SiteHead, JSON-LD escaping and hreflang
All three HTML shells render one component that emits every head tag, with the data assembled on the server. Plus a detail worth remembering: angle brackets in JSON-LD must be escaped.
Shipping a static site is not "upload the folder": caching, compression, honest 404s
Almost every static-hosting bug lives in response headers. Which files may be cached forever, which may not be cached at all, and why a static site must never use an SPA fallback.
The @/ alias lives in three configs, and all three are required
Deep relative imports are banned project-wide, and the price is configuring the alias in three places. Miss one and you get editor errors, a broken build, or silently dropped styles.
zod as a frontmatter contract: bad articles fail the build
The Content Collections schema is the single source of truth. A title over 120 characters, a mangled date or a string where an array belongs fails immediately instead of silently dropping a page.
Why I built yet another personal site
Not because there was nowhere to write, but because there was nowhere to put writing, small games and tools side by side.
A zero-config registry: adding a game means adding a directory
App lists, detail routes, the home page picks and the sitemap are all discovered by import.meta.glob. The price is Vite's hard rule: literal relative paths only.
Views hold no logic: all data assembly lives in src/server
A page here exports a route, imports two components and passes props. Everything else — fetching, paths, dates — happens in src/server. Here is what that buys, and the type constraint it forces.
