This site has been rebuilt from the ground up. New look, new stack, same idea — a quiet home for the work and a notebook for the build. Welcome.
What changed
The previous site was a Nuxt 2 app from 2021: Vue 2, Tailwind 2, a sprinkling of plugins, and content lived inside components. It served well, but the framework had reached end-of-life and the content layer was hard to extend.
The new site keeps the visual character — dense typography, generous whitespace, a strong accent — and rewrites everything underneath:
- Content moves out of components into typed collections (
src/content/) with Zod schemas for home copy, life chapters, legal pages, and the blog you’re reading now. Posts are MDX. - Pages render static at build time and ship as plain HTML. Interactive bits hydrate on idle, not on load.
- i18n is route-based (
/de/...) instead of plugin-based, which removes a layer of magic from URLs and SEO. - One toolchain for lint, format, and check (Biome) replaces the old ESLint + Prettier pair.
The stack
- Astro 6 — content-first, mostly-static, islands for the few pieces that need JavaScript.
- React 19 + Motion — the only interactive bits (cursor effects, mobile menu, marquee) are React islands animated with Motion.
- MDX — long-form content with the option to drop in components when a post needs them.
- Tailwind 4 alongside a small set of hand-written CSS custom properties for type and color tokens.
- Biome for lint + format, Netlify for hosting, Resend for the contact form.
A look inside: the cursor trail
The thing most people notice first is the streak that follows the cursor. It’s a
React island that listens for pointermove, measures velocity between samples, and
spawns short-lived blobs that stretch along the direction of motion — so a slow drag
leaves soft dots, while a fast flick leaves a comet.
The core of it is small. Stripped of throttling and the React state plumbing, the geometry is just this:
const onMove = (e: PointerEvent) => {
const now = performance.now();
const dx = e.clientX - lastX;
const dy = e.clientY - lastY;
const dt = Math.max(now - lastTime, 1);
// Speed → how much the blob stretches along its travel direction.
const speed = Math.sqrt(dx * dx + dy * dy) / dt;
const stretch = 1 + Math.min(speed / FAST_SPEED, 1) * (MAX_STRETCH - 1);
const angle = (Math.atan2(dy, dx) * 180) / Math.PI;
spawn({ x: e.clientX, y: e.clientY, angle, stretch });
lastX = e.clientX;
lastY = e.clientY;
lastTime = now;
};
Each spawned mark is a blurred radial gradient inside an AnimatePresence — it
fades out within ~700ms and removes itself. The full source, including the
throttle (MIN_DIST, MIN_INTERVAL), the reduced-motion bail-out, and the
coarse-pointer guard, lives in src/components/motion/CursorTrail.tsx.
If you have Reduce Motion enabled at the OS level, the effect doesn’t render —
the entire component returns null. Same for touch devices, where it would be
both pointless and expensive.
What’s next
More posts will land here as small tools, workflow notes, and the occasional deeper write-up on a piece of the site. Nothing on a schedule — only when there’s something worth keeping.
Thanks for visiting.