<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>afonso jorge ramos</title><description>Product-minded software engineer and tech lead. Most of my work is full-stack TypeScript, and I&apos;m framework-agnostic: most at home in React, but just as comfortable shipping in Svelte, Solid, Vue, or Angular. I ship across languages just as readily: I core-maintain Spicetify, a Go CLI with 20M+ downloads and 23k+ GitHub stars, and I brought macOS support to qbz, a bit-perfect hi-fi audio player written in Rust, upstreaming fixes to it and to the Rust audio crates it builds on, like coreaudio-rs and notify-rust.</description><link>https://afonsojramos.me/</link><item><title>The Accidental Homelab</title><link>https://afonsojramos.me/blog/accidental-homelab/</link><guid isPermaLink="true">https://afonsojramos.me/blog/accidental-homelab/</guid><description>Nine years of self-hosting media, from Plex on a gaming PC to TrueNAS with 5×18TB raidz2 and ~30 apps. Built one budget at a time.</description><pubDate>Tue, 05 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In 2016, as a university student, I installed Plex on my gaming desktop and started serving local media off it. The desktop was an i7-4770K with a GTX 770, the first PC I’d built myself, back in 2013 at 15. Nine years later, that same Plex install has grown into a TrueNAS server running ~30 self-hosted apps, ~40 TiB of usable storage on a raidz2 pool (with a story behind that number, &lt;a href=&quot;#what-writing-this-post-turned-up&quot;&gt;see below&lt;/a&gt;), and serving roughly 20 people across a few countries. None of it was planned. Every step came after the previous one started to break, and was paid for whenever the budget caught up.&lt;/p&gt;
&lt;h2 id=&quot;before-plex-20102016&quot;&gt;Before Plex (2010–2016)&lt;/h2&gt;
&lt;p&gt;I’d been torrenting since 2010. I was twelve, &lt;a href=&quot;https://en.wikipedia.org/wiki/UTorrent&quot;&gt;uTorrent&lt;/a&gt; was the client, and the workflow was strictly manual: pick the show or movie I wanted, search for it, find a healthy seed, download, watch. The uTorrent download folder &lt;em&gt;was&lt;/em&gt; my entire media library. Half of it was things I’d already watched, half was things I was queuing up for the weekend, and the only “library management” was my own memory plus a &lt;a href=&quot;https://en.wikipedia.org/wiki/TVShowTime&quot;&gt;TV Show Time&lt;/a&gt; account that kept track of which episode of which show I was up to.&lt;/p&gt;
&lt;p&gt;This was the &lt;a href=&quot;https://en.wikipedia.org/wiki/KickassTorrents&quot;&gt;KickassTorrents&lt;/a&gt; era, and KAT was awesome - moderators, comment threads on every release, a community that would flag bad rips and call out fakes. The site got taken offline in a &lt;a href=&quot;https://www.justice.gov/archives/opa/pr/us-authorities-charge-owner-most-visited-illegal-file-sharing-website-copyright-infringement&quot;&gt;US-led operation&lt;/a&gt; in 2016, and the gap eventually got filled, for me at least, by private trackers, where ratios are enforced and quality gets policed much more directly. Tens of terabytes of seeding later, I’m still on the same ones.&lt;/p&gt;
&lt;p&gt;Plex, when I found it in 2016, was the abstraction I’d been missing. It turned that flat folder of files into something I could actually browse - metadata, posters, subtitles, watch progress - and, more importantly, share with people who weren’t going to learn what &lt;code&gt;.mkv&lt;/code&gt; meant.&lt;/p&gt;
&lt;h2 id=&quot;the-gaming-rig-era-20162020&quot;&gt;The Gaming-Rig Era (2016–2020)&lt;/h2&gt;
&lt;p&gt;The first version was the laziest possible setup: Plex on my main Windows desktop, drives merged with Windows’ built-in Storage Spaces so 2×1TB + 1×4TB looked like a single mount point, port-forward so a couple of family members could stream from outside. The “server” was whatever happened to be on the gaming PC at the time. I held out on Plex Pass until 2018, when I caught one of their 50%-off lifetime sales - still easily the best money I’ve ever spent on this whole project.&lt;/p&gt;
&lt;p&gt;It worked exactly as well as you’d expect. Plex transcoding is CPU-bound, gaming is GPU-bound, but they both fight for memory bandwidth and disk IO. Three people watching from outside the house on shaky connections, and my gaming session noticed. I could pause my game, kick people, or accept that nobody’s experience was going to be great. But to be honest, this did not happen that often back then.&lt;/p&gt;
&lt;h2 id=&quot;the-i7-4770k-as-always-on-server-20202023&quot;&gt;The i7-4770K as Always-On Server (2020–2023)&lt;/h2&gt;
&lt;p&gt;In 2020, with my first salary out of university, I bought a new gaming desktop - Ryzen 9 5900X, RTX 2060 Super - and “retired” the i7-4770K. Retired meaning “moved to a corner with the drives still attached and never turned off again.” First time the home server became a separate machine from my gaming PC.&lt;/p&gt;
&lt;p&gt;The stack matured a bit. Sonarr and Radarr came online (or maybe before, not entirely sure when I brought these on); access to a couple of decent private trackers and a few reliable public ones meant new media just showed up without me babysitting it. Parsec gave me remote access. Overseerr arrived later and took requests off my hands - friends and family could ask for content directly (I did share *arr access for a bit before this).&lt;/p&gt;
&lt;p&gt;This is also when the user count grew past family, and transcoding got real. Three concurrent remote streams on the 4770K’s iGPU was the limit. I never wanted to disable transcoding entirely (some people had genuinely bad upstream connections), but the 4770K just didn’t have the headroom.&lt;/p&gt;
&lt;p&gt;The whole thing was still on Windows. NTFS had a lot of data on it; a clean Linux migration was always “next year.” I knew Storage Spaces would haunt me eventually, but it kept working. Should I have just shipped the whole library to S3 and dealt with the migration cold? Probably. But moving 8 TB of Linux ISOs to a cloud bucket on a non-fiber upload is a project of its own (to be fair, we did eventually get fiber a year or two later).&lt;/p&gt;
&lt;p&gt;In 2021 I bought my first 18 TB drive. Single drive, no redundancy, “I’ll figure out RAID later.” That made the eventual migration even more daunting with even more Linux ISOs to migrate!&lt;/p&gt;
&lt;h2 id=&quot;the-2023-reset&quot;&gt;The 2023 Reset&lt;/h2&gt;
&lt;p&gt;By 2023 the smell was real. I wanted out of Windows, I wanted real ZFS, and I wanted a server that wasn’t living in fear of a Windows Update at 03:00. I also didn’t want to take the existing setup down while building the new one - there were ~16 people relying on this thing by then, and “your media is broken for two weeks” wasn’t something I wanted to do to anyone.&lt;/p&gt;
&lt;p&gt;So I bought a used PC from a local secondhand site: Ryzen 7 5800X, RTX 3080, 64 GB of RAM. The point of the deal wasn’t the 5800X - it was the GPU and the memory. I swapped both into the 5900X gaming rig (which got a meaningful upgrade for almost nothing), stripped the used box down to CPU + motherboard + chassis, and stood it up next to the running 4770K as the new server. Built the new platform fully, copied data across, swapped people over. User-facing downtime was a few hours.&lt;/p&gt;
&lt;h2 id=&quot;why-truenas-and-not-unraid-or-proxmox&quot;&gt;Why TrueNAS (and Not Unraid or Proxmox)&lt;/h2&gt;
&lt;p&gt;Most homelab content for media servers points at Unraid: mixed disk sizes, single parity, paid license, very forgiving of “I bought one drive at a time over four years.” For someone whose drive collection grew organically - like mine had - Unraid is the obvious answer.&lt;/p&gt;
&lt;p&gt;I went with TrueNAS Community Edition (formerly TrueNAS Scale) for three reasons.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ZFS is first-class.&lt;/strong&gt; Unraid only added real ZFS support recently, and even then its core array layer is XFS-per-disk with parity. ZFS gives me snapshots, scrubs, send/recv, end-to-end checksums, and a pool layer I trust. After a year of running this I’ve caught zero silent bit-rot, but it’s the kind of thing you only know is working because nothing’s gone wrong.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;All same-size disks anyway.&lt;/strong&gt; My organic-growth phase ended in 2021. Going forward I knew I’d buy 18 TB drives until I died - they sit at the sweet spot for €/TB. Once you commit to one drive size, Unraid’s flexibility advantage evaporates and ZFS’s rigidity stops mattering.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The apps platform.&lt;/strong&gt; TrueNAS’s built-in app catalog runs containers as first-class objects on the host, with iXVolumes (ZFS-backed) for app state. I don’t need a separate Proxmox + LXC + manual ZFS layer. One UI, one auth, one upgrade flow.&lt;/p&gt;
&lt;p&gt;If you have ten differently-sized drives accumulated over a decade, get Unraid. If you’re starting fresh and willing to commit to one size, get TrueNAS. I did wipe my old non-18TB drives and sold them, by the way.&lt;/p&gt;
&lt;h2 id=&quot;the-present-518tb-raidz2-30-apps-16-people&quot;&gt;The Present: 5×18TB raidz2, ~30 Apps, 16 People&lt;/h2&gt;
&lt;p&gt;Today the pool is five 18 TB Seagate IronWolf Pros in a single raidz2 vdev: two-disk redundancy, ~40 TiB usable today (currently ~79% full), with about 7 TiB stuck behind a legacy parity layout that I only noticed while writing this post - more on that further down. Single-vdev is fine here because the IOPS-sensitive apps run on a separate pool - ZFS scales IOPS per vdev, so a 5-disk raidz2 has the random-read throughput of roughly one disk, which doesn’t matter for sequential media streaming. Most of the drives came from &lt;a href=&quot;https://serverpartdeals.com&quot;&gt;ServerPartDeals&lt;/a&gt;, a recertified-disk shop that ships enterprise drives at a meaningful discount over new. Worked out at roughly half of new-IronWolf-Pro pricing, the trade-off being that the 5-year warranty runs from manufacture date rather than purchase date.&lt;/p&gt;
&lt;p&gt;The host is on a UPS that easily handles half an hour of operation. Nothing’s port-forwarded anymore: family-facing apps come in through a Cloudflare Tunnel at &lt;code&gt;*.afonsojramos.me&lt;/code&gt; (no exposed home IP, TLS handled at the edge), and everything admin - the *arrs, qBittorrent, dashboards, the lot - stays behind Tailscale. Two access tiers, one running it, the other consuming it. The stack, in rough shape:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;storage: 5×18TB raidz2 plus a separate &lt;code&gt;apps&lt;/code&gt; pool for fast app state&lt;/li&gt;
&lt;li&gt;media: Plex (1,400+ movies, 21 TB of TV, 874 GB of music), Jellyfin alongside it for redundancy (and external downloads, since the Plex Pass only allows Home User downloads), Tautulli for stats, Maintainerr for retention rules&lt;/li&gt;
&lt;li&gt;photos: Immich&lt;/li&gt;
&lt;li&gt;cloud: Seafile, plus Filebrowser and SMB shares for the unencrypted stuff&lt;/li&gt;
&lt;li&gt;auth: Authentik fronts SSO across the public apps and gates the admin tools behind a &lt;code&gt;homelab-admins&lt;/code&gt; group; Wizarr at &lt;code&gt;invite.afonsojramos.me&lt;/code&gt; is the family onboarding door, bootstrapping both Plex and Jellyfin from a single invite&lt;/li&gt;
&lt;li&gt;plumbing: Sonarr, Radarr, Bazarr, Overseerr, qBittorrent, Dockge, Glance, plus a handful of custom Dockge stacks for things the catalog doesn’t ship&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-drive-failure-which-is-why-raidz2&quot;&gt;The Drive Failure (Which Is Why raidz2)&lt;/h2&gt;
&lt;p&gt;A few weeks ago, &lt;a href=&quot;https://github.com/AnalogJ/scrutiny&quot;&gt;Scrutiny&lt;/a&gt; - the SMART dashboard I have watching the pool - lit up with UNC errors against &lt;code&gt;/dev/sdd&lt;/code&gt;, one of the recertified IronWolf Pros (8,682 power-on hours, roughly a year of always-on, 18 TB), and a failed extended self-test soon after. It’s still online, but the pool reports &lt;code&gt;READ=1&lt;/code&gt; against it. The drive is firmly dying.&lt;/p&gt;
&lt;p&gt;This is why raidz2 exists. With raidz1 (single-disk redundancy) you can lose another disk during a 4–6 day resilver window, which is a real risk: large drives mean long resilvers, the resilver itself stresses the remaining disks, and those remaining disks are also a year old and also from the same recertified batch. With raidz2 I can lose this one and another during the resilver and still be fine. The math on a five-disk pool of 18 TB drives wants two parity disks.&lt;/p&gt;
&lt;p&gt;The drive came from ServerPartDeals; the RMA paperwork is in flight. The vdev still has both parity disks intact while I’m waiting on it, so the cost so far is “wait and watch.” And I’ve also ordered a new drive, which I’ll add to the pool once it arrives.&lt;/p&gt;
&lt;h2 id=&quot;what-writing-this-post-turned-up&quot;&gt;What Writing This Post Turned Up&lt;/h2&gt;
&lt;p&gt;When I expanded this pool from 4-wide to 5-wide raidz2 with a &lt;code&gt;zpool attach&lt;/code&gt; almost a year ago, I did the napkin math for the new geometry - 5×18 TB raidz2, ~49 TiB raw, ~44 TiB usable after ZFS overhead - and then never went back to check whether the running pool actually matched. The number sat in my head as fact for almost a year. Writing this section was the first time I’d looked at it again, and it turns out that that is not what I’ve been looking at at all.&lt;/p&gt;
&lt;p&gt;I am currently getting ~40 TiB total, ~4 TiB short of the number I’d been carrying around since the expansion. Turns out that OpenZFS raidz expansion has a footnote I’d glossed over: it only changes the layout for new writes. Records already on disk keep their pre-expansion parity ratio forever. 4-wide raidz2 is 50% parity (2 of 4 disks); 5-wide is 40% (2 of 5). Everything written before the expansion still pays the 4-wide rate, and nothing in &lt;code&gt;zpool&lt;/code&gt; or &lt;code&gt;zfs&lt;/code&gt; will restripe existing records - not scrub, not resilver, not property changes. I had, honestly, assumed that that would be done in the expansion process, but it turns out that the only fix is to rewrite the data so ZFS reallocates it under the current geometry.&lt;/p&gt;
&lt;p&gt;The tool for that is &lt;a href=&quot;https://github.com/markusressel/zfs-inplace-rebalancing&quot;&gt;markusressel/zfs-inplace-rebalancing.sh&lt;/a&gt;: walks the dataset, copies each file to a sibling tempfile (no &lt;code&gt;--reflink&lt;/code&gt;, so it’s a real fresh write), verifies size and checksum, atomic &lt;code&gt;mv&lt;/code&gt; back over the original. Resumable across runs. Napkin math says I should reclaim about 7 TiB on ~36 TB of pre-expansion media - &lt;code&gt;36 × (1 − 40/50) = 7.2&lt;/code&gt; - which would push the pool back past the ~44 TiB I’d always had in my head.&lt;/p&gt;
&lt;p&gt;The catch: it’s a multi-day full-pool I/O storm, and &lt;code&gt;/dev/sdd&lt;/code&gt; is exactly the wrong disk to be running it against. After replacing it I’ll need to resilver, scrub, and only then rebalance.&lt;/p&gt;
&lt;p&gt;The takeaway, if you’re starting fresh: &lt;code&gt;zpool attach&lt;/code&gt; raidz expansion is a one-shot operation you should plan around, not a casual capacity-add. Either commit to a rebalance pass after every expansion, or grow the pool by replacing all five disks one-by-one with larger ones (&lt;code&gt;autoexpand=on&lt;/code&gt;), which keeps the geometry constant and avoids the legacy-tier problem entirely. Given &lt;code&gt;/dev/sdd&lt;/code&gt; is going RMA anyway, the replacement could itself be the first 20 TB drive in a gradual disk-size upgrade - no parity-ratio drift, no rebalance debt, just bigger drives over time.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Edit (29 May 2026):&lt;/strong&gt; I did all of the above, and it worked. The replacement drive turned out to be a Toshiba MG09 18 TB rather than a 20 TB - a same-size swap, but it breaks up the all-Seagate, all-same-batch monoculture, which is good in my books. It was also a drive that had a good reputation in Backblaze’s reports.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;zpool replace&lt;/code&gt; resilvered in 39 hours at full redundancy (online replace - the failing disk stayed in the array the whole time, so parity never dropped to raidz1), a verification scrub came back clean, and then I let the rebalance run. It took &lt;strong&gt;9.5 days&lt;/strong&gt; for ~32 TB - I’d budgeted 3-5, and the estimate was about half what it should’ve been, because the cost is per-file &lt;code&gt;cp&lt;/code&gt;/verify overhead, not throughput: a 30 GB remux and a 2 KB &lt;code&gt;.nfo&lt;/code&gt; each pay the same fixed tax, and the TV library alone is 31,000 files. In total, it reclaimed &lt;strong&gt;5.2 TiB&lt;/strong&gt; (the pool went from 82% to 75% full), a bit under the 7.2 I’d predicted because a chunk of the library was written &lt;em&gt;after&lt;/em&gt; the expansion and was already on the efficient layout. Two gotchas worth flagging if you try this on TrueNAS: the rebalance script’s &lt;code&gt;cp -ax&lt;/code&gt; dies instantly on datasets with &lt;code&gt;aclmode=restricted&lt;/code&gt; (flip it to &lt;code&gt;passthrough&lt;/code&gt; for the run), and any snapshot predating the rebalance pins the old-layout blocks so the pool &lt;em&gt;grows&lt;/em&gt; as you go - I had to drop five stale snapshots mid-run to avoid filling the pool before it could shrink. The honest verdict: the rebalance is the rare homelab chore that delivers exactly what the napkin math promises.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;no-regrets&quot;&gt;No Regrets&lt;/h2&gt;
&lt;p&gt;Looking back, I don’t really have any. Self-hosting in 2016 was a much lonelier hobby than it is in 2026. No &lt;a href=&quot;https://reddit.com/r/selfhosted&quot;&gt;/r/selfhosted&lt;/a&gt; at its current size, no Tailscale, no TrueNAS Community Edition, no recertified-drive economy you could trust at scale, and homelab content on YouTube was a tiny fraction of what it is today. Most of what looks obvious in 2026 had to be figured out from scratch back then, and a lot of the choices that look “wrong” in retrospect were the only realistic option at the time.&lt;/p&gt;
&lt;p&gt;The hurdles were also good for me. Storage Spaces taught me what I wanted from a real filesystem. Running Plex on my gaming PC taught me why isolation matters. The Windows years taught me what I was willing to pay for in operational simplicity. None of that would have landed the same way if I’d been handed today’s tooling on day one.&lt;/p&gt;
&lt;p&gt;A few opinions that have held up, regardless of when you start:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Storage Spaces is fine until it isn’t. Use it, then leave it before your library makes the migration scary.&lt;/li&gt;
&lt;li&gt;Don’t run Plex on your gaming PC. The moment your library has more than a couple of viewers, it deserves its own machine.&lt;/li&gt;
&lt;li&gt;Recertified enterprise drives are great once you trust the supplier. ServerPartDeals’ IronWolf Pros land at roughly half the price of new for the warranty trade-off.&lt;/li&gt;
&lt;li&gt;Pay for Plex Pass when it goes on sale. The 50%-off lifetime promos come around. Best deal in self-hosting. (&lt;strong&gt;Edit (29 May 2026):&lt;/strong&gt; turns out, this was an even better deal than I thought, since from July 2026, &lt;a href=&quot;https://9to5mac.com/2026/05/19/plex-increasing-lifetime-plex-pass-cost-to-whopping-750/&quot;&gt;Plex Pass will be available for 750$&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The thing the homelab guides on YouTube don’t tell you is that it never actually starts as a homelab. It starts as one Plex install on whatever’s nearest, and a few years later you catch yourself reading about ECC memory on a Sunday afternoon.&lt;/p&gt;</content:encoded></item><item><title>blockrate</title><link>https://afonsojramos.me/projects/blockrate/</link><guid isPermaLink="true">https://afonsojramos.me/projects/blockrate/</guid><description>Per-provider ad-blocker block-rate measurement: a 1.6 KB OSS client, a self-hostable ingestion server, and a hosted dashboard.</description><pubDate>Wed, 22 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Existing “ad block detectors” tell you whether &lt;em&gt;a&lt;/em&gt; blocker exists on the visitor’s machine. They don’t tell you &lt;em&gt;which third-party tools you actually rely on are blocked.&lt;/em&gt; Optimizely and PostHog have very different block rates. So do GA4 and Segment. Knowing the per-provider number is the difference between “ignore it” and “we should reverse-proxy this one server-side.”&lt;/p&gt;
&lt;p&gt;&lt;code&gt;blockrate&lt;/code&gt; measures it. Three tiers depending on how much infrastructure you want to run:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OSS library&lt;/strong&gt; (1.6 KB, zero deps) - drop into any web app, point at any reporter. First-party origin only, by design - third-party reporter URLs are themselves blockable, defeating the purpose. The core README explains why this is load-bearing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Self-hosted server&lt;/strong&gt; - &lt;code&gt;bunx blockrate-server&lt;/code&gt; and you have an ingestion server with SQLite by default (Postgres optional via Drizzle), a built-in dashboard, and an API key. One binary, one command.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hosted version&lt;/strong&gt; (&lt;a href=&quot;https://blockrate.app&quot;&gt;blockrate.app&lt;/a&gt;) - sign in, get a key, see per-provider rates. For people who don’t want to operate any of the above.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Built on Bun + Drizzle for the server, TanStack Start + Better Auth for the dashboard. Framework adapters for Next.js, TanStack Start, SvelteKit, Nuxt, SolidStart, and vanilla JS in the box. Source at &lt;a href=&quot;https://github.com/afonsojramos/blockrate&quot;&gt;github.com/afonsojramos/blockrate&lt;/a&gt;.&lt;/p&gt;</content:encoded></item><item><title>slidev-decks</title><link>https://afonsojramos.me/projects/slidev-decks/</link><guid isPermaLink="true">https://afonsojramos.me/projects/slidev-decks/</guid><description>A lean, zero-config CLI for managing multiple Slidev presentations in one repo - interactive picker, fuzzy match, incremental builds, GitHub Pages deploy.</description><pubDate>Wed, 15 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://sli.dev&quot;&gt;Slidev&lt;/a&gt; is a beautiful slide framework, but it has a single-presentation mental model: one repo, one &lt;code&gt;slides.md&lt;/code&gt;. If you give a few talks a year and want to keep them all in one place - with shared themes, easy diffing, GitHub Pages deploys - you end up writing the same scaffolding scripts every time. &lt;code&gt;slidev-decks&lt;/code&gt; is that scaffolding extracted into a small CLI.&lt;/p&gt;
&lt;p&gt;It is also, in 2026, one of the best slide tools for the AI era. The source is plain Markdown - an agent can read, edit, or generate a deck end-to-end the same way it edits any other repo, with diffs that review like code and no UI to automate against. Outline a talk in chat, hand it the file, and the slides write themselves. Try doing that with Keynote.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/og.CPi2T8Lm_lvlOp.webp&quot; alt=&quot;slidev-decks interactive picker&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sd&lt;/code&gt; is the short alias. &lt;code&gt;sd&lt;/code&gt; alone opens an interactive picker; &lt;code&gt;sd ai&lt;/code&gt; fuzzy-matches against folder names and frontmatter titles to jump straight to the deck; &lt;code&gt;sd new&lt;/code&gt; walks you through creating one (templates use &lt;code&gt;{{TITLE}}&lt;/code&gt; / &lt;code&gt;{{AUTHOR}}&lt;/code&gt; / &lt;code&gt;{{YEAR}}&lt;/code&gt; placeholders); &lt;code&gt;sd build --all&lt;/code&gt; rebuilds everything and skips decks whose source hasn’t changed since last build; &lt;code&gt;sd export ai --dark&lt;/code&gt; produces PDFs. Any flag it doesn’t recognise gets forwarded to Slidev untouched, so nothing is taken away.&lt;/p&gt;
&lt;p&gt;Detects your package manager from the lockfile. Reads &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;author&lt;/code&gt; from each deck’s frontmatter for the picker. Includes a reusable composite GitHub Action for building and deploying every deck under a custom base path. Published on npm as &lt;a href=&quot;https://www.npmjs.com/package/slidev-decks&quot;&gt;&lt;code&gt;slidev-decks&lt;/code&gt;&lt;/a&gt;; source at &lt;a href=&quot;https://github.com/afonsojramos/slidev-decks&quot;&gt;github.com/afonsojramos/slidev-decks&lt;/a&gt;.&lt;/p&gt;</content:encoded></item><item><title>qbz</title><link>https://afonsojramos.me/projects/qbz/</link><guid isPermaLink="true">https://afonsojramos.me/projects/qbz/</guid><description>macOS support for qbz, a native, bit-perfect hi-fi Qobuz desktop player written in Rust on Tauri.</description><pubDate>Sat, 04 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://qbz.lol&quot;&gt;qbz&lt;/a&gt; is a native, bit-perfect hi-fi player for &lt;a href=&quot;https://www.qobuz.com&quot;&gt;Qobuz&lt;/a&gt;, built in &lt;strong&gt;Rust&lt;/strong&gt; on top of &lt;strong&gt;Tauri&lt;/strong&gt;. It started life as a Linux-only app. I brought it to &lt;strong&gt;macOS&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/qbz-home.BFPOdOF9_Z1vYEAJ.webp&quot; alt=&quot;qbz home screen running on macOS&quot;&gt;&lt;/p&gt;
&lt;p&gt;Most of the work was the kind you only notice when it is wrong. Real &lt;strong&gt;CoreAudio&lt;/strong&gt; output with device probing and sample-rate switching, so playback stays bit-perfect end to end. Hunting down a track-change crackle that turned out to be a buffer handover bug. Deep links for &lt;code&gt;qobuzapp://&lt;/code&gt;, native notifications with album artwork, &lt;code&gt;x86_64&lt;/code&gt; cross-compilation for Intel Macs, and ad-hoc signing so Gatekeeper stops rejecting the bundle. Each one is small on its own; together they are the difference between “compiles on macOS” and “feels native on macOS”.&lt;/p&gt;
&lt;p&gt;Some of it pushed one layer down, into the crates qbz is built on. macOS sample-rate switching needed device-rate enumeration that &lt;a href=&quot;https://github.com/RustAudio/coreaudio-rs&quot;&gt;&lt;code&gt;coreaudio-rs&lt;/code&gt;&lt;/a&gt; did not expose, so I &lt;a href=&quot;https://github.com/RustAudio/coreaudio-rs/pull/149&quot;&gt;added it there&lt;/a&gt;. Album artwork in notifications needed &lt;a href=&quot;https://github.com/hoodie/notify-rust&quot;&gt;&lt;code&gt;notify-rust&lt;/code&gt;&lt;/a&gt; to accept an image path, so I &lt;a href=&quot;https://github.com/hoodie/notify-rust/pull/264&quot;&gt;added that too&lt;/a&gt;. For a while qbz ran on my forks of both; once the changes were released, it went back to the published crates.&lt;/p&gt;
&lt;p&gt;Almost all of the app-level work landed &lt;strong&gt;upstream&lt;/strong&gt; in &lt;a href=&quot;https://github.com/vicrodh/qbz&quot;&gt;vicrodh/qbz&lt;/a&gt; across a few dozen merged pull requests, starting with &lt;a href=&quot;https://github.com/vicrodh/qbz/pull/181&quot;&gt;the initial macOS support PR&lt;/a&gt;. Working in someone else’s audio codebase, in Rust, on a platform it was never written for, was a good reminder of how much careful work sits underneath the word “port”.&lt;/p&gt;</content:encoded></item><item><title>The Abstraction Imperative: Why Framework-Defined Infrastructure is the Next Step in Software Evolution</title><link>https://afonsojramos.me/blog/framework-defined-infrastructure/</link><guid isPermaLink="true">https://afonsojramos.me/blog/framework-defined-infrastructure/</guid><description>Every major leap in software development has followed the same pattern: we abstract away complexity that no longer needs manual attention. Framework-defined infrastructure isn&apos;t just another deployment strategy, it&apos;s the continuation of a decades-long trajectory toward letting developers focus on what actually matters.</description><pubDate>Sat, 22 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every major leap in software development has followed the same pattern: we abstract away complexity that no longer needs manual attention. &lt;a href=&quot;https://vercel.com/blog/framework-defined-infrastructure&quot;&gt;Framework-defined infrastructure&lt;/a&gt; is not merely another deployment strategy; it is the logical continuation of a decades-long trajectory allowing developers to focus on the problem, not the plumbing. At no point in this blog post will I argue that we don’t need plumbers. Instead, I will argue that we should worry less about plumbing by trusting well-tested abstractions that others have carefully refined over time.&lt;/p&gt;
&lt;h2 id=&quot;the-history-of-software-is-the-history-of-abstraction&quot;&gt;The History of Software is the History of Abstraction&lt;/h2&gt;
&lt;p&gt;In the 1950s, programmers wrote in machine code, toggling switches to represent binary instructions. Assembly language abstracted machine code. High-level languages like C abstracted assembly. Garbage collection abstracted memory management. Virtual machines abstracted operating systems. Cloud computing abstracted physical hardware.&lt;/p&gt;
&lt;p&gt;Each abstraction faced resistance. &lt;em&gt;“Real programmers”&lt;/em&gt; supposedly needed direct memory access. Garbage collection was &lt;em&gt;“too slow”&lt;/em&gt; for serious applications. Virtual machines added &lt;em&gt;“unnecessary overhead.”&lt;/em&gt; Cloud computing meant &lt;em&gt;“losing control.”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In each case, the abstraction became dominant, though not universal. Not because it was perfect, but because it let most developers solve most problems at a higher level. Assembly still exists for embedded systems, manual memory management remains critical for certain performance-sensitive applications, and some organizations still run their own data centers. The question was never whether to abstract completely, but when the abstraction became &lt;strong&gt;good enough&lt;/strong&gt; to trust &lt;strong&gt;for the majority of use cases&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id=&quot;where-we-are-now-the-infrastructure-abstraction-gap&quot;&gt;Where We Are Now: The Infrastructure Abstraction Gap&lt;/h2&gt;
&lt;p&gt;Modern application development has reached an inflection point. We’ve abstracted nearly everything about software except how we deploy it.&lt;/p&gt;
&lt;p&gt;The move to web applications itself represents a massive abstraction, instead of writing platform-specific native code for Windows, macOS, Linux, iOS, and Android, developers write once for the browser and let the browser handle platform differences. APIs and microservices abstracted monolithic architectures. Containerization abstracted runtime environments. The pattern is consistent: each generation removes another layer of manual platform-specific work.&lt;/p&gt;
&lt;p&gt;Yet deployment remains stubbornly manual. We’ve moved beyond FTPing files to servers, yes, but consider a typical web application today. Developers write code using framework conventions, routes, components, server functions, middleware. But when deployment time comes, they must translate these high-level concepts into low-level infrastructure primitives: Lambda functions, API Gateways, load balancers, CDN configurations, caching layers.&lt;/p&gt;
&lt;p&gt;This translation layer is where things break down. As the evolution of Platform as a Service (PaaS) has shown, &lt;a href=&quot;https://dzone.com/articles/platform-as-a-service-paas-origins-and-architectur&quot;&gt;the progression from bare metal to virtualized infrastructure represents “the evolutionary nature” of modern software&lt;/a&gt;. Yet even with traditional PaaS offerings, developers still face somewhat of a gap between application code and infra configuration.&lt;/p&gt;
&lt;p&gt;With traditional Infrastructure as Code (IaC) approaches, you write a server-side rendering function in your framework, then spend hours configuring the serverless function, its memory limits, timeout settings, IAM roles, and API Gateway integration. The gap between framework concept and infrastructure primitive remains stubbornly manual. Terraform fatigue is real.&lt;/p&gt;
&lt;h2 id=&quot;framework-defined-infrastructure-closes-the-gap&quot;&gt;Framework-Defined Infrastructure Closes the Gap&lt;/h2&gt;
&lt;p&gt;Framework-defined infrastructure eliminates this translation layer by making the framework itself the interface for infrastructure decisions. As &lt;a href=&quot;https://vercel.com/blog/framework-defined-infrastructure&quot;&gt;Vercel describes it&lt;/a&gt;, &lt;em&gt;“the deployment environment automatically provisions infrastructure derived from the framework and the applications written in it.”&lt;/em&gt; The platform reads your framework code and provisions infrastructure automatically based on what it understands your code needs.&lt;/p&gt;
&lt;p&gt;This isn’t an entirely new concept. In fact, the roots trace back to 2006 when Zimki introduced what Simon Wardley called &lt;a href=&quot;https://en.wikipedia.org/wiki/Platform_as_a_service&quot;&gt;“framework-as-a-service”&lt;/a&gt;, a fascinating early attempt that could have made &lt;a href=&quot;https://www.porter.run/blog/history-of-paas-how-canon-almost-became-a-major-cloud-provider&quot;&gt;Canon one of the first major cloud providers&lt;/a&gt; before the industry settled on the term Platform as a Service. Though Zimki ultimately closed, &lt;a href=&quot;https://www.heroku.com/about/&quot;&gt;Heroku launched in 2007&lt;/a&gt; and successfully commercialized this vision with their mantra that developers should “focus on what they do best: building great apps” while the platform handles infrastructure. Heroku’s architecture was designed to &lt;a href=&quot;https://www.heroku.com/about/&quot;&gt;remove obstacles so developers can focus on building&lt;/a&gt; rather than managing servers.&lt;/p&gt;
&lt;p&gt;Similarly, Netlify popularized the JAMstack architecture, a term &lt;a href=&quot;https://www.netlify.com/jamstack/&quot;&gt;coined by CEO Mathias Biilmann&lt;/a&gt;, which emphasizes prerendering content and deploying directly to the edge. As Netlify explains, their approach is fundamentally about &lt;a href=&quot;https://www.netlify.com/jamstack/&quot;&gt;“abstraction and simplicity”&lt;/a&gt;, where build automation understands framework patterns and automatically optimizes deployments.&lt;/p&gt;
&lt;p&gt;The framework conventions themselves become infrastructure declarations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When you export &lt;code&gt;getServerSideProps&lt;/code&gt; in Next.js, you’re declaring that route needs server-side compute&lt;/li&gt;
&lt;li&gt;When you set &lt;code&gt;output: &apos;server&apos;&lt;/code&gt; in Astro, you’re declaring the entire site needs SSR capabilities&lt;/li&gt;
&lt;li&gt;When you create a &lt;code&gt;+page.server.ts&lt;/code&gt; in SvelteKit, you’re declaring that route requires server-side data loading&lt;/li&gt;
&lt;li&gt;When you use &lt;code&gt;export const prerender = false&lt;/code&gt; in SolidStart, you’re opting that route into dynamic rendering&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The framework has already made the infrastructure decision, it just needs a platform that understands it.&lt;/strong&gt; This isn’t magic. It’s pattern recognition applied to infrastructure. Frameworks impose structure and conventions specifically to make code predictable and understandable. Framework-defined infrastructure leverages that predictability to automate infrastructure provisioning the same way frameworks automate application structure.&lt;/p&gt;
&lt;h2 id=&quot;why-this-matters-beyond-any-single-platform&quot;&gt;Why This Matters Beyond Any Single Platform&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The power of framework-defined infrastructure isn’t tied to any particular deployment platform.&lt;/strong&gt; Multiple platforms, Vercel, Netlify, Cloudflare Pages, AWS Amplify, Azure Static Web Apps, have independently converged on the same insight: let the framework define infrastructure needs. Though implementations vary in how much configurability they expose, the core principle remains consistent.&lt;/p&gt;
&lt;p&gt;This convergence validates the abstraction. Just as multiple operating systems implement virtual memory and multiple languages implement garbage collection, multiple platforms implementing framework-defined infrastructure proves the concept has staying power.&lt;/p&gt;
&lt;p&gt;As Vercel notes in their piece on &lt;a href=&quot;https://vercel.com/blog/vercel-the-anti-vendor-lock-in-cloud&quot;&gt;avoiding vendor lock-in&lt;/a&gt;, “Framework conventions like the Next.js App Router, Remix loaders, SvelteKit endpoints, and Nitro storage adapters work differently. You build against the framework, not the platform. Multiple platforms can run the same framework code, keeping your application portable across any infrastructure that supports it.”&lt;/p&gt;
&lt;p&gt;The competitive advantage shifts from proprietary primitives to better implementations of the same abstraction. Platforms compete on performance, developer experience, observability, and pricing, not on lock-in. In fact, approximately 70% of Next.js applications run outside of Vercel, with companies like Walmart, Nike, and Claude.ai self-hosting at massive scale.&lt;/p&gt;
&lt;p&gt;There are legitimate concerns about vendor lock-in when platforms handle multiple aspects of your stack. As one developer &lt;a href=&quot;https://gomakethings.com/the-challenge-with-netlify-vercel-cloudflare-and-so-on/&quot;&gt;notes&lt;/a&gt;, “when one provider handles your hosting, manages automated deployment, runs a few dozen micro-services and serverless APIs, provides your database through their proprietary API, and more, migrating away to somewhere else becomes very expensive.” But framework-defined infrastructure actually addresses this concern by keeping the abstraction layer in the open-source framework, not in proprietary platform APIs.&lt;/p&gt;
&lt;h2 id=&quot;the-developer-experience-transformation&quot;&gt;The Developer Experience Transformation&lt;/h2&gt;
&lt;p&gt;The real benefit surfaces in daily development work. With framework-defined infrastructure, your local development environment matches production behavior because both run the same framework code. There’s no simulation layer, no mocked services, no “works on my machine” debugging.&lt;/p&gt;
&lt;p&gt;As Vercel explains, platforms built on vendor primitives need complex simulators for local development, Cloudflare provides Wrangler to simulate Workers locally, AWS developers use LocalStack or SAM CLI to mock Lambda and other services. These tools approximate production behavior but never match it exactly.&lt;/p&gt;
&lt;p&gt;This echoes Heroku’s original insight: by &lt;a href=&quot;https://www.porter.run/blog/what-is-heroku&quot;&gt;abstracting away the underlying infrastructure&lt;/a&gt;, developers “don’t have to worry about anything except for application logic.” With framework-defined infrastructure, you test your application by running &lt;code&gt;next dev&lt;/code&gt; or &lt;code&gt;remix dev&lt;/code&gt;, the standard framework tooling. What you see locally is what runs in production because the framework, not the platform, defines the behavior.&lt;/p&gt;
&lt;p&gt;This eliminates an entire class of problems. You don’t need to maintain separate local development simulators like LocalStack or Wrangler. You don’t need to keep IaC configurations in sync with application changes. You don’t need platform-specific tooling that only half-works on your operating system.&lt;/p&gt;
&lt;p&gt;Tools like &lt;a href=&quot;https://sst.dev/&quot;&gt;SST&lt;/a&gt; represent an interesting middle ground—they use framework conventions to &lt;em&gt;generate&lt;/em&gt; IaC (CloudFormation/Terraform) rather than bypassing it entirely. This hybrid approach still requires maintaining infrastructure code, but reduces the manual translation burden by inferring infrastructure needs from your application code.&lt;/p&gt;
&lt;h2 id=&quot;abstraction-enables-specialization&quot;&gt;Abstraction Enables Specialization&lt;/h2&gt;
&lt;p&gt;Critics worry that abstraction means losing control. But abstraction doesn’t eliminate control, it enables specialization.&lt;/p&gt;
&lt;p&gt;When you write a Next.js application, you’re not locked out of infrastructure. You can still deploy to EC2 and manage every detail if needed. Companies like Walmart and Nike do exactly this at massive scale. The difference is that you can also choose not to, letting platforms that specialize in infrastructure handle those concerns while you specialize in your product.&lt;/p&gt;
&lt;p&gt;This is how every successful abstraction works. You can still write assembly if you need to, but most developers rightfully choose to write in higher-level languages and let the compiler handle optimization. You can still manage memory manually, but most developers let garbage collection handle it. You can still provision servers, but most developers let cloud platforms handle it.&lt;/p&gt;
&lt;p&gt;As the evolution of cloud computing shows, &lt;a href=&quot;https://cloud.google.com/learn/paas-vs-iaas-vs-saas&quot;&gt;PaaS sits between IaaS and SaaS&lt;/a&gt; specifically to “abstract infrastructure complexities, allowing developers to focus on building and innovating.” Framework-defined infrastructure extends this progression. You can still configure infrastructure manually if you need to, but most developers can let the framework and platform handle it automatically.&lt;/p&gt;
&lt;h2 id=&quot;the-economics-of-abstraction&quot;&gt;The Economics of Abstraction&lt;/h2&gt;
&lt;p&gt;Abstractions succeed when the economic value of simplification exceeds the cost of flexibility lost. Framework-defined infrastructure passes this test decisively.&lt;/p&gt;
&lt;p&gt;Consider the engineering cost of a typical deployment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Writing and maintaining IaC configurations&lt;/li&gt;
&lt;li&gt;Keeping infrastructure in sync with application changes&lt;/li&gt;
&lt;li&gt;Managing environment differences between development and production&lt;/li&gt;
&lt;li&gt;Training new team members on platform-specific tooling&lt;/li&gt;
&lt;li&gt;Debugging issues caused by misconfiguration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now consider the alternative: writing framework code that automatically deploys correctly. As Vercel found when studying &lt;a href=&quot;https://vercel.com/blog/accelerating-developer-velocity-and-creating-high-impact-web-teams&quot;&gt;high-impact web teams&lt;/a&gt;, “when developers control their entire workflow, teams previously balancing both hardware and software can focus solely on the strategic aspects of product delivery. Conversations shift from ‘how’ and ‘if’ to ‘what’ and ‘when,’ as infrastructure is no longer an obstacle to launching products.”&lt;/p&gt;
&lt;p&gt;The reduction in cognitive load, time spent, and error rate represents genuine economic value. A Forrester Total Economic Impact report found that teams using framework-defined infrastructure saw higher customer conversion rates generating $2.6 million in incremental profits and higher website traffic generating $7.7 million in incremental profits.&lt;/p&gt;
&lt;p&gt;Netlify’s experience with the JAMstack architecture demonstrates similar benefits. By &lt;a href=&quot;https://www.netlify.com/blog/2020/04/01/automate-your-web-workflows-with-the-jamstack/&quot;&gt;automating the build and deploy process&lt;/a&gt;, platforms ensure “code can be shipped with little to no opportunity for error,” while the architecture itself is “designed to scale only when needed” rather than requiring premature optimization.&lt;/p&gt;
&lt;p&gt;Some applications have special requirements that framework-defined infrastructure doesn’t handle well. That’s fine. Those applications can opt out and configure infrastructure manually. But for the majority of web applications, the standard patterns that frameworks encode work perfectly, and automating their infrastructure deployment makes obvious economic sense.&lt;/p&gt;
&lt;h2 id=&quot;what-this-means-for-the-industry&quot;&gt;What This Means for the Industry&lt;/h2&gt;
&lt;p&gt;Framework-defined infrastructure represents a maturation of the platform-as-a-service model. Early PaaS offerings like Heroku abstracted infrastructure but required you to build against their specific deployment model. Modern framework-defined infrastructure abstracts infrastructure while letting you build against open source frameworks.&lt;/p&gt;
&lt;p&gt;This distinction matters. When the abstraction layer is proprietary, adoption requires trust in a single vendor. When the abstraction layer is an open source framework, adoption requires trust in the broader ecosystem. The latter scales better.&lt;/p&gt;
&lt;p&gt;This is reflected in what Vercel calls their &lt;a href=&quot;https://vercel.com/blog/open-sdk-strategy&quot;&gt;Open SDK strategy&lt;/a&gt;: “We want what we build to work extremely well on Vercel, but not at the cost of lock-in… We will build first on Vercel, where we can iterate fastest to ensure the best end-to-end developer and user experience. And as we become confident projects are mature, we’ll invest in ensuring our SDKs and tools are deployable to any platform.”&lt;/p&gt;
&lt;p&gt;Similarly, Netlify and Cloudflare have &lt;a href=&quot;https://www.netlify.com/blog/supporting-an-open-web-with-netlify-cloudflare/&quot;&gt;come together to support open frameworks&lt;/a&gt; like TanStack and Astro, recognizing that “a rising tide lifts all boats.” As they note, “the strongest projects emerge when multiple organizations align behind shared goals, giving developers the broadest opportunities for success.”&lt;/p&gt;
&lt;p&gt;We should expect framework-defined infrastructure to become the default deployment model for framework-based applications, just as cloud computing became the default infrastructure model for most applications. Some organizations will continue self-hosting and managing infrastructure directly, just as some organizations still run their own data centers. But the center of gravity will shift.&lt;/p&gt;
&lt;h2 id=&quot;the-evolution-continues&quot;&gt;The Evolution Continues&lt;/h2&gt;
&lt;p&gt;The pattern is already extending beyond basic deployment. Vercel’s recent introduction of their &lt;a href=&quot;https://vercel.com/blog/introducing-workflow&quot;&gt;Workflow Development Kit&lt;/a&gt; shows how framework-defined infrastructure concepts apply to durability and long-running processes: “Functions can pause for minutes or months, survive deployments and crashes, and resume exactly where they stopped.” Rather than manually configuring message queues and retry logic, developers write standard async functions with durability directives, and &lt;a href=&quot;https://vercel.com/blog/introducing-workflow&quot;&gt;the platform handles persistence automatically&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Similarly, their &lt;a href=&quot;https://vercel.com/blog/self-driving-infrastructure&quot;&gt;self-driving infrastructure&lt;/a&gt; initiative explores how production data can inform infrastructure optimization: “Code defines infrastructure, production informs code, and infrastructure adapts automatically.” This closes the loop between application behavior and resource allocation without requiring manual intervention.&lt;/p&gt;
&lt;p&gt;Vercel’s approach to &lt;a href=&quot;https://vercel.com/blog/life-of-a-request-application-aware-routing&quot;&gt;application-aware routing&lt;/a&gt; demonstrates how deeply framework understanding can integrate with infrastructure: “Vercel uses your framework code to define infrastructure and how requests are handled at runtime. This tight integration gives the platform full visibility into your app’s structure: routes, layouts, rewrites, middleware, functions, static assets, and routing logic.”&lt;/p&gt;
&lt;h2 id=&quot;conclusion-abstraction-is-inevitable&quot;&gt;Conclusion: Abstraction is Inevitable&lt;/h2&gt;
&lt;p&gt;Software development has always moved toward better abstractions. We abstract when the benefits of simplicity outweigh the costs of lost control, when patterns emerge that are stable enough to codify, when the manual work no longer creates competitive advantage.&lt;/p&gt;
&lt;p&gt;Framework-defined infrastructure meets all these criteria. The patterns are stable, server-side rendering, static generation, API routes, and edge middleware aren’t going anywhere. The manual work, translating framework concepts to infrastructure primitives, creates no competitive advantage. The simplification, automatic infrastructure from framework code, delivers real value.&lt;/p&gt;
&lt;p&gt;As &lt;a href=&quot;https://blog.heroku.com/modern-web-app-architecture&quot;&gt;Heroku’s evolution demonstrates&lt;/a&gt;, “ever-increasing complexity means developers need to build and operate more adaptable systems.” Framework-defined infrastructure represents exactly this kind of adaptability: composing infrastructure from framework patterns rather than manual configuration.&lt;/p&gt;
&lt;p&gt;The question isn’t whether framework-defined infrastructure will become standard practice, but how quickly it will happen. Given that multiple platforms have independently adopted similar approaches, given that major companies successfully deploy framework-based applications across different infrastructure models, and given the clear economic benefits, the trajectory seems clear.&lt;/p&gt;
&lt;p&gt;Abstractions that work become invisible. In a few years, explaining that you used to manually configure infrastructure for every framework route will sound as strange as explaining that you used to manually manage memory allocation. We’ll wonder why we ever did it differently.&lt;/p&gt;</content:encoded></item><item><title>How to Sync Apple Reminders with Android (When You&apos;re Hooked on the Menubar)</title><link>https://afonsojramos.me/blog/todo-sync-with-apple-reminders-and-outlook/</link><guid isPermaLink="true">https://afonsojramos.me/blog/todo-sync-with-apple-reminders-and-outlook/</guid><description>Learn how to sync Apple Reminders with Android using Microsoft as a bridge solution. Perfect for users committed to reminders-menubar on Mac who need occasional Android access. Includes setup steps, limitations, and honest assessment of sync delays and notification quirks.</description><pubDate>Tue, 23 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you’re like me and have discovered &lt;a href=&quot;https://github.com/DamascenoRafael/reminders-menubar&quot;&gt;&lt;code&gt;reminders-menubar&lt;/code&gt;&lt;/a&gt;, you know exactly why leaving Apple Reminders isn’t an option. This open-source menubar app transforms Apple’s basic reminder system into something genuinely great - instant access with a keyboard shortcut, natural language input, and that satisfying feeling of capturing tasks without breaking your flow. And I’ve looked at other options, but nothing else comes close to the seamlessness of reminders-menubar on macOS, even if you’re willing to pay (and I’m not).&lt;/p&gt;
&lt;p&gt;The problem? I also use an Android phone. And Apple Reminders doesn’t exactly play nice outside the walled garden.&lt;/p&gt;
&lt;p&gt;So here’s my situation: I’m completely sold on the workflow that reminders-menubar provides on my Mac. It’s become part of how I think about task management - quick capture, always visible and with number that I hold myself accountable to. But when I pick up my Android phone, those reminders might as well not exist, or I might need to mimic them in another app - which was Google Keep for a while, but I kept forgetting to check it.&lt;/p&gt;
&lt;p&gt;Fair warning, the solution I’m about to share isn’t perfect, as it does involve having a middleman, Microsoft, but after you try the &lt;code&gt;reminders-menubar&lt;/code&gt; workflow, I’m pretty confident you’ll embrace it, like I have, as this is currently your best option when needing Android access.&lt;/p&gt;
&lt;h2 id=&quot;why-reminders-menubar-changes-everything&quot;&gt;Why Reminders-Menubar Changes Everything&lt;/h2&gt;
&lt;p&gt;But before we dive into the Android sync solution, let me explain why I’m going through this trouble instead of just switching to multi-platform tool such as Todoist or TickTick.&lt;/p&gt;
&lt;p&gt;Reminders-menubar is a lightweight menubar Mac app that expands on Apple Reminders by gives you instant access to it through the menubar, or a quick keyboard shortcut. No opening apps, no clicking through menus, no friction.&lt;/p&gt;
&lt;p&gt;The magic is that it uses Apple’s native EventKit framework, so everything syncs perfectly with iCloud. Your reminders appear instantly on your iPhone, iPad, and Apple Watch (even if in my case I only have my Mac). At the end of the day, it’s free, it’s fast, and it just works. Which is exactly why I don’t want to give it up just so I can have the same ToDos on my phone. I tend to avoid paid subscriptions when free alternatives exist, even if they aren’t quite as polished and require a bit more tinkering.&lt;/p&gt;
&lt;h2 id=&quot;the-microsoft-bridge-solution&quot;&gt;The Microsoft Bridge Solution&lt;/h2&gt;
&lt;p&gt;Here’s the workaround: it is actually quite simple, we’re going to use Microsoft’s ecosystem to bridge Apple and Android. Microsoft Outlook can sync with Apple Reminders on iOS, and Microsoft To Do can access those same reminders on Android. Plus Microsoft To Do has a pretty nice widget.&lt;/p&gt;
&lt;h2 id=&quot;setting-it-up&quot;&gt;Setting It Up&lt;/h2&gt;
&lt;p&gt;The setup is surprisingly straightforward, even if the underlying architecture feels a bit convoluted.&lt;/p&gt;
&lt;h4 id=&quot;macos-setup&quot;&gt;macOS Setup&lt;/h4&gt;
&lt;p&gt;First, we need to configure your Mac to sync Apple Reminders with Microsoft:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open macOS System Settings → Internet Accounts&lt;/li&gt;
&lt;li&gt;Add your Microsoft account if it’s not already there&lt;/li&gt;
&lt;li&gt;Make sure “Reminders” is checked in the account services&lt;/li&gt;
&lt;li&gt;That’s it! Nothing else to configure here.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;ios-setup&quot;&gt;iOS Setup&lt;/h4&gt;
&lt;p&gt;On your iPhone or iPad:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open iOS Settings → Apps → Reminders&lt;/li&gt;
&lt;li&gt;Tap Reminders Accounts → Add Account&lt;/li&gt;
&lt;li&gt;Add your Microsoft Outlook account&lt;/li&gt;
&lt;li&gt;Ensure the “Reminders” toggle is ON&lt;/li&gt;
&lt;li&gt;That’s it! Nothing else to configure here.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This creates the bridge so that reminders created through &lt;code&gt;reminders-menubar&lt;/code&gt; (or the native Reminders app) sync to Microsoft’s servers.&lt;/p&gt;
&lt;h4 id=&quot;on-your-android-device&quot;&gt;On Your Android Device&lt;/h4&gt;
&lt;p&gt;Install Microsoft To Do from the Play Store and sign in with the same Microsoft account. That’s it. Your reminders should start appearing shortly. And since you’re using Microsoft To Do, you can actually use other platforms that integrate with it, such as Samsung’s native Reminder app.&lt;/p&gt;
&lt;h2 id=&quot;the-only-downside-is-actually-an-upside&quot;&gt;The Only Downside Is Actually An Upside&lt;/h2&gt;
&lt;p&gt;Here’s the one quirk you should know about: when a reminder’s alarm fires, it rings on BOTH devices. And dismissing it on one device doesn’t dismiss it on the other.&lt;/p&gt;
&lt;p&gt;Set a reminder for 3 PM, and at 3 PM both your iPhone and Android buzz. Dismiss it on Android, and your iPhone keeps nagging. This is particularly noticeable with recurring reminders (not that I have daily reminders that bug me on multiple devices simultaneously 🫣, but still).&lt;/p&gt;
&lt;p&gt;This happens because each platform maintains its own notification state. The reminder data syncs perfectly, but the “I’ve handled this” status doesn’t.&lt;/p&gt;
&lt;p&gt;But here’s the thing - I’ve actually started to appreciate this “bug” as a feature. It’s pretty hard to miss an important reminder when two devices are insisting you pay attention to it. Now, I hear you, notifications can be annoying. But for truly important deadlines or time-sensitive tasks, having that redundant notification system isn’t the worst thing in the world - we’re talking about reminders, after all.&lt;/p&gt;
&lt;h2 id=&quot;the-bigger-picture&quot;&gt;The Bigger Picture&lt;/h2&gt;
&lt;p&gt;What strikes me about this whole situation is how it highlights the state of cross-platform compatibility in 2025. We’re still using workarounds and compromises to make basic productivity tools work across ecosystems.&lt;/p&gt;
&lt;p&gt;Apple’s walled garden provides genuine benefits - the integration really is excellent when you stay inside it. But it punishes flexibility. The fact that a free, open-source Mac app (reminders-menubar) can make Apple’s basic reminder system more compelling than premium cross-platform alternatives says something about the value of deep platform integration, as well as the imense capabilities of open-source software.&lt;/p&gt;
&lt;p&gt;Microsoft, ironically, has positioned themselves as the peace broker by building quality apps on both platforms and enabling sync between them. For now, if you need Apple Reminders on Android like me - especially if reminders-menubar is part of why you’re committed to Apple’s ecosystem - this Microsoft bridge approach is your best bet.&lt;/p&gt;
&lt;p&gt;Is it elegant? No. Does it preserve the workflow that makes reminders-menubar so valuable? Absolutely. And honestly, once you get it set up, you’ll forget about the Microsoft middleman entirely - it just works seamlessly in the background while you focus on actually getting things done.&lt;/p&gt;</content:encoded></item><item><title>Elysia vs Hono: API Handlers in Astro with Cloudflare</title><link>https://afonsojramos.me/blog/elysia-vs-hono-astro-cloudflare/</link><guid isPermaLink="true">https://afonsojramos.me/blog/elysia-vs-hono-astro-cloudflare/</guid><description>Exploring the nuances of API frameworks in Astro when deployed to Cloudflare Pages and the challenges of accessing environment variables</description><pubDate>Tue, 11 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When building a modern web application with Astro, one of the first decisions you’ll face is how to handle API routes. If you’re like me and enjoy the benefits of type safety and developer experience, you’ve probably considered using either Elysia or Hono - two popular Bun-first API frameworks that have gained significant traction in the TypeScript ecosystem.&lt;/p&gt;
&lt;p&gt;I recently went through the process of migrating (on the same day 😅) from &lt;a href=&quot;https://elysiajs.com/&quot;&gt;Elysia&lt;/a&gt; to &lt;a href=&quot;https://hono.dev/&quot;&gt;Hono&lt;/a&gt; for my personal website, and I wanted to share my experience, particularly when deploying to Cloudflare Pages and dealing with environment variables.&lt;/p&gt;
&lt;h2 id=&quot;the-initial-setup-elysia&quot;&gt;The Initial Setup: Elysia&lt;/h2&gt;
&lt;p&gt;Let me be honest upfront - I didn’t strictly &lt;em&gt;need&lt;/em&gt; a framework for my simple API routes. Astro’s built-in API functionality would have been perfectly adequate for my use case. But as developers, we often choose technologies not just for practical reasons, but also for the sake of experimentation and learning. I wanted to try something new and see what these Bun-optimized frameworks had to offer.&lt;/p&gt;
&lt;p&gt;My journey began with Elysia, a framework that promises “sub-millisecond” performance and a delightful developer experience. Setting up API routes in Astro with Elysia is straightforward:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { getTopTracks } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;~/api/lastfm&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { APIRoute } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;astro&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { Elysia } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;elysia&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; app&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Elysia&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({ prefix: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;/api&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, aot: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;false&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;app.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;get&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;/top-tracks&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, getTopTracks);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; handle&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; APIRoute&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ({ &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;request&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; app.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;handle&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(request);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; GET&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; handle;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; POST&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; handle;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This worked perfectly in development. The API routes were fast, type-safe, and the developer experience was excellent. However, when I deployed to Cloudflare Pages, I encountered a significant limitation.&lt;/p&gt;
&lt;h2 id=&quot;the-cloudflare-challenge&quot;&gt;The Cloudflare Challenge&lt;/h2&gt;
&lt;p&gt;Cloudflare Pages uses a runtime environment that differs from your local development setup. One key difference is how environment variables are accessed. In Cloudflare, environment variables are not directly available through &lt;code&gt;process.env&lt;/code&gt; but are instead passed through the request context, which means that it is not globally available like in your local development environment.&lt;/p&gt;
&lt;p&gt;With Elysia, I couldn’t find a clean way to access the Cloudflare runtime context and pass it to my API handlers. My Last.fm API key was stored as a Cloudflare secret, which then is available as an environment variable, but my handlers couldn’t access it because Elysia’s &lt;code&gt;handle&lt;/code&gt; method only accepts the request object, not the full Astro context.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; handle&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; APIRoute&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ({ &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;request&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;locals&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // No way to pass locals.runtime.env to Elysia handlers 😭&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; app.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;handle&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(request);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;enter-hono-the-solution&quot;&gt;Enter Hono: The Solution&lt;/h2&gt;
&lt;p&gt;So, if Elysia was out of the question, I needed to find a new framework that could handle the Cloudflare runtime context. And who’s the second most popular framework after Elysia? Hono! And to be honest, Hono is becoming a way more exciting framework with a ton of built-in features.&lt;/p&gt;
&lt;p&gt;Now, being aware of the problem at hand, I was quick to discover that Hono does offer a more flexible approach to handling contexts.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// [...slugs].ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { getNowPlaying, getTopTracks } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;~/api/lastfm&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { APIRoute } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;astro&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { Hono, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Context &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; HonoContext } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;hono&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { BlankInput } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;hono/types&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Bindings&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;  LASTFM_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; interface&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Context&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; extends&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; HonoContext&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;{ &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;Bindings&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Bindings&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;BlankInput&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt; {}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; app&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Hono&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;{ &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;Bindings&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Bindings&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }&gt;().&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;basePath&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;/api&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;app&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  .&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;get&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;/now-playing&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;c&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; getNowPlaying&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(c))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  .&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;get&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;/top-tracks&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;c&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; getTopTracks&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(c));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; handle&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; APIRoute&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ({ &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;request&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;locals&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // To get the locals.runtime object correctly typed, we need to first follow the guide: https://docs.astro.build/en/guides/integrations-guide/cloudflare/#typing&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // Then we can pass the environment variables to the Hono app&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  app.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;fetch&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(request, { &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;locals.runtime.env });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; GET&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; handle;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; POST&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; handle;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key difference is that Hono’s &lt;code&gt;fetch&lt;/code&gt; method accepts a second parameter where you can pass environment variables and other context. This allowed me to pass &lt;code&gt;locals.runtime.env&lt;/code&gt; from Astro’s context directly to my Hono app, making those environment variables available to my API handlers.&lt;/p&gt;
&lt;h2 id=&quot;refactoring-the-api-handlers&quot;&gt;Refactoring the API Handlers&lt;/h2&gt;
&lt;p&gt;With this change, I also needed to update my API handlers to accept the Hono context:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Before with Elysia&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; getTopTracks&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; API_KEY&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; process.env.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;LASTFM_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // Rest of the handler&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// After with Hono&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; getTopTracks&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;c&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; API_KEY&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; c.env.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;LASTFM_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // Rest of the handler&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach has several advantages:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Improved type safety for environment variables&lt;/li&gt;
&lt;li&gt;Access to Cloudflare-specific features&lt;/li&gt;
&lt;li&gt;Cleaner separation of concerns&lt;/li&gt;
&lt;li&gt;Better testability since dependencies are injected (not that I’m going to write tests for my personal website 🫣, but still)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Is my codebase heavily tied to Cloudflare Pages? A bit, but I’m okay with that. Moving from it, if I wore to move away from the amazing Cloudflare experience that I’ve been enjoying, would still be easier than the migration from NextJS (with CSS Modules) to Astro.&lt;/p&gt;
&lt;h2 id=&quot;performance-considerations&quot;&gt;Performance Considerations&lt;/h2&gt;
&lt;p&gt;Both Elysia and Hono are incredibly fast frameworks, optimized for Bun and modern JavaScript runtimes. In my testing, I couldn’t notice any significant performance difference between them for my use case.&lt;/p&gt;
&lt;p&gt;Elysia claims to be faster in benchmarks, but Hono’s flexibility with context handling made it the better choice for my Cloudflare deployment. As with most performance comparisons, your mileage may vary depending on your specific requirements.&lt;/p&gt;
&lt;p&gt;Additionally, it’s worth noting that &lt;a href=&quot;https://developers.cloudflare.com/workers/runtime-apis/web-standards/&quot;&gt;Cloudflare Workers uses the V8 JavaScript engine&lt;/a&gt; (not even Node.js), which means we’re running in a different environment than what these Bun-optimized frameworks were primarily designed for. Currently, if I wanted a true Bun runtime in production, I’d need to deploy a Docker container on a platform like &lt;a href=&quot;https://fly.io/&quot;&gt;fly.io&lt;/a&gt;, which adds complexity I’d rather avoid, or &lt;a href=&quot;https://render.com/&quot;&gt;Render&lt;/a&gt;, a platform that I don’t really like. Cloudflare’s simplicity and performance make it worth adapting my framework choice.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The migration from Elysia to Hono was relatively painless and solved my specific issue with accessing environment variables in Cloudflare Pages. While both frameworks seem to be excellent choices for building API routes in Astro, Hono’s approach to context handling makes it particularly well-suited for Cloudflare deployments.&lt;/p&gt;
&lt;p&gt;If you’re building an Astro site with API routes and planning to deploy to Cloudflare Pages, I recommend not using a framework at all, but if you want to use one, I recommend starting with Hono to avoid the limitations I encountered. The ability to pass the full runtime context to your handlers will save you time and frustration down the road.&lt;/p&gt;
&lt;p&gt;Sometimes the best technical decisions come from experimentation rather than strict necessity. And I do like to experiment with new technologies, so I’m glad I went through this experience.&lt;/p&gt;
&lt;p&gt;As a closing note, I’m extremely bullish on &lt;a href=&quot;https://bun.sh/&quot;&gt;Bun&lt;/a&gt; as a JavaScript runtime. Its built-in HTTP server capabilities are becoming increasingly powerful with each release, potentially making it a compelling alternative to dedicated frameworks like Elysia or Hono (or even Vite for that matter) for certain use cases. As Bun continues to mature, we might find ourselves reaching for these frameworks less often, especially for simpler API routes. That said, the ecosystem around Bun is thriving precisely because of innovative frameworks like these, and I’m excited to see how they all evolve together.&lt;/p&gt;</content:encoded></item><item><title>Optimising prompt engineering for better AI outputs</title><link>https://afonsojramos.me/blog/prompt-engineering/</link><guid isPermaLink="true">https://afonsojramos.me/blog/prompt-engineering/</guid><description>Learn how well-crafted prompts improve AI performance, user experience, and efficiency.</description><pubDate>Mon, 03 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This article was also published on the &lt;strong&gt;CNCF blog&lt;/strong&gt; &lt;a href=&quot;https://www.cncf.io/blog/2025/01/03/optimising-prompt-engineering-for-better-ai-outputs/&quot;&gt;here&lt;/a&gt; and on the &lt;strong&gt;YLD blog&lt;/strong&gt; &lt;a href=&quot;https://www.yld.io/blog/optimising-prompt-engineering-for-better-ai-outputs&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Remember when searching for information online involved typing in a few keywords and sifting through pages of results? Thankfully, those days are long gone.&lt;/p&gt;
&lt;p&gt;Today’s search engines have transformed the way we find information online. From simple keyword matching to advanced technologies like semantic search and natural language processing, search engines have come a long way. But, have you ever stopped to consider the UX design choices that make these search experiences possible?&lt;/p&gt;
&lt;p&gt;An effective UX/ search experience design involves a deep understanding of how humans interact with machines and the ability to adapt to that relationship. It’s not an innate skill, but rather something that can be developed over time, much like the act of googling is a skill that many of us have honed through years of practice. Similarly, interacting with Generative AI models requires a certain level of skill and knowledge which involves Prompt Engineering.&lt;/p&gt;
&lt;p&gt;This article aims to provide a comprehensive guide to Prompt Engineering, helping you understand its importance, techniques, and best practices. You’ll be equipped to create effective prompts that maximise the potential of AI models and enhance user experiences.&lt;/p&gt;
&lt;h2 id=&quot;why-is-prompt-engineering-important&quot;&gt;Why is Prompt Engineering important?&lt;/h2&gt;
&lt;p&gt;Well-crafted prompts are essential for maximising the potential of AI models, particularly LLMs. They enhance AI performance by providing clear instructions and context, leading to more accurate and relevant responses.&lt;/p&gt;
&lt;p&gt;Prompt Engineering improves user experience by making interactions more intuitive and reducing ambiguity, minimising the risk of misinterpretation. It enables AI models to handle complex tasks, adapt to different use cases, and ensure consistency in outputs, which is vital for integrated systems.&lt;/p&gt;
&lt;h2 id=&quot;precision-in-prompt-engineering&quot;&gt;Precision in Prompt Engineering&lt;/h2&gt;
&lt;p&gt;Prompt Engineering is a discipline that involves developing and optimising prompts to efficiently use language models for a wide variety of applications and research topics. This discipline is particularly useful for developers, researchers, and anyone looking to leverage AI models for various applications and research topics. It encompasses various skills and techniques essential for interacting with and developing LLMs. Mastering this discipline enables you to optimise these interactions, achieving more accurate and relevant outcomes.&lt;/p&gt;
&lt;p&gt;A prompt refers to a statement or question that is employed to trigger a response from a language model or other AI system. Prompts are generally crafted to offer context or instructions to the AI model, directing it to produce a specific type of output or carry out a particular task. A prompt can be provided to the language model by the user or by the system itself, serving as a means to define its default behaviour.&lt;/p&gt;
&lt;p&gt;Creating effective prompts requires a deep understanding of both the AI model (various models respond uniquely to different types of prompts) and the user’s intent. Just as search engines use algorithms to understand the user’s query and return relevant results, Prompt Engineering involves designing prompts to communicate the user’s intent to the AI model.&lt;/p&gt;
&lt;p&gt;Experimenting with different formats, testing various instructions and contexts, and refining the prompt based on the AI model’s responses are key to creating prompts that produce the desired response from the AI model while minimising the risk of misinterpretation or ambiguity.&lt;/p&gt;
&lt;p&gt;The images below demonstrate how slight prompt variations can lead to very different results. However, consistency is crucial in integrated systems. A good prompt aims to produce repeatable outputs with minimal variation, ensuring reliable and predictable AI performance.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/chatgpt-spaghetti-made-of.-2kdkv-a_1miahn.webp&quot; alt=&quot;Screenshot of the first prompt: “What is spaghetti made of?”&quot;&gt;&lt;/p&gt;
&lt;p class=&quot;text-center italic&quot;&gt;
  Screenshot of the first prompt: “What is spaghetti made of?”
&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/chatgpt-spaghetti.BCur8iEt_Z1JB7OO.webp&quot; alt=&quot;Screenshot of the second prompt with a slight variation: ”What is spaghetti?”&quot;&gt;&lt;/p&gt;
&lt;p class=&quot;text-center italic&quot;&gt;
  Screenshot of the second prompt with a slight variation: ”What is spaghetti?”
&lt;/p&gt;
&lt;p&gt;When it comes to the daily interaction with an LLM, a varied yet similar result would be acceptable. But, when it comes to using an LLM as part of an integrated system, consistency is an important factor. What makes a prompt “good” is about producing a somewhat repeatable output with minimal variation.&lt;/p&gt;
&lt;h2 id=&quot;are-all-prompts-equal&quot;&gt;Are all prompts equal?&lt;/h2&gt;
&lt;p&gt;No. Not all prompts are created equally because different types of prompts serve different purposes and can significantly impact the quality and relevance of the AI model’s responses. Understanding the various types of prompts and their applications is key to effective Prompt Engineering.&lt;/p&gt;
&lt;p&gt;As a way to scientifically define methods of communication with LLMs, many have tried to create both techniques and frameworks that systematically define how to write these prompts.&lt;/p&gt;
&lt;p&gt;Here are some common types of AI prompts that serve unique purposes:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One-Shot and Few-Shot prompts&lt;/strong&gt;: If you want to follow a structure for a JIRA ticket, providing a well-structured template will help the AI generate similar ones. Few-shot or One-Shot prompts both help the AI adapt quickly to new tasks by providing a small set of examples, enhancing its ability to generate relevant and accurate responses. These prompting techniques involve providing the AI with examples of the desired task or output before asking it to complete a similar task. By showing the model what is expected through one or a few examples, the AI learns the context and format needed and applies it to new inputs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Zero-Shot prompts&lt;/strong&gt;: A practical use case is asking the AI to translate a sentence from English to French without providing any examples; the AI must rely on its pre-training to understand and perform the task. This approach is particularly useful for assessing how well the AI can handle novel or unexpected queries, demonstrating its ability to apply learned patterns to new contexts. Unlike few-shot prompts, zero-shot prompts require the AI to perform tasks without prior examples, relying solely on its pre-training. This approach is valuable for evaluating the AI’s adaptability and versatility.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Chain-of-Thought prompts&lt;/strong&gt;: These prompts guide the AI to follow a logical progression or reasoning pathway to reach a conclusion or solve a problem. The prompt encourages the AI to detail its
Each of these Prompt Engineering techniques can be adapted and combined depending on the specific requirements of the task at hand and the capabilities of the AI model being used.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hybrid prompts&lt;/strong&gt;: Combining multiple techniques, hybrid prompts might integrate direct instructions with creative challenges or conditional elements with exploratory questions to guide the AI more effectively according to complex needs. For example, you might provide the AI with a Few-Shot prompt to understand the structure of a report, then add a Chain-of-Thought prompt to ensure it details its reasoning, and finally include a Meta-prompt to ask the AI to reflect on its approach. Hybrid prompts are versatile and can be tailored to meet the specific requirements of various tasks, making them particularly useful for complex and multi-faceted projects.&lt;/p&gt;
&lt;p&gt;Effective Prompt Engineering involves a deep understanding of these techniques and the ability to apply them creatively. By leveraging the right prompt type, you can significantly enhance the AI model’s performance and the overall user experience. If you want to read more about the several types of prompts, check out &lt;a href=&quot;https://www.promptingguide.ai/techniques&quot;&gt;Prompting Guide’s techniques page&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;writing-your-best-prompt&quot;&gt;Writing your best prompt&lt;/h2&gt;
&lt;p&gt;Crafting the perfect prompt is like giving directions to a slightly distracted, incredibly smart friend - you need to be clear, concise, and maybe even a little clever.&lt;/p&gt;
&lt;p&gt;Start with a basic prompt and refine it iteratively based on the responses you receive, fine-tuning the AI’s outputs to your specific requirements. Incorporate relevant keywords and specific details to guide the AI more effectively towards the desired output.&lt;/p&gt;
&lt;p&gt;Moreover, don’t use jargon or assume knowledge. Be aware of the model’s limitations to craft prompts within its capabilities, avoiding overly complex requests that lead to poor responses. Utilise feedback to continuously improve your prompts, as insights from users or the outputs themselves can guide adjustments for better results.&lt;/p&gt;
&lt;p&gt;Mind the length of your prompts to prevent confusion and higher token consumption, which can increase costs.&lt;/p&gt;
&lt;p&gt;Although the model may be equipped to handle specific challenges like trick questions about prime numbers, this doesn’t guarantee it can manage every query type. Even after conjuring the best prompt, the model can still confidently reply with false information. In some cases, it might only succeed because similar examples were included in its training data, as shown in the examples in the screenshot below:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/gemini-prime.DO5Cw8VN_ZrB7T8.webp&quot; alt=&quot;Screenshot showing an interaction with an AI model, where the prompt contains trick questions, making it unlikely to provide a factual answer.&quot;&gt;&lt;/p&gt;
&lt;p class=&quot;text-center italic&quot;&gt;
  Screenshot showing an interaction with Gemini, where the prompt contains trick questions, making it unlikely to provide a factual answer.
&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/chatgpt-prime.DhJe535l_Z1ygyWJ.webp&quot; alt=&quot;Screenshot showing an interaction with an AI model, where the prompt contains trick questions, making it unlikely to provide a factual answer.&quot;&gt;&lt;/p&gt;
&lt;p class=&quot;text-center italic&quot;&gt;
  Screenshot of prompting ChatGPT to provide unbiased, factual mathematical answers about the possibility of prime numbers ending in 42.
&lt;/p&gt;
&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;If you find yourself facing a complex problem and need assistance in crafting effective prompts, don’t hesitate to ask AI for help. Leveraging the AI’s capabilities can provide valuable insights and suggestions to refine your prompts, ensuring you get the best possible outcomes. If you feel confident in your prompting capabilities, put yourself to the test against &lt;a href=&quot;https://gandalf.lakera.ai/&quot;&gt;Gandalf&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Finally, different types of prompts complement each other, and combining them can lead to more effective and nuanced interactions with AI models. Whether you’re using One-Shot prompts for quick adaptation, Zero-Shot prompts for versatility, or hybrid prompts for complex tasks, understanding how to leverage these techniques together can significantly enhance the AI’s performance and the overall user experience.&lt;/p&gt;</content:encoded></item><item><title>The Future of AI Computing: WASM and GenAI</title><link>https://afonsojramos.me/blog/wasm-genai/</link><guid isPermaLink="true">https://afonsojramos.me/blog/wasm-genai/</guid><description>My long term bet on Web Assembly (WASM) and Generative AI (GenAI) to power the future of AI computing.</description><pubDate>Mon, 02 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This article was also published on the YLD blog &lt;a href=&quot;https://www.yld.io/blog/the-key-to-building-smarter-scalable-ai-powered-applications&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;While Generative AI (GenAI) has taken centre stage, Web Assembly (WASM) works in the background, complementing GenAI by shifting how we approach performance, portability, and security in web and cross-platform applications.&lt;/p&gt;
&lt;p&gt;While these technologies may seem distinct, their combination unlocks exciting possibilities: WASM’s ability to deliver near-native performance in constrained environments perfectly complements the computation-heavy demands of GenAI models.&lt;/p&gt;
&lt;p&gt;Together, they create a powerful partnership, improving how we deploy and experience AI-driven applications.&lt;/p&gt;
&lt;p&gt;This article explores how GenAI and WASM work together to unlock new possibilities for businesses using AI applications. You will also learn some strategies to harness their full potential with accessibility and scalability in mind.&lt;/p&gt;
&lt;h2 id=&quot;a-perfect-pair-for-the-future-of-ai-computing&quot;&gt;A perfect pair for the future of AI computing&lt;/h2&gt;
&lt;p&gt;WASM is a low-level, portable binary instruction format designed for safe, efficient execution across various environments. Initially created to enhance web application performance, it has quickly extended its reach to edge computing, serverless platforms, and embedded systems. Its appeal lies in three key strengths:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Near-native performance&lt;/li&gt;
&lt;li&gt;Portability as a universal execution target&lt;/li&gt;
&lt;li&gt;Security through its sandboxed environment (especially in browsers)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By decoupling code execution from specific platforms or architectures, WASM allows developers to build once and deploy anywhere, making this function an ideal approach to developing modern, distributed applications.&lt;/p&gt;
&lt;p&gt;The rise of Vercel-like WASM-focused companies highlights the growing adoption of WASM’s unique benefits for developers. With WASM, developers gain greater autonomy and flexibility, enabling them to build and deploy high-performance applications without being tied to specific platforms. Additionally, companies using WASM can optimise performance while minimising resource usage, making the process of building and deploying applications much more efficient.&lt;/p&gt;
&lt;p&gt;Generative AI, on the other hand, has rapidly advanced with models like o1 and Deepseek-R1 now capable of creating human-like text, generating realistic images, and even writing functional code. These models rely on vast amounts of training data and significant computational power, highlighting their potential for major innovation. At the same time, they spark important discussions about the resources required and the ethical considerations surrounding this powerful technology.&lt;/p&gt;
&lt;h2 id=&quot;opportunities-challenges-and-key-considerations&quot;&gt;Opportunities, challenges, and key considerations&lt;/h2&gt;
&lt;p&gt;GenAI models are computationally intensive, making them difficult to deploy in resource-constrained environments. Real-time applications, which require fast responses, face challenges with traditional deployment methods, as they struggle to process data quickly enough. This leads to slower performance or, in some cases, failure to deploy. While tools like Ollama have proven that most models can run locally, the performance that you can find on basic devices, such as entry-level smartphones or laptops, is typically much lower than on more powerful systems like the one you’re reading this on.&lt;/p&gt;
&lt;p&gt;However, despite these limitations, GenAI makes an excellent candidate for optimisation through WASM because WASM can make AI applications more accessible across a range of devices. While WASM offers a lightweight and portable execution environment, its ability to address GenAI challenges largely depend on the specific use case and optimisation strategies. Here are some key points to consider:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Portability&lt;/strong&gt;: GenAI models compiled into WASM modules can run across platforms like browsers and edge devices. However, achieving smooth operation may require significant optimisations and adjustments for each environment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Efficiency&lt;/strong&gt;: WASM can improve inference performance in local setups, but the benefits may be limited for larger GenAI models or more complex workloads due to WASM’s current hardware limitations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scalability&lt;/strong&gt;: Serverless platforms adopting WASM can simplify GenAI model deployment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Privacy&lt;/strong&gt;: WASM can help prioritise privacy by enabling GenAI models to run locally, but this approach may involve trade-offs in terms of model complexity and computational overhead.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For example, a WASM module could host a language model directly in a web browser, enabling offline chatbots without needing server connectivity. While tools like Ollama already offer similar functionality, WASM allows for browser-based deployment, which brings the flexibility that comes with the browser.&lt;/p&gt;
&lt;h2 id=&quot;real-world-use-cases&quot;&gt;Real-world use cases&lt;/h2&gt;
&lt;p&gt;In the real world, we’re already seeing examples of WASM and GenAI working together, like &lt;a href=&quot;https://whisper.ggerganov.com/&quot;&gt;Whisper running directly in the browser&lt;/a&gt;. This hybrid approach allows users to choose the model and adjust performance according to their needs. There are exciting possibilities on the horizon where WASM and GenAI could combine to create innovative solutions. Here are a few examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;In-Browser AI Assistants&lt;/strong&gt;: Deploy lightweight GenAI models directly in browsers using WASM, providing real-time assistance without network latency.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge Device Applications&lt;/strong&gt;: Run WASM-optimised GenAI models on IoT devices for tasks like image recognition or anomaly detection.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Serverless AI APIs&lt;/strong&gt;: Host GenAI models as WASM modules on serverless platforms, reducing operational costs while improving scalability.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These use cases demonstrate how WASM empowers GenAI to operate efficiently in diverse environments, from cloud servers to smaller edge devices.&lt;/p&gt;
&lt;h2 id=&quot;overcoming-barriers-to-realise-wasm-and-genais-full-potential&quot;&gt;Overcoming barriers to realise WASM and GenAI’s full potential&lt;/h2&gt;
&lt;p&gt;While the combination of WASM and GenAI promises a lot of great things, several challenges must be addressed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GPU Limitations&lt;/strong&gt;: WASM currently lacks native GPU support, making it difficult to accelerate GenAI workloads that rely on parallel processing, a critical feature for handling large-scale AI tasks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory Constraints&lt;/strong&gt;: This compute boundary is a significant hurdle for WASM when working with AI workloads requiring high levels of parallelism. Furthermore, large GenAI models often exceed WASM’s default memory limits, necessitating careful optimisation to fit these constraints.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Early-stage Ecosystem&lt;/strong&gt;: While WASM is evolving rapidly, tools and libraries for integrating WASM with AI frameworks are still maturing, highlighting the ecosystem’s early-stage nature.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Emerging tools like WebLLM and platforms such as Turso are addressing some of these limitations in innovative ways. For example, WebLLM demonstrates the feasibility of running large language models directly in the browser by leveraging WebGPU for acceleration. It’s important to note that WebGPU is still an experimental API, and its integration with WASM for AI workloads is in its early stages, requiring further development and testing.&lt;/p&gt;
&lt;p&gt;Similarly, Turso, a distributed database built on libSQL, has introduced features like native vector search and 1-bit quantisation for vector embeddings. These advancements make it easier to deploy AI applications that prioritise local-first processing and efficient resource usage. However, both WebLLM and Turso highlight the gaps in WASM’s ecosystem, such as the need for GPU integration and optimised toolchains for AI workloads. Addressing these challenges will be key to unlocking the full potential of this pairing.&lt;/p&gt;
&lt;h2 id=&quot;how-wasm-and-genai-are-redefining-the-future-of-software-development&quot;&gt;How WASM and GenAI are redefining the future of software development&lt;/h2&gt;
&lt;p&gt;Looking ahead, advancements in both WASM and GenAI promise to deepen their integration through the following ways:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;WebGPU Support&lt;/strong&gt;: Introducing WebGPU to WASM environments will enable hardware acceleration for AI workloads. However, it’s worth noting that &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API&quot;&gt;WebGPU is still an experimental API&lt;/a&gt;, and its integration with WASM for AI workloads is in the early stages, requiring further development and testing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Model Optimisation&lt;/strong&gt;: Techniques like quantisation and pruning will make it easier to deploy GenAI models within WASM’s constraints.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Standardised Toolchains&lt;/strong&gt;: Improved tooling will simplify the process of compiling and deploying GenAI models as WASM modules.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Private Personal Assistants&lt;/strong&gt;: Having the model run locally will open up the possibilities to protect the end user from endless data-sharing, one that is a given if the benefits of using AI keep increasing.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These developments are set to enable real-time, AI-driven applications that are fast, portable, and accessible to a wide range of users.&lt;/p&gt;
&lt;p&gt;WASM and GenAI are two disruptive technologies poised to change how we build and deploy software. WASM’s portability and performance make it an ideal runtime for GenAI, enabling the creation of applications that are both powerful and accessible.&lt;/p&gt;
&lt;p&gt;As these technologies continue to evolve, now is the time for developers to explore their synergy. Whether you’re building the next-gen AI assistant, a cutting-edge edge computing solution, or serverless applications, the pairing of WASM and GenAI offers endless possibilities to be unlocked.&lt;/p&gt;</content:encoded></item><item><title>critical mass portugal</title><link>https://afonsojramos.me/projects/critical-mass/</link><guid isPermaLink="true">https://afonsojramos.me/projects/critical-mass/</guid><description>The official site for Critical Mass Portugal - a grassroots cycling movement reclaiming city streets and advocating for safer, more bike-friendly cities.</description><pubDate>Sun, 01 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Critical_Mass_(cycling)&quot;&gt;Critical Mass&lt;/a&gt; is a worldwide movement, born in San Francisco in 1992, where cyclists meet on the last Friday of every month and ride together through city streets. Not a protest, a celebration - and an obvious-once-you-see-it argument that bicycles are real urban transportation, not toys. In Portugal it runs in Porto, Lisboa, Coimbra, and a growing list of smaller cities, organised entirely by volunteers, and until recently every chapter ran on its own scattered Instagram pages and WhatsApp groups.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/critical-mass-2025.CrkHKPTV_Z2065Do.webp&quot; alt=&quot;Cyclists at Critical Mass Portugal 2025&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://massacritica.pt&quot;&gt;massacritica.pt&lt;/a&gt; is the home I built for the whole movement: a single bilingual (PT/EN) site that lists every city’s monthly ride, surfaces upcoming events, hosts blog posts about cycling advocacy, and gives organisers a non-technical way to keep their content fresh. Built with Astro and TailwindCSS, internationalised via &lt;a href=&quot;https://inlang.com/m/gerre34r/library-inlang-paraglideJs&quot;&gt;Paraglide.js&lt;/a&gt; for type-safe translations. Content management started on &lt;a href=&quot;https://github.com/sveltia/sveltia-cms&quot;&gt;Sveltia CMS&lt;/a&gt; and recently migrated to &lt;a href=&quot;https://blog.cloudflare.com/emdash-wordpress/&quot;&gt;emdash&lt;/a&gt;, Cloudflare’s new WordPress-style publishing platform, so contributors can edit through a friendly UI without ever touching git directly. Deployed to Cloudflare for fast edge rendering anywhere in the country.&lt;/p&gt;
&lt;p&gt;The repo is open at &lt;a href=&quot;https://github.com/afonsojramos/critical-mass&quot;&gt;github.com/afonsojramos/critical-mass&lt;/a&gt; - community pull requests for new cities and ride reports very welcome.&lt;/p&gt;</content:encoded></item><item><title>gitify</title><link>https://afonsojramos.me/projects/gitify/</link><guid isPermaLink="true">https://afonsojramos.me/projects/gitify/</guid><description>A menubar app for GitHub notifications - PR reviews, mentions, issue updates, and CI failures as an unread count, with one click into the thread.</description><pubDate>Fri, 12 May 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;How are GitHub notifications supposed to work anyway? Do you keep the GitHub notifications inbox open in a browser tab, constantly refreshing? Or do you manage to handle the constant flurry of emails for every github notification? For me, the best way to manage something this important, is usually by keeping it in sight, and that’s why &lt;a href=&quot;https://gitify.io&quot;&gt;Gitify&lt;/a&gt; exists. A small menubar app for GitHub notifications. PR reviews, mentions, issue updates, and CI failures show up as an unread count in the menubar; clicking through takes you to the thread. Truly the best way to stay responsive at work and not miss any pings.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/notifications.C_u0SUn7_ZuHnY1.webp&quot; alt=&quot;Gitify notifications&quot;&gt;&lt;/p&gt;
&lt;p&gt;I joined the project as a contributor in mid-2023 and stayed on as a core maintainer alongside a small, dedicated team. A lot of the work is the load-bearing kind: improving a long-lived Electron app up to date with latest standards, aligning the UI with &lt;a href=&quot;https://primer.style&quot;&gt;GitHub’s Primer design system&lt;/a&gt; so it feels native to the platform it surfaces, and triaging the steady stream of “this notification didn’t show up” reports that come with serving real users. Underneath that, a fairly constant cadence of new features ships with every release with the help of @setchy. Gitify today does meaningfully more than the app I first contributed to.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/filters.PdfbeETx_Z1iw3YG.webp&quot; alt=&quot;Gitify filters&quot;&gt;&lt;/p&gt;
&lt;p&gt;Over the years Gitify has accumulated &lt;strong&gt;over 1.75 million lifetime downloads&lt;/strong&gt; across all releases, 5k+ GitHub stars, and a small but committed maintainer team. TypeScript end-to-end, Electron + React + Tailwind, tested with Vitest.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/settings.DD_ryrCs_1oS724.webp&quot; alt=&quot;Gitify settings&quot;&gt;&lt;/p&gt;
&lt;p&gt;Feel free to give it a go at &lt;a href=&quot;https://www.gitify.io&quot;&gt;gitify.io&lt;/a&gt;!&lt;/p&gt;</content:encoded></item><item><title>Spicetify - An open-source journey</title><link>https://afonsojramos.me/blog/spicetify-open-source-journey/</link><guid isPermaLink="true">https://afonsojramos.me/blog/spicetify-open-source-journey/</guid><description>How did I get to be one of the core maintainers of Spicetify? Let&apos;s find out!</description><pubDate>Mon, 12 Dec 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;According to my message history, it all started around February 2020 when I found out about Spicetify through some random Reddit post in the illustrious &lt;a href=&quot;https://www.reddit.com/r/unixporn/&quot;&gt;r/unixporn&lt;/a&gt; - a subreddit where the highest tier of nerds share their Unix setups and configs - and I was immediately intrigued.&lt;/p&gt;
&lt;p&gt;I had been using Spotify for a while, but even though the UI is not &lt;strong&gt;awful&lt;/strong&gt;, they do have some very anti-user behaviours. Initially, I was simply a user, but then ideas to improve it started coming up, and I started to develop a more active role in the development. One of my first contributions was to the &lt;a href=&quot;https://spicetify.app/docs/advanced-usage/custom-apps#new-releases&quot;&gt;New Releases Custom App&lt;/a&gt;. This extension creates a page where we can see all the new releases for the artists we follow. Conceptually, it is a simple feature, however, Spotify’s “notifications” were practically useless, even to this day &lt;strong&gt;[1]&lt;/strong&gt;, which made it quickly become one of my favourite Spicetify features, which also led to my first contribution (&lt;a href=&quot;https://github.com/spicetify/spicetify-cli/issues/247&quot;&gt;spicetify-cli#247&lt;/a&gt;).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;[1]&lt;/strong&gt; They did do &lt;strong&gt;something&lt;/strong&gt; on mobile, but if you follow more than 30 artists the list gets pretty polluted with singles. The only filter available is either Music or Podcasts, which is not very useful. And I mostly listen to Spotify on my PC anyway, so it doesn’t really matter.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Spicetify’s core, what we call &lt;a href=&quot;https://github.com/spicetify/spicetify-cli&quot;&gt;spicetify-cli&lt;/a&gt;, is a Command-line Interface (CLI) tool that goes through Spotify’s binaries mutating them through regex matching to allow for the promised customisation. It is written in Go, which is a highly performant language that I learnt a couple of years prior to give a &lt;a href=&quot;https://github.com/afonsojramos/competitive-programming/blob/master/advent-of-code/2018/go-guide.md&quot;&gt;workshop&lt;/a&gt; on it at &lt;a href=&quot;https://ieee.fe.up.pt/&quot;&gt;IEEE University of Porto Student Branch&lt;/a&gt;, a student branch of the IEEE that I was a member of at the time - and later was elected Vice-President for a year.&lt;/p&gt;
&lt;p&gt;With time, I started helping out more and more in handling issues, developing small features, and keeping the latest Spotify version supported &lt;strong&gt;[2]&lt;/strong&gt;. Eventually, &lt;a href=&quot;https://github.com/khanhas&quot;&gt;khanhas&lt;/a&gt; made me a maintainer on GitHub to help with everything. In late 2021, Spicetify’s popularity truly blew up and we had to start handling a lot more issues and pull requests, which was a very exciting time for me. However, it was also the last time we saw khanhas, as he decided to leave the project and focus on other things. Thankfully, he left the project in a very good state, and I was able to take over as the new maintainer. Additionally, there were a small group of people that were already helping out with the project, which helped immensely.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;[2]&lt;/strong&gt; Spotify releases most of the times do not break the CLI itself, but when they do it is because we need to apply updates to the regexes that match the Spotify binaries. This is a very tedious process, but it is crucial to keep the CLI working.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It was September 2020 when I started working full-time, but it was in 2022 that my workload, other side-projects and overall life started pilling up a bit more, and, eventually, I noticed myself not being able to dedicate as much time to Spicetify as I would have liked. This is when I shifted to a more managerial role, where I would help out with the more complex issues, but also help out with the more mundane tasks, such as reviewing pull requests and merging them. I also created the &lt;a href=&quot;https://spicetify.app&quot;&gt;Spicetify Documentation&lt;/a&gt; website, which is a very important part of the project, as it is the first place people look at when they want to customise their Spotify, unlike previously when people could only look at GitHub’s wiki, which was not very user-friendly for newcomers. And also created the &lt;a href=&quot;https://github.com/spicetify&quot;&gt;Spicetify GitHub organisation&lt;/a&gt;, which is where all our repositories are hosted since previously they were hosted across several different contributors’ accounts.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/spicetify-visitors.TetB9ZvF_Z15fWq2.webp&quot; alt=&quot;Spicetify Docs Visitors&quot;&gt;&lt;/p&gt;
&lt;p&gt;Overall, it has been quite a ride, but seeing hundreds of thousands of downloads per GitHub release and over 200K unique visitors per month to our documentation is a very rewarding feeling. I am very grateful to all the contributors, users and maintainers that have helped out in this journey, and I hope that we can continue to make Spicetify better and better.&lt;/p&gt;</content:encoded></item><item><title>parrot</title><link>https://afonsojramos.me/projects/parrot/</link><guid isPermaLink="true">https://afonsojramos.me/projects/parrot/</guid><description>A hassle-free, highly performant, host-it-yourself Discord music bot built with Serenity in Rust, powered by yt-dlp.</description><pubDate>Sat, 01 Jan 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When Discord’s biggest music bots started shutting down one after the other in 2021 over copyright pressure, the obvious move for most servers was to find a hosted replacement. We took the opposite path and built our own: &lt;a href=&quot;https://github.com/aquelemiguel/parrot&quot;&gt;parrot&lt;/a&gt;, a small, fast, host-it-yourself Discord music bot designed to do one thing well - play audio from anywhere yt-dlp can reach, with as little ceremony as possible.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/logo.Lsn-4ZWQ_Z2iyBPI.webp&quot; alt=&quot;parrot logo&quot;&gt;&lt;/p&gt;
&lt;p&gt;Written in Rust on top of &lt;a href=&quot;https://github.com/serenity-rs/serenity&quot;&gt;Serenity&lt;/a&gt; and &lt;a href=&quot;https://github.com/serenity-rs/songbird&quot;&gt;Songbird&lt;/a&gt;, parrot is built around the idea that a self-hosted bot should be a single binary with a &lt;code&gt;.env&lt;/code&gt; file, not a service. A single &lt;code&gt;docker run&lt;/code&gt; and you have a working bot. The codebase is small enough that new contributors can find their way around quickly, which is part of why it has kept growing.&lt;/p&gt;
&lt;p&gt;It has been a long-running collaboration with &lt;a href=&quot;https://github.com/aquelemiguel&quot;&gt;@aquelemiguel&lt;/a&gt; and &lt;a href=&quot;https://github.com/joao-conde&quot;&gt;@joao-conde&lt;/a&gt;, focused on keeping the surface area small while staying responsive to the kinds of edge cases real Discord servers run into - flaky network conditions, large queues, abusive inputs. Source on &lt;a href=&quot;https://github.com/aquelemiguel/parrot&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;</content:encoded></item><item><title>album details extractor</title><link>https://afonsojramos.me/projects/album-details-extractor/</link><guid isPermaLink="true">https://afonsojramos.me/projects/album-details-extractor/</guid><description>Five-interface tool that extracts album metadata from Spotify, Apple Music, Tidal, Deezer, Qobuz, and Bandcamp into a compact JSON object on your clipboard - extension, bookmarklet, CLI, and Spicetify.</description><pubDate>Sun, 21 Nov 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Right-click any album, get clean JSON in your clipboard. That’s the whole product:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;title&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Currents&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;artist&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Tame Impala&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;image&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;https://image-cdn-ak.spotifycdn.com/image/...&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;url&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;https://open.spotify.com/album/79dL7FLiJFOO0EoehUHQBv&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My &lt;a href=&quot;/music&quot;&gt;/music&lt;/a&gt; page is the running list of albums I have listened to and loved - partly automated, partly hand-curated. The friction of capturing the title, artist, cover, and canonical URL each time I wanted to add one was exactly the kind of annoyance that a small tool can fix. The output is the same shape no matter which store you started from, so the destination - a music page, a blog post, a &lt;code&gt;music.json&lt;/code&gt; file - does not care.&lt;/p&gt;
&lt;p&gt;There are five ways to trigger it, all wrapping the same extractor core:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Browser extension&lt;/strong&gt; (Chrome and Firefox, MV3) - right-click any album link anywhere on the web, or use the toolbar button when on an album page&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bookmarklet&lt;/strong&gt; - single-line &lt;code&gt;javascript:&lt;/code&gt; URL, zero install, works on any browser&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Standalone CLI&lt;/strong&gt; - single-file binary, no runtime, prints JSON to stdout&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/projects/spicetify&quot;&gt;Spicetify&lt;/a&gt; extension&lt;/strong&gt; - adds the menu entry inside Spotify desktop’s native right-click&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Supported sources: &lt;strong&gt;Spotify&lt;/strong&gt;, &lt;strong&gt;Apple Music&lt;/strong&gt;, &lt;strong&gt;Tidal&lt;/strong&gt;, &lt;strong&gt;Deezer&lt;/strong&gt;, &lt;strong&gt;Qobuz&lt;/strong&gt;, and &lt;strong&gt;Bandcamp&lt;/strong&gt;. Adding a new one is one file in &lt;code&gt;src/shared/sources/&lt;/code&gt; plus a registration line - every interface picks it up automatically.&lt;/p&gt;
&lt;p&gt;Source at &lt;a href=&quot;https://github.com/afonsojramos/album-details-extractor&quot;&gt;github.com/afonsojramos/album-details-extractor&lt;/a&gt;. The browser extension is on &lt;a href=&quot;https://addons.mozilla.org/en-GB/firefox/addon/album-details-extractor/&quot;&gt;Mozilla Add-ons&lt;/a&gt; for Firefox and the &lt;a href=&quot;https://chromewebstore.google.com/detail/album-details-extractor/kfpkjhjengocbiaipfcbdhpjbaenkanb&quot;&gt;Chrome Web Store&lt;/a&gt; for Chromium.&lt;/p&gt;</content:encoded></item><item><title>spicetify</title><link>https://afonsojramos.me/projects/spicetify/</link><guid isPermaLink="true">https://afonsojramos.me/projects/spicetify/</guid><description>A command-line tool that lets you customize the official Spotify client - themes, extensions, custom apps, and full control over the UI.</description><pubDate>Wed, 16 Jun 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://spicetify.app&quot;&gt;Spicetify&lt;/a&gt; is a CLI written in Go that patches the official Spotify desktop client to allow theming, custom extensions, and entire custom apps. What started as a small modding project has grown into a full ecosystem: &lt;strong&gt;20M+ lifetime downloads&lt;/strong&gt; (~370k per release), 23k+ stars on GitHub, a &lt;a href=&quot;https://github.com/spicetify/marketplace&quot;&gt;marketplace&lt;/a&gt; for browsing community work, a &lt;a href=&quot;https://github.com/spicetify/spicetify-creator&quot;&gt;creator framework&lt;/a&gt; for building extensions, and a thriving Discord community.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/logo.D9pUmCmA_1k7ieQ.webp&quot; alt=&quot;Spicetify customised Spotify client&quot;&gt;&lt;/p&gt;
&lt;p&gt;I have been a core maintainer since 2022, after &lt;a href=&quot;https://github.com/khanhas&quot;&gt;@khanhas&lt;/a&gt; - the original author - stepped away. The handover happened just as the project was experiencing a surge in popularity, which meant inheriting a backlog of issues and pull requests alongside the very real work of keeping the CLI compatible with each new Spotify release. A lot of that work is regex-driven binary patching, which sounds fragile but is, in fact, fragile.&lt;/p&gt;
&lt;p&gt;Beyond the maintenance work, I built and maintain the &lt;a href=&quot;https://spicetify.app&quot;&gt;Spicetify Documentation&lt;/a&gt; - the canonical reference for installation, theming, and extension development. It pulls in around &lt;strong&gt;300,000 visits a month&lt;/strong&gt;! The &lt;a href=&quot;/blog/02-spicetify&quot;&gt;opening remarks blog post&lt;/a&gt; goes into more depth on the history and how I ended up here. Source for the CLI lives at &lt;a href=&quot;https://github.com/spicetify/cli&quot;&gt;github.com/spicetify/cli&lt;/a&gt;.&lt;/p&gt;</content:encoded></item><item><title>Welcome to my blog</title><link>https://afonsojramos.me/blog/opening-remarks/</link><guid isPermaLink="true">https://afonsojramos.me/blog/opening-remarks/</guid><description>The first post is always the worst</description><pubDate>Mon, 14 Jun 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;println!(&quot;Hello World!&quot;);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;As a learning &lt;strong&gt;rust&lt;/strong&gt;acean, I’ve chosen to start this blog with a classic, but &lt;strong&gt;rust&lt;/strong&gt;ic &lt;em&gt;(has this joke been made before?)&lt;/em&gt;, Hello World!&lt;/p&gt;
&lt;p&gt;Bad programming jokes aside, I will use this blog to write posts about tech, music, states of mind, opinions, etc. Maybe I’ll even do one about beer 🍺! In reality, I don’t quite have any idea what I will write about, maybe about everything and maybe about nothing. But I’ll certainly try to write about something, even if it is an excuse to fuel my creative side!&lt;/p&gt;
&lt;p&gt;As an engineer, I often consider myself too analytic in the creative process. Not that it is a bad thing, it’s just something that is part of me and that I am aware of. It is not for no reason that I’ve decided to focus on backend development, as problem-solving is my jam! However, if it involves designing, I’ll probably rely a lot on the “inspiration” phase, which roughly translates to &lt;em&gt;“look at other designs and rip off all the good ideas”&lt;/em&gt;. Again, this is also not a bad thing - &lt;em&gt;I think&lt;/em&gt; - but it means that my creative work will never be truly &lt;em&gt;original&lt;/em&gt; and be based on building on what’s good ✨ out there ✨.&lt;/p&gt;
&lt;p&gt;In the end, my admiration for the creative process of artists, designers and writers is immeasurable. It’s something that can be easily noticed by looking at the amount of music I listen to and concerts that I go to, but it goes way beyond that! We all have some sort of creativity within us, but some people are so much better at using it and that makes them amazing. And I hope that by creating this blog I can practice that, expand my creative side and that you enjoy this journey I am embarking on.&lt;/p&gt;
&lt;p&gt;See you in the next post! ✌&lt;/p&gt;</content:encoded></item><item><title>discrakt</title><link>https://afonsojramos.me/projects/discrakt/</link><guid isPermaLink="true">https://afonsojramos.me/projects/discrakt/</guid><description>A Discord Rich Presence that shows your friends what you&apos;re watching, anywhere, on any device, in any app.</description><pubDate>Wed, 01 Jan 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Discord Rich Presence has long been a fun way to broadcast what you are doing - playing a game, listening to Spotify - without saying a word. The same did not exist for what you were watching, especially across the messy reality of streaming today: a movie on the TV, an episode on the iPad in bed, something half-watched on a laptop. Discrakt fills that gap by polling &lt;a href=&quot;https://trakt.tv&quot;&gt;Trakt&lt;/a&gt; for your “currently watching” status and surfacing it on Discord, regardless of where the content is actually playing.&lt;/p&gt;
&lt;p&gt;I built it back when I was gaming a lot more and living in Discord most evenings. I do neither as much these days, but Discrakt has kept ticking along.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/profile-status.CNv4Deqi_AAiv.webp&quot; alt=&quot;Discrakt profile status showing the current movie&quot;&gt;&lt;/p&gt;
&lt;p&gt;The trick is that &lt;strong&gt;anything that scrobbles to Trakt works&lt;/strong&gt;. Stremio, Plex, Kodi, Infuse, VLC - any app with a Trakt integration reports playback in real time, and Discrakt picks it up. As long as you have one machine running Discord and Discrakt, your status follows you everywhere else: streaming on the TV from across the room, watching on your phone on the couch, even on a different device entirely.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/member-list.D9xwfh0z_9QRqw.webp&quot; alt=&quot;Discrakt entry in the Discord member list&quot;&gt;&lt;/p&gt;
&lt;p&gt;Built in Rust for low overhead, the app runs quietly from the system tray with separate Rich Presence apps for movies and TV shows, localized titles via TMDB, deep links to IMDB and Trakt, and a browser-based OAuth setup wizard so the first run is “enter your Trakt username and you’re done.”&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/tray.BhBxq6FX_1BKp3G.webp&quot; alt=&quot;Discrakt system tray menu&quot;&gt;&lt;/p&gt;
&lt;p&gt;Available via &lt;a href=&quot;https://github.com/afonsojramos/homebrew-discrakt&quot;&gt;Homebrew&lt;/a&gt;, Winget, and direct downloads on the &lt;a href=&quot;https://github.com/afonsojramos/discrakt/releases&quot;&gt;releases page&lt;/a&gt;. Source on &lt;a href=&quot;https://github.com/afonsojramos/discrakt&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;</content:encoded></item></channel></rss>