PortfolioCase Study

Client Work — Next.js 16 + React 19

Bonita Cafe Website

April – June 2026

The public marketing site for Bonita Cafe — a family-owned Colombian coffee and brunch spot in Briskeby, Oslo. I designed and built the entire site myself: a fast, single-page Next.js experience covering the story, chalkboard-style menu, and takeaway ordering, with a live Instagram gallery that degrades gracefully when the feed is unavailable. Deployed to a self-hosted Docker + Caddy stack set up by a teammate.

Client WorkDesign + DevelopmentSelf-Hosted
Bonita Cafe Website

Bonita Cafe website — bonitacafe.no, the cafe's public menu, story, and ordering site.

Technical Implementation

Frontend

  • Next.js 16 with App Router
  • React 19
  • TypeScript
  • Tailwind CSS v4 with custom brand tokens
  • Google Fonts — Fraunces, Caveat, DM Sans

Content & Integrations

  • Instagram Graph API live feed via ISR (hourly revalidate)
  • Curated fallback gallery for when the feed is unavailable
  • Wolt & Foodora takeaway ordering
  • Embedded map, hours, and contact info
  • Umami analytics for privacy-respecting event tracking

Infrastructure (set up by a teammate)

  • Self-hosted Docker (standalone Next.js build)
  • Shared Caddy edge ingress (bonitasites-edge) fronting multiple sites
  • GitHub Actions CI/CD — lint, typecheck, build, deploy on push to prod
  • Automated Instagram token rotation service (14-day cycle)
  • rsync-based deploy over SSH — no host ports exposed

Key Features

Chalkboard-Style Menu

A tabbed, chalkboard-styled menu with the cafe's real items and prices — breakfast, smoothies, coffee, pastas, and salads.

Live Instagram Gallery

Fetches the latest posts and reels from @bonitacafe_oslo server-side, cached via ISR and revealed six at a time with a "Show more" button.

Curated Fallback Gallery

If the Instagram token is missing, invalid, or the feed runs dry, the gallery falls back to a curated set of cafe photos — so the page never looks broken.

Takeaway Ordering

Direct Click & Collect ordering alongside Wolt and Foodora delivery links, all surfaced from the nav and hero.

Animated Hero & Marquee

Split hero layout with a hand-drawn sticker graphic and an animated marquee strip highlighting the cafe's story.

Visit Section

Opening hours, contact details, and an embedded map for the Briskeby, Oslo location.

Development Process

Designing and Building a Single-Page Site

I designed and built the whole site myself — no external Figma or brief to work from. That meant deciding the brand direction (the Colombian-inspired colour palette, the Fraunces/Caveat/DM Sans type pairing, the hand-drawn chalkboard motif) and then implementing it as a single scrollable page: hero, story, menu, takeaway, visit, gallery, and footer. The menu uses real items and prices rather than placeholder content.

Brand-True but Usable

Owned the full design-to-code path — custom Tailwind v4 tokens carry the brand colours and typography throughout, with no separate design handoff.

The chalkboard menu tabs had to look hand-drawn while remaining keyboard- and screen-reader-friendly.

A Live Feed with a Safety Net

The gallery pulls from the Instagram Graph API server-side, cached with ISR and refreshed roughly hourly. Instagram's API only returns posts authored by the account — not collaboration posts — and tokens expire, so the feed needed to degrade gracefully rather than break.

Graceful Degradation

Curated fallback images in public/images/ are shown whenever the token is missing, invalid, or the live feed is empty — and are appended once the live feed runs out.

A separate token-refresher service rotates the 60-day Instagram access token automatically every 14 days.

Shipping It for Real — Self-Hosted, Not Vercel

The site is deployed as a small Docker stack (Next.js standalone + token-refresher) behind bonitasites-edge, a Caddy reverse proxy shared across the cafe's other sites — infrastructure my teammate Jerome built and maintains. Getting the app to run cleanly in that setup meant building it as a standalone Next.js output and keeping the Instagram integration resilient to a headless, no-Vercel environment.

CI/CD Pipeline

Push to prod triggers: npm ci → lint → tsc --noEmit → next build → rsync to the server → docker compose up --build -d.

The app publishes no host ports — the shared edge Caddy is the only public entrypoint, handling TLS for bonitacafe.no.

Challenges & Solutions

Live Instagram Feed Without Breaking the Page

Challenge: Instagram tokens expire, the API excludes collab posts, and the feed can come back empty — any of which could leave the gallery broken.

Solution: Built a curated fallback gallery that kicks in automatically, paired with an ISR revalidate window and an automated 14-day token rotation service so the live feed rarely lapses in the first place.

Self-Hosting Without Vercel

Challenge: The site needed to run on the cafe's own infrastructure, sharing a server and TLS termination with other Bonita sites rather than each having its own public entrypoint.

Solution: Jerome built and maintains the Docker + Caddy edge infrastructure; my part was making sure the app itself — the Instagram integration especially — behaved correctly as a standalone build in that shared, no-Vercel environment.

A Chalkboard Menu That's Still Accessible

Challenge: The brand called for a playful, hand-drawn chalkboard look for the menu, which risked working against usability for a real menu customers need to read quickly.

Solution: Kept the visual styling in Tailwind's design layer while the tab navigation itself follows standard accessible patterns — real prices, clear categories, and no illegible decorative fonts on actual menu text.

Planned Enhancements

Table Reservations

Add an online reservation flow directly on the site instead of relying on phone or Instagram DMs.

Bilingual Routing

Formalise the current English/Norwegian content into proper localised routes instead of a single mixed-language page.

Loyalty Integration

Connect the gift card and ordering flows to a lightweight loyalty or rewards system for regulars.