Built from Scratch.
A bilingual Next.js agency website with a custom CMS backoffice, built solo from architecture to deployment.
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.
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.
Tech Stack
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.
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.
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.
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.
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.
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.
Feature Highlights
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.
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.
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.
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.
Architecture Decisions
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.
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.
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.
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
Architecture and Setup
App Router scaffolding, custom i18n system, Supabase SSR client, middleware for locale detection and auth, and the dynamic [lang] routing structure.
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.
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.