How this site is built
A few times in interviews people have asked why a backend developer runs a site with no client-side framework at all. It is easier to write the answer down once than to repeat it every time.
So, in order: what I settled on, what else I looked at, and where it later tripped me up.
Stack
- Astro — a static site generator. Runs the code at build time and writes plain HTML.
- TypeScript — build time only, none of it reaches the browser.
- Plain CSS with custom properties. No preprocessor, no utility classes.
- My own VPS with nginx — serves the built static files.
- Zero runtime dependencies. Not a single library is shipped to the client.
Why Astro
I needed a bilingual personal site that I edit once every few months. My wish list was short: the page should open instantly even on a mobile connection, the text should live separately from the markup, and I did not want to be repairing the build a year later because of a major framework release. Astro fit that well — it runs the code once at build time and leaves plain HTML behind. Nothing runs on the client until I ask for it.
What else I considered:
- Next.js
- Too much. It solves problems I do not have: rendering per request, data loading, cache revalidation. I would be paying in complexity for features I never use.
- Hand-written HTML
- A fair option, and I considered it seriously. I dropped it because of the two languages: I would have to keep two copies of the markup and sync them on every edit. That is exactly the kind of task where manual syncing drifts apart sooner or later.
- Hugo
- Builds faster and needs no Node. But Go templates are less familiar to me, and on a two-page site the build-speed gain is zero.
The cost: I need Node to build, and I will have to update dependencies once a year. That suits me — it is one dependency to update, not thirty.
Why no React
The styles and scripts are inlined into the HTML, so there are no separate asset files at all. Here is what each page weighs on disk and what actually goes over the wire, since nginx serves everything with gzip:
| Page | On disk | Over the wire |
|---|---|---|
/ | 165 KB | 29 KB |
/en/ | 148 KB | 26 KB |
/stack/ | 38 KB | 10 KB |
Twenty-nine kilobytes for the main page covers everything at once: text, layout, diagrams, and the description behind every technology in the stack section. No bundle, no hydration, no loader. The only external request the page makes is for the fonts.
There are two scripts on the page, both written by hand and inlined into the HTML: a seven-line theme toggle and the technology tooltips. Together they are under four kilobytes, and that is all the JavaScript on the site.
There is barely anything here that needs to be interactive: text, links and a couple of diagrams. Shipping a few hundred kilobytes of runtime for that would mean paying for something the page does not even have. If I ever need a genuinely complex widget, Astro lets me add React to that one component instead of the whole site. So far I have not needed it.
A side effect I did not plan but am glad about: without JavaScript the site loses only the tooltips and the theme toggle — all the text is still there. And it prints to PDF cleanly straight from the browser.
Two languages, one set of markup
The texts live in src/content/ru.ts and en.ts; the markup is shared. I edit a sentence in one place and both pages rebuild.
The content shape is described with types. If I add an experience entry to the Russian version and forget the English one, the build fails with a type error instead of publishing a half-empty page. A cheap safeguard against my own carelessness.
Diagrams
The diagrams in the architecture section are not images. Each one is described as a list of nodes and edges:
{ id: "kafka", col: 3, row: 4, kind: "broker" }
{ from: "progress", to: "kafka", async: true }A component computes the coordinates and draws the SVG. What that buys me: the labels are translated while the geometry stays shared; the diagram inherits the theme colours and stays readable on a light background and in print; and the text in the diagram is real text, so page search finds it and a screen reader can read it.
The layout is deliberately simple — a grid of columns and rows with right-angled edges. For diagrams this size that is enough, and a proper graph layout algorithm would be solving a problem I do not have.
Accessibility and themes
Dark theme by default, light on the toggle, and the choice is remembered. I checked every text-on-background pair against WCAG AA in both themes. That is also why the filled buttons use a darker blue: white text on the original shade gave only 3.2:1 where 4.5 is required.
Nodes in the diagrams differ by border style as well as colour — solid, dashed, dotted. Otherwise the diagram falls apart for colour-blind readers and in black-and-white print.
Accessibility is also the reason the technology tooltips could not be pure CSS. :hover shuts out half the audience: there is no hover on a phone, and a keyboard user cannot open the tooltip at all. So each chip is a button, the card opens on hover, focus and tap, closes on Escape, and is tied to the chip through aria-describedby. That is what the second script is for: seventy lines so the tooltips work for people who are not holding a mouse.
Smaller things: a skip link for keyboard users, a visible focus ring, prefers-reduced-motion turning animations off, and wide diagrams scrolling inside their own container instead of dragging the page sideways.
Build and deployment
The deployment is deliberately low-tech, no CI: I build the site locally (npm run build), copy the static files to my server over SSH, and nginx serves them at bonkurov.ru. HTTPS is a Let’s Encrypt certificate that renews itself, and http redirects to https.
Before every build I run a small check of my own. It looks for unfilled placeholders such as [~450] in the texts and refuses to build if it finds any. The reason is mundane: I keep drafts in the same files as the finished text, and I once came close to publishing a page with [name the domain] in the opening paragraph.
What is deliberately missing
- Analytics
- No counters, no pixels, no cookie banner. I do not need visitor data badly enough to make the visitor pay for it.
- External requests
- Nothing beyond the fonts. Everything else sits next to the HTML.
- A blog
- A section updated twice a year works against its author: it shows the absence of activity rather than the activity.
- A CMS
- I edit the text in a code editor. For a site that changes once a quarter, an admin panel is one more service to maintain — and one more thing to break.
What I would do differently
The diagrams should probably live in their own format instead of sitting in TypeScript. With just two of them I do not notice, but around ten it will start to bother me that editing a single label means rebuilding the whole project.
And defaulting to the dark theme instead of following the system setting is a choice in favour of how the site looks over what the visitor is used to. I know it is arguable. The toggle in the header is the compromise.