MFK

AI Systems Architect · Rome, Italy

Open to work · Available immediately

This Site

A portfolio that turned into a publishing platform

📅 2026👤 Designer & Engineer🏢 Personal — Rome, Italy👁 3 views
Next.js 16TypeScriptSupabaseSassCanvas API
⚠️

Problem

Off-the-shelf portfolio templates all look the same, and hosted blogs own your content and your analytics. I wanted a site I controlled end to end, without paying three subscriptions for it.

🔧

Solution

Next.js 16 App Router with a Supabase-backed blog, first-party privacy-preserving analytics, HMAC-signed admin sessions, and two canvas-rendered apps. No CMS, no analytics vendor, no auth provider.

Result

A single deployment serving a portfolio, a blog with a markdown editor and moderation queue, an admin dashboard, and two free tools. Runs entirely on free tiers.

Impact

  • Blog platform with editor, image uploads, reactions and moderated comments
  • First-party analytics: read time, scroll depth, referrers, countries — no cookies
  • HMAC-signed sessions replacing a static shared secret
  • Two canvas apps rendering to PNG in the browser
  • All authored CSS in one SCSS file, themed from a single map

01The Problem

A portfolio is easy. A portfolio that also publishes, measures its own readership, and hosts working software is a different problem. The usual answer is to assemble it from services — a hosted CMS for posts, an analytics vendor, an auth provider, a form service. Each one is a subscription, a third-party script, and a place where your content lives on someone else's terms. Analytics vendors in particular want a cookie banner, which means asking every reader for consent to collect data I did not actually need. I wanted to own the whole stack and find out what that costs in practice.

02The Blog Platform

Posts live in Supabase. The admin panel is a markdown editor with live preview, cover and inline image upload to Supabase Storage, tags, series, canonical URLs, and draft state. Because I also publish on dev.to, the reading experience merges both sources. A shared module fetches from Supabase and the dev.to API in parallel, sorts by date, and tags each entry with its origin. The homepage shows the three most recent from either. Cross-posted articles carry a canonical URL so search engines are never asked to pick a winner. Readers get four reaction types and moderated comments, both session-based with no login. The moderation queue was the part I nearly forgot — comments arrived with an approved flag and no interface to flip it, so early submissions sat invisible in the database until I noticed.

03Analytics Without Cookies

Every visit records a random session id in localStorage, then counts one view per session per day. On top of that it measures active read time — a timer that pauses when the tab is hidden — and maximum scroll depth, flushed on pagehide so the last write survives the page closing. Country and city come from headers the platform already sets, so there is no geo-IP service. Device, browser and OS are parsed from the user agent. Referrers are classified into named sources rather than stored raw. None of it identifies anyone, nothing persists across browsers, and there is no cookie, so there is nothing to consent to. The pair I actually use is read time against scroll depth: together they tell you whether people finished or bounced at the intro, which a view count never will.

04Authentication

The first version stored a shared secret directly in a cookie and compared it with a string equality check. That fails three ways: the comparison leaks timing information, the token never expires, and rotating the secret invalidates every existing session — which is exactly what kept logging me out. It now issues an HMAC-SHA256 signed session carrying its own expiry, verified on every request with a constant-time comparison. It runs on Web Crypto so the same verification works inside Edge middleware, which guards both pages and API routes. Login is rate limited to six attempts per fifteen minutes.

05The Canvas Apps

Dear Diary generates diary entries across eight visual templates — vintage, parchment, spiral notebook, dark academia, cyber log, typewriter, night sky, holographic — with a photo that renders as if taped to the page, and exports to PNG or JPG. The first version styled DOM and screenshotted it with html2canvas. Gradients drifted, transforms broke, fonts fell back to defaults, and the page ignored its container width. Rewriting it to paint directly to a canvas fixed all four in one change, and removed a dependency. The lesson generalised: if the output is an image, build it as an image. Screenshotting a layout is a workaround for not having drawn one.

06Performance

The animated backdrop repainted the entire canvas on every mousemove — thirty-one gradient and path operations across the full viewport, at sixty frames a second, for a parallax offset of a few pixels. A second canvas drew a cursor trail at the same time, clearing the whole viewport each frame. Both are now close to free. The backdrop paints once and moves with a CSS transform on the compositor. The trail clears only the rectangle its particles occupy. Device pixel ratio is capped at two, since retina displays were painting four times the pixels for a background sitting at twenty-two percent opacity. Polling is visibility-aware throughout. The worst offender was a blinking terminal cursor updating every 530 milliseconds — a hundred and thirteen state updates a minute, all queued by the browser while the tab sat in the background.

07Styling

All authored CSS lives in one SCSS file: tokens, article typography, cursor, print styles, app chrome. Both colour themes are a single Sass map emitted to custom properties by a mixin, so changing the accent is one value in one place. Getting Tailwind v4 into a Sass file took some finding. The bare import errors in Sass; the url() form is passed through but Tailwind then ignores it. The form that works is importing the real file path, because Sass passes through any import ending in .css and Tailwind resolves real paths. That kind of detail is why the site has a colophon.

08What I Would Do Differently

I changed the accent colour without checking contrast. The light theme shipped at 1.44:1 against its background — links and button labels were not merely hard to read, they were invisible. Measuring takes thirty seconds and I did it after the fact rather than before. I also consolidated the stylesheets and silently dropped the font import in the same change. The site fell back to Georgia and system-ui and stayed that way for weeks, because a missing font does not throw an error. It looks slightly wrong, and slightly wrong is easy to stop seeing. Both were the same mistake: verifying that something builds is not the same as verifying that it works.