---
type: collection
section: posts
generated_at: 2026-04-11T07:04:41+02:00
total_entries: 6
---

# Posts

## The Case for Owning Your Website

---
type: post
id: dhwyguwnnotsdcqz
title: The Case for Owning Your Website
url: >
  https://www.jakubpelak.com/posts/case-for-owning-website
section: posts
tags:
  - Webdev
  - Web
  - Ownership
  - Reflection
published_at: 2026-04-06
---

I've had a personal website in some form for most of my career. Not because I had a grand plan for it, but because it seemed like an obvious thing to have—a place on the internet that's mine. Not rented, not shaped by someone else's interface, not dependent on what a platform decides to prioritize this quarter.

It doesn't need to be complicated. In fact, it works better when it isn't. A single page, a few links, maybe a short description of what you do—that's already enough. The point isn't to build something impressive. It's to have something that exists on your terms.

### An Anchor, Not a Replacement

Most of us exist across multiple platforms. Twitter[^1], Instagram, Substack, LinkedIn, and whatever comes next. Each of them solves a specific problem well—distribution, discovery, convenience. There's a reason they're successful, and it makes sense to use them.

But they're not you. They're representations of you, (link: https://www.hanselman.com/blog/your-words-are-wasted text: filtered through someone else's system target: _blank rel: external noopener). And those systems (link: https://www.wired.com/story/tiktok-platforms-cory-doctorow/ text: change target: _blank rel: external noopener). Sometimes gradually, sometimes all at once.

That's where a personal website starts to matter. Not as a replacement, but as an anchor. A single place that doesn't shift underneath you. A point of reference you can always link back to. Something that stays stable even as everything around it evolves.

### Ownership, Practically

The domain is the obvious part—it's your address, the one thing you can carry with you regardless of where or how the site is hosted. But there's another layer that matters just as much: owning the code. Even if that code is a single HTML file.

I think there's an important difference between publishing content into a system and having direct control over how your site is built. Being able to open a file, change something, and know exactly what happened. No hidden layers, no abstractions you didn't choose.

A personal website doesn't need to compete with platforms. It doesn't need features, feeds, or (link: https://www.reforge.com/blog/growth-loops text: growth loops target: _blank rel: external noopener). It just needs to exist. And because of that, it can stay small—a few static pages, plain content, minimal structure. That kind of simplicity holds up over time. There's very little to break, maintain, or migrate.

I'm not arguing against platforms. They're extremely good at what they do. The problem isn't using them—it's relying on them as your only presence. They don't belong to you, and they don't have to stay the way they are. Your website doesn't replace them. It sits next to them.

### Technical Bias

I approach this from a technical angle, admittedly. I like understanding how things work, and I like being able to change them. Over time, I found myself gravitating toward systems where content is just files—simple, predictable, and easy to host almost anywhere.

That's what led me to (link: https://getkirby.com text: Kirby target: _blank rel: external noopener). No database, just a set of files you fully control. It's not the only way to build a website, but it aligns well with this idea of ownership. It's also the approach I follow with the templates I build at (link: https://tablo.supply text: tablo.supply target: _blank rel: external noopener)—small, self-contained websites that don't require much to run and don't lock you into anything.

I keep coming back to the idea of building my own CMS too. Not because I need one, but because it's a useful exercise in constraint. What's the minimum you actually need? Where does complexity start creeping in? Those questions tend to return.

If you don't have a website yet, don't overthink it. You don't need a system, a design, or a plan. You just need a place. Start small and adjust over time.

A personal website isn't about rejecting platforms or doing things the hard way. It's about having a stable reference point—something that doesn't move unless you decide it should. Everything else can change around it.

[^1]: Yes, I still call it Twitter. No deep reason—it just feels like a more fitting name, especially as a verb.

## Tartare-Style Burger, Done the Hard Way (So It Stays Simple)

---
type: post
id: frupfj09snpbbmnh
title: >
  Tartare-Style Burger, Done the Hard Way
  (So It Stays Simple)
url: >
  https://www.jakubpelak.com/posts/simple-hard-tartare-style-burger
section: posts
tags:
  - Cooking
  - Recipe
  - Food
  - Minimalism
  - Personal
published_at: 2026-02-15
---

This started as a small experiment and quietly turned into one of the best burgers I’ve made at home. Not because it’s fancy, or original, or optimized for social media. Quite the opposite. It’s a deliberately minimal burger built around constraints: limited ingredient availability, sensitivity to freshness, and a strong preference for food that feels *light* rather than heavy. Over time, a few decisions proved themselves enough times to become intentional choices.

(image: file://k9lthbfau2riwino)

### Why Tartare Meat

I live in a fairly remote place. Getting truly fresh, high-quality minced beef is unreliable at best. I’m also very sensitive to food that isn’t fresh — especially meat — so “it’s probably fine” isn’t a risk I enjoy taking.

Supermarket beef tartare solves that problem surprisingly well.

It’s intended to be eaten raw, which means it’s handled, processed, and packaged with that expectation in mind. Even when bought at a regular supermarket, tartare-grade beef is consistent, clean, and predictable. I can rely on it. That matters, especially when the burger is intentionally cooked only on one side.

As a bonus, its fine texture and lean cut make it ideal for a light sear. It holds together without fillers and stays tender without becoming mushy.

### One-Side Sear, On Purpose

The patty is browned from one side only. No flipping, no chasing crust symmetry.

Part of this is about texture — crisp on one side, soft and juicy on the other — but part of it is digestion. Heavy browning, especially when combined with a lot of fat, tends to feel… heavier. Overdone crust is often praised as flavor, but for me it crosses into diminishing returns quickly.

Medium heat, controlled browning, and restraint produce a burger that tastes meaty, not aggressive.

### Onions: Experiments and Conclusions

I tried placing very thinly sliced onions *under* the meat, smash-burger style. The onions were good. Soft, sweet, nicely cooked.

The meat, however, lost its crust. That trade-off wasn’t worth it.

A much better result came from caramelizing the onions separately and adding them only during assembly. You get sweetness and depth without sacrificing the integrity of the patty. It’s an extra step, yes — but one that earns its place.

Raw onions, even sliced paper-thin on a mandoline, work too. But caramelized onions are a clear upgrade.

### Cheese, Steam, and a Small Trick

Cheese goes on top of the meat *while it’s still in the pan*. Nothing else.

Right before the meat is done, I add a tiny splash of water to the hot fat and close the pan with a lid. The resulting steam melts the cheese quickly and evenly, without further browning the meat or burning the patty underneath. It’s a small diner trick, but it works beautifully here.

The onions are added later, during assembly, together with thinly sliced pickled cucumbers. Other pickled options work just as well. This keeps the structure clean and familiar — classic burger composition, just executed with a lighter hand.

### Pork Fat, Duck Fat, and Why It Matters

I usually cook this in pork fat. It adds depth without overpowering the beef. If you have it, duck fat works just as well — maybe even better. Slightly richer, slightly rounder. Neutral oils work, but they feel like a missed opportunity. Since the ingredient list is short, every element has to earn its place.

### The Toaster, Not the Pan

This is the part where I knowingly disagree with popular burger advice.

I toast the buns dry, in a toaster.

Most buns available to me are light and airy. Toasting them in fat turns them into sponges almost instantly. They soak up grease, collapse before browning, and lose structure long before they gain flavor.

The toaster does something different: it creates a dry, crisp surface that acts as a protective layer. After toasting, I spread a small amount of butter on the hot sides. You still get aroma and richness, but without destroying the bun’s integrity.

When assembled, the bun holds up. It doesn’t dissolve under juices. That matters more than theoretical flavor gains.

### Assembly Philosophy

Mustard, ketchup, pickles. Nothing clever.

These aren’t there to dominate. They’re there for acidity, sweetness, and contrast. A thin tomato slice works well too, especially if lightly salted beforehand, but I don’t always have one on hand.

The goal isn’t to build *the* burger. It’s to build *this* one — light, meaty, slightly spicy, and clean enough that you don’t feel weighed down afterward.

### Final Thoughts

This burger isn’t about shortcuts, but it’s also not about complexity. It’s about making a few deliberate choices and sticking to them, even when conventional wisdom suggests otherwise.

Like good software, it benefits from constraint. Remove what doesn’t earn its place. Respect the materials. Don’t overengineer the solution.

Sometimes that’s enough.

## Why I Built a Utility-First CSS Framework — and Eventually Moved On

---
type: post
id: 8xiyuqn1lqa7ntx9
title: >
  Why I Built a Utility-First CSS
  Framework — and Eventually Moved On
url: >
  https://www.jakubpelak.com/posts/utility-first-css-framework-moved-on
section: posts
tags:
  - Webdev
  - CSS
  - History
  - Reflection
published_at: 2025-12-19
---

In 2017, I built my own utility-first CSS framework. Not because it was trendy, and not because I wanted to launch “the next big thing”, but simply because I was tired of fighting CSS in real client work. The framework was called (link: https://abrusco.com text: Abrusco target: _blank rel: external noopener), and for a long time it quietly did exactly what I needed it to do.

By coincidence, Abrusco’s first commit landed about two weeks before (link: https://tailwindcss.com text: Tailwind target: _blank rel: external noopener)’s. Same general idea, same direction, very different outcomes. This is not a regret story. It’s more a reflection on what I was trying to solve at the time, why it mattered to me, and why (link: https://www.mooreds.com/wordpress/archives/3616 text: letting Abrusco go eventually target: _blank rel: external noopener) felt like the right decision.

### The Problem I Was Trying to Solve

Anyone who has worked with CSS long enough knows how this usually goes. A project starts clean, well-structured, and easy to reason about. There’s a style guide, a system, maybe even a sense of pride. Then reality arrives. Clients ask for small visual tweaks. Deadlines get tighter. Quick fixes sneak in. Over time, the neat architecture slowly turns into layers of overrides and exceptions that nobody loves, but everyone depends on.

(link: https://adamwathan.me/css-utility-classes-and-separation-of-concerns text: Utility-first CSS target: _blank rel: external noopener) felt like a more honest response to that reality than most (link: https://www.smashingmagazine.com/2013/10/challenging-css-best-practices-atomic-approach text: architectural approaches I had tried before target: _blank rel: external noopener). It’s often far healthier to change `mr-2` to `mr-4` on a single element than to introduce another special-case selector that bends the cascade a little further out of shape. Around that time I came across (link: https://tachyons.io text: Tachyons target: _blank rel: external noopener), (link: https://basscss.com text: Basscss target: _blank rel: external noopener), and a few talks exploring this idea, and it clicked hard enough that I wanted a solution I could fully shape around my own workflow.

So I built one.

### What Abrusco Was (and Wasn’t)

Abrusco was never meant to be Tailwind before Tailwind. It was deliberately simpler and more pragmatic. I wanted a CSS-first tool, without a mandatory compiler or complex setup. One stylesheet you could drop into a project and start working immediately. Utility classes would handle most layout and spacing problems—roughly 80 to 90 percent of what I needed—while anything truly project-specific, like animations or complex visual effects, would still live in plain CSS.

Because there was no compilation step, I leaned heavily on CSS variables. Colors, spacing, scales—everything was designed to be adjustable at runtime. Some of the solutions were small but meaningful. One example I used a lot was padding that automatically compensated for border width, so adding a border wouldn’t subtly break visual rhythm. These details mattered to me because they made everyday work calmer and more predictable.

Abrusco wasn’t a theoretical exercise. I shipped real projects with it, and it served me well. Later on, I even released a small CLI setup using PostCSS plugins for imports, minification, and purging, but the core idea stayed the same: you could always just include the CSS file and get moving.

### Letting It Go

At the same time, (link: https://tailwindcss.com text: Tailwind target: _blank rel: external noopener) was growing. It arrived with a very similar philosophy, but also with something I didn’t have: the time and focus to polish documentation, build momentum, and grow a large community around it. The underlying idea was strong, but Tailwind turned it into a platform. That kind of execution matters.

Watching Tailwind evolve never felt discouraging. If anything, it felt validating.

Eventually, the question became less about whether Abrusco could continue, and more about whether it *should*. Turning it into a fully polished, community-ready framework would have required a level of ongoing maintenance, documentation, and promotion that simply didn’t fit my priorities anymore. Meanwhile, Tailwind kept improving, and over time even adopted directions I personally liked more, especially as it moved closer to CSS again rather than further away from it.

At some point, pushing Abrusco forward would have been about competition for its own sake, not about solving my problems better. That was the moment I decided to move on and focus my energy elsewhere—on products, client work, and ideas that felt more aligned with where I wanted to go.

### Looking Back

Abrusco didn’t become a global framework, and it never built a large community. But it shaped how I think about CSS, about tools, and about building things in general. It proved to me that I wasn’t just following trends, but independently working through the same problems others were beginning to articulate.

And honestly, that’s enough.

Abrusco still exists if you’re curious:

* GitHub: (link: https://github.com/lemmon/abrusco target: _blank rel: external noopener)
* Homepage: (link: https://abrusco.com target: _blank rel: external noopener)

Sometimes a tool doesn’t need to conquer the world to be meaningful. Sometimes it just needs to solve your problems at the right moment, help you grow, and then quietly step aside. Abrusco did exactly that for me.

## Blank Bookmarks Slate

---
type: post
id: v5csvQzMF60S3FuI
title: Blank Bookmarks Slate
url: >
  https://www.jakubpelak.com/posts/blank-bookmarks-slate
section: posts
tags:
  - Web
  - Productivity
  - Life
published_at: 2024-09-24
---

Like anybody else, I keep my ideas organized in web browser's bookmarks. And like many others, it is a mess. An endless collection of folders and piles of cooking recipes, website inspirations, stuff I'd like to buy one day (that I surely don't need anymore), or countless ideas I’ve since lost interest in.

Lately, I've been noticeing a trend... Whenever I need to find something, I just can't. I know it's there, I know I have, without question, bookmarked it. But with the sheer volume of items, the one thing I need just doesn't want to surface.

I've done partial cleanups in the past. Deleting certain sections, but mostly non-work-related bits. Today I decided to get rid of them all. To wipe the slate clean. This archive if everything and nothing, at the same time, is not usefull to me anymore. I am starting fresh. I know that infinite labyrinth of folders and bookmarks will inevitably be rebuilt back soon enough. I am not worried.

## Vercel Open Graph Card With No Framework

---
type: post
id: sUtUDRVCq3s6Oz4Y
title: >
  Vercel Open Graph Card With
  No Framework
url: >
  https://www.jakubpelak.com/posts/vercel-og-card-no-framework
section: posts
tags:
  - Webdev
  - Vercel
  - JavaScript
  - Node.js
  - SEO
  - Open Graph
published_at: 2024-03-13
---

I was pretty excited when Vercel announced their library (link: https://www.npmjs.com/package/@vercel/og text: `@vercel/og` rel: external) for dynamically generating (link: https://vercel.com/docs/functions/og-image-generation text: Open Graph images rel: external). However I have immediately noticed that all the examples require installing either Next.js or transpiling your code with JSX. Luckily, JSX converts to plain javascript objects. With only few required in my case, there is no need to install any unnecessary libraries.

This (jsx):

```html
<div style="color: orange">Hello!</div>
```

becomes this (plain javascript):

```js
{
  type: 'div',
  props: {
    style: {
      color: 'orange',
    },
  },
}
```

### The Example

Check out the (link: https://ogcardone.vercel.app/api/card text: Live Demo rel: external) or fork and tweak the (link: https://github.com/lemmon/ogcard-vercel-examples/tree/main/01-one text: GitHub Repository rel: external) yourself.

```js
// api/card.js
import { ImageResponse } from '@vercel/og'

export const config = {
  runtime: 'edge',
}

export default async function () {
  return new ImageResponse(
    {
      type: 'div',
      props: {
        style: {
          width: '100%',
          height: '100%',
          display: 'flex',
          flexDirection: 'column',
          justifyContent: 'center',
          alignItems: 'center',
          padding: 64,
          color: '#040404',
          backgroundColor: '#f2f2f2',
          fontSize: 80,
          lineHeight: 1.125,
          letterSpacing: -1,
          textAlign: 'center',
        },
        children: 'Hello There 👋',
      },
    },
    {
      width: 1200,
      height: 630,
    }
  )
}
```

This is all the code you need to generate simple open graph card image. No redundant libraries, no code to transpile. Look at the dependencies:

```json
{
  "type": "module",
  "dependencies": {
    "@vercel/og": "0.6.2"
  }
}
```

Even fonts and emojis work out of the box. Thank you Vercel!

### The Localhost Problem

This code doesn't run on your local machine when running `vercel dev`. I remember having this issue a year back when I first started experimenting with this library. In both cases there were no issues when deployed to Vercel.

However a few months back, somewhere around version 0.5.x, the code started working on my local machine just fine. But... in time of writing this post, and creating the example above, I encountered another issue with no luck of running it locally. Bear this in mind when experimenting with it on your own. I have tired going back and forth with @vercel/og versions with no success. I am attaching my current stack for future reference. Sometimes you just have to wait a bit for issue tu resolve by itself. 🤷‍♂️

```
Node.js ............. 18.19.1
NPM .................. 10.5.0
Vercel CLI ........... 33.5.5
@vercel/og ............ 0.6.2
```

## Hello World

---
type: post
id: Z4gCxFcjw8vALVdO
title: Hello World
url: >
  https://www.jakubpelak.com/posts/hello-world
section: posts
tags: [ ]
published_at: 2024-03-01
---

Mandatory first post. Well... Hello World!

👋

