Ápice HQ
Two very different businesses under one brand — audiovisual production and digital infrastructure — and a site that sends each visitor down the right funnel before the first call.
- Client
- Ápice HQ
- Year
- 2026
- Role
- Web architecture, UI/UX design, conversion strategy
- React
- Vite
- Design system
- Accessibility
Context
Ápice combines two distinct businesses under one brand: audiovisual production (Ápice Filmmaking) and digital infrastructure, automation and AI (Ápice Infra).
The problem
Communicate two lines of business without confusing the prospect. Someone looking for a video shouldn't get lost among automation services — and someone looking for automation shouldn't have to scroll past a showreel to find it.
Architecture
- Modular structure. One landing page per division, each with its own funnel, tied together by a common brand narrative and a shared contact page.
- Visual direction. Editorial typography, high contrast and restrained motion, on a custom design system built with React, Tailwind and Radix primitives.
- Accessibility. A skip-to-content link, visible focus rings that adapt to dark sections, ARIA on the navigation and mobile menu, a labeled contact form with a live status region, and
prefers-reduced-motionrespected across the site's animations. - Performance on a heavy site. The 3D hero is split into its own chunk and loaded lazily, and its render loop pauses whenever it scrolls out of view. Images get multi-width WebP sets,
loading="lazy"below the fold,fetchpriority="high"on the one that decides the largest paint, and explicit dimensions so nothing shifts while loading. Code is split per route. - Infrastructure. A React single-page app where every route gets its own prerendered HTML file with its title, description and Open Graph tags, plus a preload of that route's code. It's served as static files by nginx behind Traefik on a VPS, with security headers, pre-compressed assets and self-hosted, cookieless analytics.
Key decisions & trade-offs
Decision
Separate funnels per division
Each lead arrives already classified — production or software — which simplifies the sale: the first conversation already starts in the right place.
Trade-off · Two funnels to maintain and measure instead of one.
Decision
A custom design system
Two divisions with different audiences need to look like one company. A shared system of type, color and components keeps them consistent without making them identical.
Trade-off · More upfront design work than using a component library's defaults.
Decision
Static files with a prerendered head per route, instead of a server
A marketing site doesn't need a server deciding anything per request. Google runs JavaScript, but the crawlers behind link previews on LinkedIn, WhatsApp, Slack and X don't — they only read the HTML — so each route ships its own file with its metadata already resolved. nginx serving static files is fast, cheap and hard to break.
Trade-off · Page bodies render in the browser, so they depend on JavaScript; every content change needs a build and a deploy.
Decision
Self-hosted analytics without cookies
Umami, on our own server, measures traffic without cookies or persistent identifiers — so the site needs no consent banner, and visitor data doesn't go to a third party.
Trade-off · One more service to run and back up.
The hard part
After a deploy, returning visitors could get a blank page.
Why it happened. Each route's index.html was served without a Cache-Control header. The deploy syncs files with rsync, which preserves modification times, so browsers applied heuristic caching and kept the old HTML. That old HTML pointed at JavaScript chunks with content hashes in their names — and the deploy's --delete had just removed them from the server. Old HTML, missing scripts: a blank page.
The fix. Two cache policies, decided by path:
/assets/— content-hashed files whose name changes when their content does — are cached forever asimmutable.- Everything else (each route's HTML, the 404 page,
robots.txt, the sitemap) isno-cache, must-revalidate: the browser can keep a copy, but must check it's current on every visit.
The trap inside the fix. In nginx, add_header isn't inherited by a location block that declares its own. Setting Cache-Control inside the two locations would have silently dropped every security header from exactly those responses — which is all of them. The policy is resolved with a map instead and emitted once at the server level, next to the security headers, so nothing is overridden.
Results
- Lighthouse · Accessibility (mobile)
- 100 / 100
- Lighthouse 13, 3 runs, September 2026.
- Lighthouse · Best Practices and SEO (mobile)
- 100 / 100
- Same runs, both categories.
- Cumulative Layout Shift
- 0
- Same runs — every image has explicit dimensions.
Stack
- React
- TypeScript
- Vite
- React Router
- Tailwind CSS · Radix
- GSAP
- three.js
- nginx · Traefik
- Umami