Case Study // 04

Built from Scratch.

A bilingual Next.js agency website with a custom CMS backoffice, built solo from architecture to deployment.

Next.jsTypeScriptTailwind CSSSupabaseFramer MotionVercel
RoleFull-Stack Developer
Timeline2025–2026
ClientCalliope
Live Sitecalliope.pt
About the project

Overview

Calliope is a creative digital agency based in the Algarve, Portugal, offering event coverage, podcast creation, and social media management. I designed and built their website solo: a fully bilingual public site in English and Portuguese, plus a protected content management backoffice so the team can publish and edit blog posts without touching a third-party CMS. No Contentful, no Sanity, no WordPress. The client owns everything.

Challenges

The Challenge

Bilingual SEO, not just a language toggle

A basic language switcher wasn't good enough. Both the English and Portuguese versions of the site needed to be fully indexed by search engines with correct canonical URLs, hreflang alternates, and locale-specific Open Graph metadata. Any shortcut here would mean the Portuguese version effectively didn't exist for search.

A CMS that belongs to the client

The agency wanted full content ownership. No ongoing third-party licence, no platform dependency. That meant building a custom backoffice where the team could draft, edit, and publish blog posts directly, all inside the same Next.js codebase as the public site.

Animations that earn their place

Calliope sells creative services, so the site is part of their pitch. Animations had to feel considered and intentional, not decorative. They also couldn't compromise performance scores.

A fully self-contained stack

No third-party CMS, no external auth provider. The stack had to be lean, owned, and operable by a small non-technical team without ongoing maintenance overhead.

The Solution

I built the entire site in Next.js 16 with the App Router, using a custom i18n system, a Supabase-backed CMS, and a single middleware function handling both locale detection and auth. Every public route is a distinct, server-rendered page with its own metadata. Every backoffice route is protected at the edge. The contact form uses Server Actions and a honeypot, with delivery via Resend. Analytics run through a self-hosted Umami instance embedded in the backoffice dashboard.

Stack

Tech Stack

Application Framework

Next.js 16

App Router with per-route Server Components. Server Actions replaced a separate API layer for form handling and blog CRUD. Each public route generates its own metadata, canonical URL, and hreflang tags independently.

Type Safety

TypeScript

Strict mode throughout. Most useful in the i18n dictionary layer, where missing translation keys are easy to miss at runtime but get caught at build time with proper typing.

Styling

Tailwind CSS 4

Utility-first styling with a custom component library in components/ui/. A cn() helper handles conditional class merging throughout. No inline styles, no CSS modules.

Database and Auth

Supabase

PostgreSQL, Auth, and SSR session management in one service. The SSR-compatible client keeps session state consistent between server and client. Session refresh happens in middleware before any server component reads user data.

Animations

Framer Motion

Used for the splash screen, hero sequences, scroll indicators, and hover microinteractions. Every animated element is a Client Component. Server Components stay fully static.

Deployment

Vercel

Zero-config deployment for Next.js. Edge middleware runs i18n routing and auth in a single pass at the CDN layer, before any server component executes.

Features

Feature Highlights

01

Bilingual i18n with Cookie Persistence

Every user-visible string comes from TypeScript dictionary files, no hardcoded text anywhere on the public site. When a user switches language, a NEXT_LOCALE cookie is set and middleware reads it on every subsequent request. Returning visitors always land in their preferred language, with no client-side hydration required.

02

Custom CMS Backoffice

The backoffice at /backoffice/ is a protected Next.js application inside the same codebase. Middleware blocks unauthenticated access at the edge. Inside, the team can create, edit, and publish blog posts stored in Supabase, and view Umami analytics without leaving the dashboard.

03

Splash Screen with Show-Once Logic

The splash screen animation plays once per browser session using sessionStorage. First visit gets the full sequence: logo reveal, stagger fade, transition out. Subsequent navigations skip it entirely. The premium first impression stays intact without annoying returning visitors.

04

Contact Form with Honeypot Protection

The contact form submits via a Server Action that calls Resend for email delivery. Before sending, the action checks a hidden honeypot field invisible to real users but filled by most bots. No CAPTCHA, no friction for real users. Spam is silently discarded server-side.

Decisions

Architecture Decisions

01

Custom i18n over next-intl

I built the i18n layer from scratch instead of using next-intl. The project has two static locales, no pluralisation complexity, and no ICU messages. A custom TypeScript dictionary with a t() function and a LanguageProvider context covered everything needed, with zero runtime overhead and type-safe keys that surface missing translations at build time rather than silently failing in production.

02

Single middleware for i18n routing and auth

The middleware.ts file does two jobs: locale detection and rewriting for public routes, and Supabase session verification for backoffice routes. Running both in one middleware function means one edge execution per request. The logic branches on pathname: requests to /backoffice/** hit the auth guard, everything else hits the locale resolver.

03

Server Actions over API routes

Contact form submissions and all blog post operations (create, update, publish, delete) are handled by Server Actions in app/actions/. This removed the need for a dedicated API layer. Each action verifies the Supabase session server-side. Client-side auth state is never trusted for privileged operations.

Outcomes

Results

The site is live at calliope.pt, fully bilingual with correct hreflang, canonical URLs, and Open Graph metadata across all public routes.

The team can self-publish blog content via the backoffice with no developer involvement.

Analytics are visible inside the backoffice dashboard via the embedded Umami integration.

Core Web Vitals are strong. Server Components and next/image keep LCP low, with no layout shifts from the animation layer.

Timeline

Phase 01: Foundation

Architecture and Setup

App Router scaffolding, custom i18n system, Supabase SSR client, middleware for locale detection and auth, and the dynamic [lang] routing structure.

Phase 02: Build

Public Site and Backoffice CMS

Full public site implementation including animations, contact form with Resend, bilingual content, and the complete backoffice with blog CRUD, Supabase auth, and Umami analytics.

Phase 03: Launch

Polish and Deployment

Performance tuning, SEO metadata for all routes, honeypot spam protection, cookie-based locale persistence, and deployment to Vercel with custom domain at calliope.pt.

Got a project? Let's talk.