Home
All projects

Aeromir

Corporate website for an HVAC engineering company

Role

Full-stack Developer (Frontend + Server BFF Layer)

Year

2026

Stack

Nuxt 4 / Vue 3 / TypeScriptNuxt UI v4GSAP 3 (MotionPathPlugin)vee-validate + ZodWordPress REST API (headless CMS, ACF, Yoast SEO)Nitro (BFF)

Aeromir

The task

The company needed a website that could solve two conflicting requirements simultaneously:

  1. Provide a high-performance, modern, and SEO-friendly storefront to showcase completed projects and services.
  2. Enable the marketing team to independently manage and edit content (articles, pricing, case details) without requiring developer intervention for every minor update.

This led to the architectural decision to decouple the project into Headless WordPress (for robust content and media management) and Nuxt 4 (for rendering, interactivity, and speed), while ensuring that neither WordPress credentials nor raw, heavy REST payloads ever reach the browser.

Stages

01

Architecture and Site Map

  • Designed the page structure: services, portfolio with type filtering, SEO pricing landing pages, and articles.
  • Established a strict separation between the Nuxt app (app/) and the Nitro server (server/) from the outset, ensuring WordPress acts solely as an isolated data source rather than a direct access point for the browser.
02

BFF Layer Development over WordPress REST

  • Set up server routes proxying the WP REST API via Basic Auth (Application Passwords).
  • Wrote typed transformers that convert raw WordPress responses (including nested _embedded media and ACF fields) into flat, clean TypeScript models (Project, Article).
03

Component System and Accessibility

  • Translated the Figma design into reusable Vue components. Configured Tailwind v4 in CSS-first mode (@theme in main.css).
  • Built the contact form using vee-validate + Zod, integrating accessibility standards directly into the base UiFormField component (automatic generation of aria-invalid and aria-describedby for errors).
04

GSAP Animation Engineering

  • Developed the complex "Work Process" section using GSAP's MotionPathPlugin. Created three independent paths tailored to different breakpoints (mobile/tablet/desktop).
  • Added support for lazy loading animations via IntersectionObserver and implemented a robust resize handler to ensure smooth performance on mobile browsers.
05

Forms and accessibility

  • Built a form using vee-validate and Zod with a typed validation schema (name, phone number validated by digit count, and an optional email validated via regex).
  • The modal is triggered programmatically using Nuxt UI’s useOverlay(), allowing the same form component to be reused with varying contextual text (e.g., a CTA in the "Work Stages" section versus specific CTAs on team member cards).

A shared UiFormField component automatically handles aria-invalid, aria-describedby, and role="alert" for errors—making accessibility a built-in feature of the reusable component rather than a separate task.

The process

Technology Stack

LayerTechnologies
FrontendNuxt 4, Vue 3, TypeScript, Tailwind CSS v4, Nuxt UI v4
AnimationsGSAP 3 (including MotionPathPlugin)
Forms & Validationvee-validate + Zod
CMS (Backend)WordPress REST API (headless, ACF, Yoast SEO)
BFF LayerNitro (server routes, cachedEventHandler)

About the Project & Task

"Aero-mir" is a full-cycle climate systems engineering company based in Novosibirsk (on the market since 2011).

The business needed to solve two conflicting tasks: to have a modern, fast, sales-driven storefront with solid SEO, while allowing the marketing team to retain their familiar admin panel to independently edit articles, pricing, and portfolios. This led to the architectural decision: splitting the project into Headless WordPress (database and media) and Nuxt 4 (rendering, interactivity, SEO). I am building the site from scratch—from designing the BFF layer to UI development and complex animations.


Under the Hood

BFF Layer Over WordPress REST API

The golden rule of this project is that the browser never communicates with WordPress directly. I separated the logic into the Nuxt application (app/) and the Nitro server (server/). The Nitro server routes act as a BFF (Backend-for-Frontend): they proxy requests via Basic Auth, fetch only the necessary fields from the CMS (via _fields and _embed) to avoid sending bloated JSON to the client, and serve clean data to the frontend.

I wrote a layer of typed transformers: raw WP data (ACF fields, nested media, Yoast SEO) is transformed on the fly into strict TypeScript models (Project, Article). The frontend doesn't need to guess if an object contains a required field—it simply works with ready-made interfaces.

For optimization, I wrapped the list fetching in a cachedEventHandler and cached the category slug to ID resolution in the process memory. Now, WordPress isn't hit every time a page is opened:

typescript
const categoryCache = new Map<string, number>();

export async function getCategoryId(slug: string): Promise<number> {
   if (categoryCache.has(slug)) return categoryCache.get(slug)!;

   const categories = await wpFetch<WPTerm[]>('/categories', { query: { slug } });
   if (!categories[0]) throw createError({ statusCode: 500, message: `Category not found` });

   categoryCache.set(slug, categories[0].id);
   return categories[0].id;
}

Animation as an Engineering Task

The "Work Process" section is a complex scene powered by the GSAP MotionPathPlugin, where a mascot moves along a curved path. To ensure this works flawlessly across all devices:

  • Three separate SVG paths were drawn (for mobile, tablet, and desktop) instead of relying on a single path with CSS transformations. Progress points are synchronized individually for each breakpoint.
  • A clever resize handler was implemented. In mobile Safari, scrolling hides the address bar, triggering a resize event that typically janks animations. I wrote a handler that restarts GSAP only if the viewport width or layout mode actually changes, ignoring height jumps.
  • GSAP and its plugins are loaded via dynamic import() exclusively upon IntersectionObserver triggers. On pages without animations, these ~30kb simply do not make it into the bundle.
typescript
let resizeRaf = 0;
const onResize = () => {
   if (resizeRaf) cancelAnimationFrame(resizeRaf);
   resizeRaf = requestAnimationFrame(() => {
      const nextWidth = window.innerWidth;
      const nextLayoutMode = getLayoutMode();
      
      // Ignore viewport height-only changes (hack for iOS Safari)
      if (nextWidth === lastViewportWidth && nextLayoutMode === lastLayoutMode) return;
      
      lastViewportWidth = nextWidth;
      lastLayoutMode = nextLayoutMode;
      initAnimation();
   });
};

Custom UI Solutions

I wrote a useHeader composable that locks scrolling when the mobile menu is open without the visual "jump" of content (by compensating for the vanishing scrollbar width via padding-right). Infinite partner carousels are built purely with CSS, featuring a fallback for prefers-reduced-motion. Forms are built with vee-validate + Zod, and accessibility features (aria-invalid, role="alert") are baked directly into the base UI components.


Current Status & Results

The site is in active development. The BFF layer logic is fully complete, along with 9 sections of the homepage (from the hero block to the portfolio showcase and the footer with the contact form). Internal pages (portfolio catalog with filters, articles, services) are currently being finalized. The expected delivery is within the coming weeks.

The results

9

fully implemented and animated sections on the homepage

3

unique SVG paths for adaptive work process animations

0

WP credentials or raw REST responses sent to the browser (absolute data isolation via the BFF)

Screens