$ cat ./posty/devlogi/pivot-devlog-1.md

cutty devlog #1 — from "a few lines of code next week" to v0.3.3 in 24 hours

maj 19 17:04 | 4 min | autor: esej | #devlogi
● update 2026-05-19 — the pivot project evolved into cutty.dev. This devlog is the history of its development, but the product lives on under a new name and a new stack.

$ git log --oneline pivot/ --since="24 hours ago"

In devlog #0 exactly 24 hours ago I wrote this sentence:

"what's still not done: [ ] any lines of code. But don't worry. Next week."

So I didn't wait until next week. Nor was it calm.

== what happened ==

Instead of "a few lines of code in a free week" we got a full cycle: v0.1 → v0.1.1 → v0.2 → v0.3.0 → v0.3.1 → v0.3.2 → v0.3.3 in one evening, one night, and one morning. A classic case of "just one more thing".

Status as of today, May 20, 22:00:

  • v0.1 MVP — 3 endpoints, 1 table, /p/{slug} 301 redirect
  • v0.1.1 normalize URL — no http://, trim, //x.com, domain validation (7 input variants)
  • v0.2 stats — hit logging, ASCII bar charts, geo breakdown via Cloudflare CF-IPCountry
  • v0.3.0 public mode — anyone can shorten without logging in, cookie owner (UUID, 1 year)
  • v0.3.1 embedded dashboard + sidebar widget
  • v0.3.2 widget polished — zero custom styles, theme handles chrome
  • v0.3.3 popup copy — because "LIVE v0.3.2" looked like a commit message, not a status

== it's LIVE ==

The project page /projekty/pivot/ has a public form. Paste anything, get esej.space/p/{slug}. You see your links only because of the cookie. Below the form is a dashboard with inline stats — by default it shows only the latest link, the rest behind the ↓ show all button (because a list of 20 links killed the UX).

In the right sidebar, where Jellyfin/IRIS/Matomo/Nextcloud are — a purple pivot badge. Click it, popup with description, link to GitHub. It looks like part of the theme from day one, because it uses the same DOM structure and the same CSS variables as the rest.

== this should have killed the project in the first 3 hours ==

But somehow it didn't. List of mushrooms along the way:

  1. PowerShell Compress-Archive creates ZIPs with backslashes in paths. The target system treated esej-pivot\esej-pivot.php as a single file with a weird name. The plugin "installed" — admin GUI saw it, REST API didn't. Activation: 404. Phantom folder on the server that couldn't be deleted from within WP. Solution: manual cleanup at the container's filesystem level (via web terminal) and switching to Python zipfile with forward slashes. (Me, naively, before: "but ZIP is a standard, what could go wrong".)
  2. WordPress wpautop filter murders JS in <script>. Converts <div> in JS strings to </p>. JSON parse crashes. Solution: base64-encode payload, decode on the client side. (Because XKCD 1739, but still in 2026.)
  3. wpautop inserts <br> between adjacent <span>. The demo on the pivot page broke. Solution: spans on one line, no newlines. (Fighting the platform I chose myself, in 2026.)
  4. CSS @media queries truncated by wpautop at {. Responsive layout crashed on mobile view. Solution: grid-template-columns: repeat(auto-fit, minmax(460px, 1fr)) — responsive without media queries.
  5. WP REST API doesn't show "phantom" plugins with broken paths. So I could have the fantasy that the old plugin didn't exist, while it did. Had to use Unraid CLI to see the truth.
  6. App Password (WP REST) has a narrower scope than cookie session. Multipart upload to /wp-admin/update.php requires cookies. Necessity of hooking in via /wp-login.php POST + maintaining the session.

== controversial decision: public mode ==

Originally, the pivot was supposed to be admin-only. "My shortener, my rules". But after thinking about it — the esej.space site thrives on traffic. If someone lands here and can shorten something without logging in, then:

  • there's a higher chance they'll come back
  • there's a higher chance they'll show it to someone
  • my costs: zero (4-char slug, base62, I already have MySQL)

So public mode without signup. Anonymous owner via cookie UUID — only you see your links. No login required. Rate limit 5 links / 15 min / IP. Honeypot anti-bot. Self-service delete.

If someone wants a dashboard with a week of activity, custom slug, custom domain — that's v0.4 and v0.5 (user accounts with magic link login). But the foundation is ready: Esej_Pivot_Owner::current() returns user_id for logged-in users, cookie UUID for anon. Dashboard, REST endpoints, ownership checks — everything is already future-proof.

== what's next ==

  • [ ] v0.4 power features — custom slug, custom domain via CNAME, bulk shorten, CLI helper, expire dates, password-protected links
  • [ ] v0.5 user accounts — magic link login, claim flow (anon cookie → account)
  • [ ] llms.txt — AI crawlers already have the green light in Cloudflare
  • [ ] site multilingual support (PL ↔ EN) — because the GitHub README is in English, but the landing page isn't

// PS: this post went from the plan "next week" to publishing 24h after devlog #0. ETA for the rest of the roadmap: don't trust me.

// PPS: the first link I shortened in production led to this post. esej.space/p/k3n9 → here. Pivot from my host to my host. Ideally superfluous. Ideally satisfying.