Frontend Overview
The Volare frontend is built with Astro 5 and Tailwind CSS v4, providing optimal performance and SEO-friendly travel product pages.
Technology Stack
Section titled “Technology Stack”| Component | Technology | Version |
|---|---|---|
| Framework | Astro | 5.x |
| Styling | Tailwind CSS | 4.x |
| Runtime | Node.js | 18+ |
| Package Manager | pnpm | 8+ |
| Deployment | Cloudflare Pages | - |
Architecture
Section titled “Architecture”Frontend├── Pages (File-based routing)│ ├── Static pages (Astro components)│ ├── Dynamic routes ([market], [slug])│ └── API endpoints (.ts files)├── Layouts (Shared structure)│ └── MainLayout.astro├── Components (Reusable UI)│ └── Feature-specific components└── Shared (Utilities & config)Key Features
Section titled “Key Features”- Zero JavaScript by default - Astro components ship no JS unless needed
- File-based routing - Pages map directly to URLs
- Dynamic routes - Market and product slug parameters
- SSR/SSG flexibility - Server or static rendering per page
- Tailwind CSS v4 - Utility-first styling with CSS variables
Project Structure
Section titled “Project Structure”frontend/├── src/│ ├── components/ # Reusable Astro components│ ├── features/ # Feature-specific components│ ├── layouts/ # Page layouts│ │ └── MainLayout.astro│ ├── pages/ # File-based routing│ │ ├── index.astro│ │ ├── health.ts│ │ ├── [market]/ # Dynamic market routes│ │ └── products/│ ├── shared/ # Shared utilities│ └── styles/ # Global styles├── public/ # Static assets├── astro.config.mjs # Astro configuration└── package.jsonRouting
Section titled “Routing”All market-scoped pages live under src/pages/[market]/ and use English segment names as file routes. For markets with translated URL segments (currently ES), middleware rewrites translated URLs to the English file routes. See i18n Routing for the full translation system.
File Routes
Section titled “File Routes”src/pages/[market]/index.astro -> /{market} (home)src/pages/[market]/home.astro -> /{market}/homesrc/pages/[market]/destinations/[country].astro -> /{market}/destinations/{country}src/pages/[market]/regions/[slug].astro -> /{market}/regions/{slug}src/pages/[market]/about-us.astro -> /{market}/about-ussrc/pages/[market]/terms.astro -> /{market}/termssrc/pages/[market]/privacy-policy.astro -> /{market}/privacy-policysrc/pages/[market]/cookie-policy.astro -> /{market}/cookie-policysrc/pages/[market]/legal.astro -> /{market}/legalsrc/pages/[market]/collections/[slug].astro -> /{market}/collections/{slug}src/pages/[market]/celebrity/ -> /{market}/celebrity/...src/pages/[market]/[...path].astro -> /{market}/{tourPath}/{slug} (product catch-all)Translated URLs (ES Market)
Section titled “Translated URLs (ES Market)”For the ES market, the first URL segment is translated. Middleware handles the rewrite transparently:
| User-facing URL | Internal file route |
|---|---|
/es/destinos/argentina | /es/destinations/argentina |
/es/sobre-nosotros | /es/about-us |
/es/terminos-condiciones | /es/terms |
English-language markets (US, UK, CA) and DE use English segments as-is.
Product Detail Routes
Section titled “Product Detail Routes”Product pages use localized tour path slugs from market configuration:
| Pattern | Example | Description |
|---|---|---|
/{market}/{tourPath}/{slug} | /es/circuito/to-maldivas | Default language |
/{market}/{lang}/{tourPath}/{slug} | /es/ca/circuit/to-maldivas | Specific language |
The tourPath segment is configured per-market and language (e.g., “circuito” for Spanish, “circuit” for Catalan).
Preview Mode: Product pages accept ?preview=<token> to display draft products using signed API URLs.
Source: frontend/src/pages/[market]/[...path].astro
API Endpoints
Section titled “API Endpoints”src/pages/health.ts -> /health (JSON response)Astro Components
Section titled “Astro Components”Basic Component
Section titled “Basic Component”---// Component script (runs at build time)interface Props { title: string; description?: string;}
const { title, description } = Astro.props;---
<article class="product-card"> <h2>{title}</h2> {description && <p>{description}</p>}</article>
<style> .product-card { @apply p-4 rounded-lg shadow-md; }</style>Layout Usage
Section titled “Layout Usage”---import MainLayout from '../layouts/MainLayout.astro';---
<MainLayout title="Page Title"> <main> <h1>Content here</h1> </main></MainLayout>Styling with Tailwind CSS v4
Section titled “Styling with Tailwind CSS v4”Configuration
Section titled “Configuration”Tailwind v4 uses CSS-first configuration:
@import "tailwindcss";
@theme { --color-primary: #3b82f6; --color-secondary: #64748b;}Usage in Components
Section titled “Usage in Components”<div class="bg-primary text-white p-4 rounded-lg"> <h1 class="text-2xl font-bold">Title</h1> <p class="text-secondary">Description</p></div>Development
Section titled “Development”Commands
Section titled “Commands”# Start development serverpnpm dev
# Build for productionpnpm build
# Preview production buildpnpm previewDevelopment Server
Section titled “Development Server”The frontend runs at http://localhost:4321 by default.
API Integration
Section titled “API Integration”Fetching from Backend
Section titled “Fetching from Backend”---const market = Astro.params.market;const lang = 'en';
const response = await fetch( `${import.meta.env.API_URL}/api/${market}/${lang}/products`);const { data: products } = await response.json();---
<ul> {products.map((product) => ( <li> <a href={`/${market}/products/${product.url_slug}`}> {product.title} </a> </li> ))}</ul>Image Optimization
Section titled “Image Optimization”Use Astro’s <Image /> component for optimized images:
---import { Image } from 'astro:assets';import heroImage from '../assets/hero.jpg';---
<Image src={heroImage} alt="Hero image" width={1200} height={600}/>Canonical and hreflang Tags
Section titled “Canonical and hreflang Tags”All pages using PublicLayout or MainLayout automatically get canonical and hreflang tags via the shared SeoHead.astro component. This generates:
<link rel="canonical">pointing to the current market’s translated URL<link rel="alternate" hreflang="...">for all 5 markets (es-ES, de-DE, en-US, en-GB, en-CA) plusx-default(ES)
No manual hreflang work is needed when using the standard layouts. The production domain for absolute URLs is https://byvolare.com.
Source: frontend/src/shared/ui/SeoHead.astro, frontend/src/shared/config/i18n.ts
Structured Data
Section titled “Structured Data”Product pages include JSON-LD structured data generated from the product API response.
Product Pages
Section titled “Product Pages”Product detail pages use a magazine-style editorial layout with an interactive trip configurator in a slide-out drawer.
Page Sections
Section titled “Page Sections”| Section | Component | Description |
|---|---|---|
| Hero | viaje/Hero | Full-bleed hero with breadcrumbs, country label, duration, price, and Reserve CTA |
| TextItinerary | viaje/TextItinerary | Day-by-day itinerary with background image |
| InfoSection | viaje/InfoSection | Accordion with highlights, meals, accommodation details, not-included |
| AccommodationCarousel | viaje/AccommodationCarousel | Horizontally scrollable accommodation cards |
| ImageSection | viaje/ImageSection | Gallery grid (single or triple layout) |
| Celebrity | home/CelebritySection | Celebrity spotlight section (from linked CmsCelebrityPage); replaces gallery when present |
| QandASection | viaje/QandASection | Tabbed FAQ categories with accordion |
| FloatingActions | ProductFloatingActions | Sticky Reserve/Details buttons + TravelDetailsModal + ConfiguratorDrawer |
Product Page Architecture
Section titled “Product Page Architecture”src/features/product/├── types/│ ├── product.ts # Product API response interfaces│ └── configurator.ts # TripConfigurator types├── mappers/│ └── productToViajeProps.ts # Maps API data to Viaje component props├── components/│ ├── ProductPage.astro # Main page assembler (uses Viaje components)│ ├── ProductFloatingActions.tsx # Composes FloatingButtons + TravelDetailsModal + ConfiguratorDrawer│ ├── ConfiguratorDrawer/ # Slide-out configurator wrapper│ │ └── ConfiguratorDrawer.tsx│ ├── TripConfigurator/ # Trip configuration form│ │ └── TripConfigurator.tsx│ └── ReserveButton.tsx # Opens configurator drawer└── index.ts
src/components/viaje/ # Shared Viaje design components├── Hero/ # Hero with breadcrumbs, label, CTA├── TextItinerary/ # Day-by-day itinerary├── InfoSection/ # Accordion-based info section├── AccommodationCarousel/ # Accommodation cards carousel├── ImageSection/ # Single or triple image gallery├── QandASection/ # Tabbed FAQ accordion├── FloatingButtons/ # Sticky bottom action buttons└── ExperienceModal/ # Experience detail modalState Management
Section titled “State Management”ProductFloatingActions is a single React island that composes all interactive product page elements: FloatingButtons, TravelDetailsModal, and ConfiguratorDrawer. It manages open/close state locally via React state.
- Hero CTA and FloatingButtons “Reserve” button open the configurator drawer
- FloatingButtons “Details” button opens the TravelDetailsModal
ConfiguratorDrawerrendersTripConfiguratorinside theDrawercomponentDrawercomponent handles escape key, backdrop click, and body scroll lock
Shared UI Components
Section titled “Shared UI Components”src/shared/ui/└── Drawer/ └── Drawer.tsx # Reusable slide-out panel with motion animationsLayout
Section titled “Layout”Product pages use PublicLayout.astro — the standard public layout with header and footer. The product page route ([...path].astro) fetches both the product data and leading price, then passes them to ProductPage.astro. A mapper (productToViajeProps.ts) transforms the API response into props for each Viaje design component.
Data flow: [...path].astro fetches product + leading price -> ProductPage.astro maps via mapProductToViajeProps() -> renders Viaje components (Hero, TextItinerary, InfoSection, etc.) + ProductFloatingActions (React island with configurator drawer).
Source: frontend/src/features/product/
Homepage
Section titled “Homepage”The homepage is fully CMS-managed, with all sections fetched from the backend API. Region data comes from a separate endpoint.
Homepage Route Pattern
Section titled “Homepage Route Pattern”/{market}/ (rewrites internally to /{market}/home)/{market}/home (direct access also works)Example: /es/Homepage Sections
Section titled “Homepage Sections”| Section | Component | Data Source |
|---|---|---|
| Hero | HomeHero | cms/home — hero section |
| Contact Intro | HomeContactSection | cms/home — contactIntro section |
| Regions | RegionInfoSection | cms/regions — separate endpoint |
| Accordion | HomeAccordionSection | cms/home — accordion section |
| Celebrity | CelebritySection | cms/home — celebrity section (poster + trailers) |
| Experience Carousel | HomeExperienceCarousel | cms/home — experienceCarousel section |
| Contact Closing | HomeContactClosing | cms/home — contactClosing section (opens ContactModal drawer) |
Homepage Architecture
Section titled “Homepage Architecture”src/pages/[market]/home.astro # Page: fetches CMS data, renders sectionssrc/features/landing/data/cmsHomeMapper.ts # Maps API response to component propssrc/components/home/├── HomeHero/ # Full-screen hero with image/video├── HomeContactSection/ # Contact CTA (intro: link, closing: opens drawer)├── HomeContactClosing/ # React wrapper: opens ContactModal with form + toast├── RegionInfoSection/ # Region cards with country links├── HomeAccordionSection/ # Expandable content items├── CelebritySection/ # Celebrity spotlight with trailer modal└── HomeExperienceCarousel/ # Slide-based experience showcaseTechnical Notes
Section titled “Technical Notes”- CMS-driven: All content comes from
GET /{market}/{lang}/cms/homeandGET /{market}/{lang}/cms/regions - Contact Closing drawer: The bottom CTA uses
HomeContactClosing(React island withclient:visible) which opens aContactModaldrawer with a contact form. Submissions POST to the backend Leads API (/api/leads) vialeadApi.ts, creating a Lead record for CRM follow-up. Region options for the form are derived from the already-fetched regions data. See Leads API for the endpoint contract - Graceful degradation: Each section is independently optional; if data is missing, the section is omitted
- Parallel fetching: Home and regions API calls run concurrently via
Promise.allSettled - Tag icons: Accordion and experience carousel icons are hardcoded in the frontend mapper, not CMS-managed
Source: frontend/src/pages/[market]/home.astro, frontend/src/features/landing/data/cmsHomeMapper.ts
Destination Pages
Section titled “Destination Pages”Country/destination landing pages showcase editorial experiences for a specific country, using PublicLayout and the PaisExperienceSection components.
Destination Route Pattern
Section titled “Destination Route Pattern”/{market}/destinations/{country}Example: /es/destinations/argentinaDestination Page Sections
Section titled “Destination Page Sections”| Section | Description |
|---|---|
| Title + Description | Country name and editorial description |
| Experience Cards (Desktop) | PaisExperienceSection — sticky 3-column gallery with scroll animations |
| Experience Cards (Mobile) | PaisExperienceMobileSection — full-height snap-scroll cards |
| Trip Modal | ExperienceBottomModal — lists linked trips with itinerary and pricing |
| Navbar + Footer | Via PublicLayout |
Destination Architecture
Section titled “Destination Architecture”src/components/pais/├── PaisExperienceSection/│ ├── PaisExperienceSection.astro # Astro wrapper (desktop)│ ├── PaisExperienceSection.react.tsx # React component│ ├── PaisExperienceSection.types.ts # Type definitions│ └── PaisExperienceSection.module.css # Styles└── PaisExperienceMobileSection/ ├── PaisExperienceMobileSection.astro # Astro wrapper (mobile) ├── PaisExperienceMobileSection.react.tsx # React component └── PaisExperienceMobileSection.module.css # StylesData Flow
Section titled “Data Flow”Country data is fetched server-side from the CMS API. Editorial experience content (title, tag, description, image) is stored per-locale in cms_country_translations.experiences JSON. Each experience references a product_template_id which is resolved to the matching ProductByMarket per requested market.
Source: frontend/src/pages/[market]/destinations/[country].astro
Checkout Flow
Section titled “Checkout Flow”The checkout flow guides users through customizing their trip after selecting an offer.
Flow Steps
Section titled “Flow Steps”TripConfigurator → Loading Screen → Flights → Hotels → Activities → Transfers → Contact → Travelers → Summary & Payment → Confirmation| Step | Route | Description |
|---|---|---|
| Loading | /{market}/checkout/{offerId}/loading | Transition screen with progress animation |
| Flights | /{market}/checkout/{offerId}/flights | Select flight options |
| Hotels | /{market}/checkout/{offerId}/hotels | Select hotel accommodations |
| Activities | /{market}/checkout/{offerId}/activities | Select optional activity upgrades per day |
| Transfers | /{market}/checkout/{offerId}/transfers | Select optional transfer upgrades per day |
| Contact | /{market}/checkout/{offerId}/contact | Enter booking contact person details |
| Travelers | /{market}/checkout/{offerId}/travelers | Enter traveler information with inline name validation (ASCII-only) |
| Summary | /{market}/checkout/{offerId}/summary | Review booking and complete payment |
| Confirmation | /{market}/confirmation/{reference} | Booking confirmed, trip details and next steps |
Loading Screen
Section titled “Loading Screen”The loading screen provides a branded transition between the trip configurator and checkout:
- Full-screen background with destination imagery
- Loading card with animated progress bar
- Rotating tips displaying travel-related messages
- 3-second minimum display ensures smooth UX
- Auto-redirect to flights page when ready
The TripConfigurator redirects to the loading page after offer creation, which then forwards to the checkout flow.
Source: frontend/src/features/checkout-loading/
Checkout Architecture
Section titled “Checkout Architecture”Checkout pages share a common architecture:
- Astro page - Server-rendered wrapper passing props to React
- React page component - Handles state, API calls, navigation
- Selector components - Day-by-day selection UI
- Shared components -
StickyHeader(with summary drawer),NavigationFooter,HelpWidget
src/features/checkout-loading/├── types/│ └── loading.ts # Loading screen types├── data/│ └── loadingContent.ts # Rotating tips content├── components/│ ├── CheckoutLoadingScreen.tsx # Main loading screen│ ├── LoadingCard.tsx # Card with progress and tips│ ├── ProgressBar.tsx # Animated progress bar│ └── RotatingTips.tsx # Cycling tip messages└── index.ts
src/features/checkout/├── types/ # TypeScript interfaces│ ├── checkout.ts # Shared types (OfferSummary, Currency, Labels)│ ├── hotel.ts # Hotel selection types (3-tier selector)│ ├── activity.ts # Activity selection types│ ├── transfer.ts # Transfer selection types│ ├── traveler.ts # Traveler data types│ └── summary.ts # Summary page types├── components/│ ├── HotelSelectionPage.tsx│ ├── ActivitySelectionPage.tsx│ ├── TransferSelectionPage.tsx│ ├── TravelerDataPage.tsx│ ├── SummaryPaymentPage.tsx│ ├── HotelSelector/ # Tabbed 3-tier hotel cards│ ├── ActivitySelector/ # Day sections, cards│ ├── TransferSelector/ # Day sections, cards│ ├── TravelerForm/ # Traveler input forms│ ├── Summary/ # Summary page components│ ├── StickyHeader.tsx # Trip info header with summary button│ ├── CheckoutSummaryDrawer.tsx # Slide-out summary with price breakdown│ ├── NavigationFooter.tsx # Back/Continue buttons│ └── HelpWidget.tsx # Support contact└── api/ └── checkoutApi.ts # Session persistence
src/features/confirmation/├── components/│ ├── ConfirmationPage.tsx # Main page component│ ├── HeroColumn.tsx # Left column with destination image│ ├── DetailsColumn.tsx # Right column with booking details│ ├── ChecklistItem.tsx # Individual checklist items│ └── ActionButtons.tsx # Download/share actions└── types/ └── confirmation.ts # Confirmation page typesCheckout State Management
Section titled “Checkout State Management”Each selection page uses the reducer pattern:
Map<number, string>tracks one selection per day (dayNumber → optionId)- Selections persist to backend session via checkout API
- Previous selections restore on page load
Session Persistence
Section titled “Session Persistence”Selections save to the checkout session (backend API):
// Activity selections{ activity_id: number, day_number: number, price: number }[]
// Transfer selections{ transfer_id: number, day_number: number, price: number }[]Edit Mode from Summary
Section titled “Edit Mode from Summary”The summary page edit buttons append ?edit=summary to step URLs. When a step page
detects this parameter, it enters edit mode:
- Navigation footer shows Cancel / Save instead of Back / Next
- Save persists selections (
advance: false) and redirects to summary - Cancel redirects to summary without saving
This avoids forcing users through all remaining steps to return to summary after a single edit. The navbar summary drawer does not use edit mode — it keeps the normal step-by-step flow since it is accessible from intermediate steps.
Edit mode utilities live in frontend/src/features/checkout/utils/editMode.ts.
Source: frontend/src/features/checkout/
Deployment
Section titled “Deployment”The frontend deploys to Cloudflare Pages:
# Deploy to stagingpnpm deploy-stagingBuild output is in dist/ directory.