All work

Tempo

Every Friday. Abuja.

Role
Solo build. Design, frontend, the Worker behind it, and deployment.
Stack
Astro, GSAP, Lenis, Web Audio, Cloudflare Workers, D1, Sass
Status
Live
Link
tempoabj.com
Tempo

The brief

TEMPO is a free rave in Abuja every Friday. On the fourth Friday of the month it becomes KLUB TEMPO, a ticketed special edition with a guest. The site had to tell people when the next night is, sell the special ones, and feel like the room does at 2am. It went from an empty Astro project to live on tempoabj.com in about a week.

A schedule nobody has to update

A weekly event site goes stale the first Friday someone forgets to edit it. So there are no dates in a CMS. The next eight nights are computed in Abuja’s timezone at build time, then again in the browser, so even an old deploy never lists a Friday that has passed. A fourth Friday always falls between the 22nd and the 28th, so working out which nights are KLUB TEMPO is a date check, not a lookup. Specials that need more (a guest, a venue, a ticket link) get a small entry in one data file, and that entry is what gives the night its own page at /klub/<slug>.

Each edition page also gets its own share card. The cards are ordinary Astro pages at 1200×630, and a script builds the site, serves it locally and screenshots them with headless Chrome. Designing a share card is just designing a page.

Find Your Tempo

The part I had the most fun with is /play, a rhythm game. Pick a genre (Afrohouse, Afrotech, Amapiano, Gqom, Bouyon) and tap on the beat as rings close in on the pad.

Nothing is downloaded. Every groove is a set of 16-step patterns synthesised with Web Audio: kicks, hats, shakers, claps and, for Amapiano, the log drum you tap along to. Notes are scheduled slightly ahead on the audio clock, with a 25ms lookahead loop, so timing holds even when the main thread is busy animating. Taps are graded against 45, 100 and 160ms windows, after compensating for the time sound takes to leave your speakers. At the end it tells you whether you rush or drag, and by how many milliseconds.

The rest of the room

The hero is a video. Until the first frame lands, a red pulse beats at 124 BPM behind the copy, so a slow connection still gets something that moves in time. Scrolling runs on Lenis with GSAP ScrollTrigger and SplitText for the reveals, and all of it stands down under prefers-reduced-motion.

The photo wall and the gallery hold 55 shots from KLUB TEMPO by GT Studios. The lightbox is an enhancement: every photo is a plain link to the full image, so it works before JavaScript loads. With it, you get arrow keys, swipe, and the neighbouring photos warmed up so paging through feels instant.

A Worker that does three small things

The site is static, with one Cloudflare Worker in front. It redirects the old workers.dev address and www to the apex domain, serves the assets, and handles one endpoint: mailing list signups into D1.

The signup form is a normal form POST, so it works without JavaScript, and it answers with JSON when the page asks for it. A honeypot field catches the lazy bots, and a per-IP rate limit catches the rest, keyed on a hash of the address so no raw IPs are stored. Signing up twice updates the row instead of failing, and WhatsApp numbers are normalised to international format (a leading 0 becomes +234) because that’s how people here write them.

What I’d keep

Compute what you can. The schedule, the KLUB nights and the edition pages all come from the calendar plus one data file. The site stays correct without anyone remembering it exists.

Plain HTML first. The form, the lightbox and the gallery all work as links and posts before any script runs. On a phone at a rave, in an in-app browser, on bad signal, that’s most visitors. One fix I shipped was switching to classic media queries so phone layouts apply on older iOS and inside in-app browsers, exactly where people tap a link from Instagram.