---
type: collection
section: posts
generated_at: 2026-08-18T13:26:34+02:00
total_entries: 3
tag_filter: Webdev
---

# 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.

## 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.

## 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
```

