Most freelancer portfolio sites are either overengineered (React, Next.js, a CMS, three build tools) or underdesigned (a Squarespace template that looks like everyone else's). I wanted something fast, fully mine, and maintainable by editing a single HTML file. So I built it from scratch.

This is a writeup of the decisions behind zainsverse.de — the design approach, the CSS architecture, the self-hosting setup, and what I'd do differently. If you need a portfolio site for your freelance work, this approach is worth understanding.

The stack Plain HTML5, CSS custom properties, vanilla JavaScript (~50 lines). No build step. No dependencies. Deployed via Coolify on a Hetzner VPS in Germany. The whole site is a single directory of HTML files.

Why No Framework

A portfolio site has a few pages, mostly static content, and needs to load instantly. React adds 40KB of JS before a single character renders. Next.js adds a build pipeline you have to maintain. For this use case, both are the wrong tool. Plain HTML loads faster, is easier to update, and will still work in 10 years without touching a node_modules folder.

The rule I set myself: if I can't update any page by editing one file and pushing to GitHub, the architecture is wrong.

Design System in CSS Custom Properties

The entire visual system lives in CSS custom properties defined in :root. Changing the accent colour, background, or typography is one line. This makes the site easy to retheme without touching content.

:root {
  --bg: #0d0d0d;
  --surface: #141414;
  --border: #1f1f1f;
  --accent: #4895ef;
  --text: #e8e8e0;
  --text-muted: #6b6b60;
  --mono: 'DM Mono', monospace;
  --serif: 'Fraunces', serif;
}

Two fonts: DM Mono for body text and UI elements (technical, precise, readable at small sizes), Fraunces for headings (editorial, personality, contrast with the mono body). Both loaded from Google Fonts with rel="preconnect" for speed.

Layout Without a Grid Library

CSS Grid and Flexbox cover everything. The hero uses a two-column grid. The portfolio and services sections use CSS Grid with auto-fill and minmax — responsive without a single media query for the grid itself. The layout adapts cleanly from desktop to mobile.

The only media queries needed are for the nav (stack on mobile), the article padding (more generous on desktop), and the footer (row vs column). That's it. No breakpoint system, no utility classes.

Scroll Animations — 20 Lines of JS

The fade-in-on-scroll effect uses the IntersectionObserver API — no scroll event listeners, no library. Elements with class reveal start invisible and transition to visible when they enter the viewport.

const observer = new IntersectionObserver((entries) => {
  entries.forEach((e, i) => {
    if (e.isIntersecting) {
      setTimeout(() => e.target.classList.add('visible'), i * 60);
    }
  });
}, { threshold: 0.1 });

document.querySelectorAll('.reveal').forEach(el => observer.observe(el));

The staggered delay (i * 60ms) means elements in the same viewport section animate in sequence rather than all at once. It costs nothing and looks significantly better.

Deployment on Coolify

The site is hosted on the same Hetzner VPS that runs my n8n instance, managed by Coolify. The deployment is a static site connected to the GitHub repo — any push to main triggers an automatic redeploy within 30 seconds.

Coolify handles the Nginx configuration, SSL certificate (Let's Encrypt), and custom domain setup. For a static site, the configuration is essentially zero — point it at the repo, set the domain, and it works. This is also the same setup I use for client static sites, which makes it an easy thing to explain and hand off.

What I'd Do Differently

The main thing I'd change: extract the shared CSS (nav, footer, base styles) into a separate file and use a simple build step — just a shell script that concatenates the shared CSS with each page's styles — instead of duplicating the base styles across every HTML file. This keeps the no-framework approach but removes the repetition.

I'd also add basic analytics (Plausible, EU-hosted) from day one. Knowing which blog posts get traffic and which portfolio projects get clicks is worth more than any A/B test after the fact.

The Point of Building It Yourself

A freelancer's website is a working example of what they can deliver. If I'm selling automation and technical work to clients, shipping a hand-coded site with clean CSS, fast load times, and self-hosted infrastructure is a more credible signal than a Wix template. It's also something I can show in proposals as a reference for the kind of attention to detail I bring.

Build your own tools. Use them as case studies. The portfolio that sells the best is the one you built for yourself.