Back to writing

How this portfolio works

Static pages written at build time, islands that load only when they earn it, and motion left as a choice. The decisions behind the site and why I made them.

viteperformanceaccessibility

I set one rule when building this site: a visitor's browser should never need my JavaScript in order to see the content. Every other decision follows from that.

Not a framework, islands

The page itself is not built with a framework. All of its content is written out as HTML while Vite compiles the site; the code that runs in the browser enhances the existing DOM rather than recreating it. Heavier libraries load only when they earn their place, as islands:

  • On first load (~44 kB gzip): the page's own logic, smooth scrolling and reveal animations.
  • The emblem is drawn on a plain 2D canvas first. The WebGL version (~152 kB gzip) arrives after that first paint, and only if the visitor has not asked for reduced motion and is not saving data.
  • The project window is written in React and loads only when a project is opened (~111 kB gzip).

Who pays for that split matters. Someone on an old phone over a bad connection gets the first 44 kB and reads the whole page. The rest is for the device that asked for it and can carry it.

Pages are written at build time

npm run build does two things. Vite produces the bundles, then my own prerender step writes every page to disk as real HTML:

bash
npm run build
# vite build  → dist/assets/*
# prerender   → index.html, en.html, writing.html, writing/*.html,
#               sitemap.xml, feed.xml, robots.txt, 404.html

Every address in dist/ returns readable text even if JavaScript never runs. The script that does run in the browser only enhances what is already on screen: the mobile menu, the reveal-on-scroll sections, and the language switch.

My favourite side effect of static generation is that there is less to break. No server-side API, no secrets, no database.

Motion is a choice

The emblem on the opening screen — my monogram, extruded and finished in orange chrome — is drawn on a plain 2D canvas first; the WebGL version arrives later, and only with permission. A beam travels up through it, heating the surface it touches and exposing the structure underneath. If a WebGL context cannot be created it falls back to the 2D version, so no device is left with an empty box. Both versions follow the same three rules:

  • They stop drawing when the form scrolls out of view
  • They stop when the tab moves to the background
  • They never start when prefers-reduced-motion: reduce is set, and the WebGL upgrade is skipped entirely

There is also a pause control in its corner. Someone who wants to stop an animation should not have to go into their system settings to do it.

Two languages, two real addresses

Turkish and English content live at separate addresses rather than behind a ?lang= parameter: / and /en. Each page carries its own canonical address and mutual hreflang links. The language switch works with JavaScript disabled, because there are real links underneath it.

This section

Writing goes through the same pipeline. Markdown files under content/writing/ are read during the build, converted to HTML and written out as static pages. The Markdown renderer lives inside the project; every character — code blocks included — is escaped before any markup is produced.

What lands here will mostly be notes on whatever I am working on: vulnerability analyses, problems met while building tools, and observations from lab work.