Skip to content

Frontend Overview

The Volare frontend is built with Astro 5 and Tailwind CSS v4, providing optimal performance and SEO-friendly travel product pages.

ComponentTechnologyVersion
FrameworkAstro5.x
StylingTailwind CSS4.x
RuntimeNode.js18+
Package Managerpnpm8+
DeploymentCloudflare Pages-
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)
  • 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
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.json
src/pages/index.astro -> /
src/pages/about.astro -> /about
src/pages/contact.astro -> /contact
src/pages/[market]/index.astro -> /{market}
src/pages/[market]/[...path].astro -> /{market}/{tourPath}/{slug}
-> /{market}/{lang}/{tourPath}/{slug}
src/pages/[market]/about-us.astro -> /{market}/about-us
src/pages/[market]/destinations/[country].astro -> /{market}/destinations/{country}
src/pages/[market]/booking/[reference].astro -> /{market}/booking/{reference}

Product pages use localized tour path slugs from market configuration:

PatternExampleDescription
/{market}/{tourPath}/{slug}/es/circuito/to-maldivasDefault language
/{market}/{lang}/{tourPath}/{slug}/es/ca/circuit/to-maldivasSpecific 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

src/pages/health.ts -> /health (JSON response)
---
// 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>
---
import MainLayout from '../layouts/MainLayout.astro';
---
<MainLayout title="Page Title">
<main>
<h1>Content here</h1>
</main>
</MainLayout>

Tailwind v4 uses CSS-first configuration:

src/styles/global.css
@import "tailwindcss";
@theme {
--color-primary: #3b82f6;
--color-secondary: #64748b;
}
<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>
Terminal window
# Start development server
pnpm dev
# Build for production
pnpm build
# Preview production build
pnpm preview

The frontend runs at http://localhost:4321 by default.

---
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>

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}
/>
---
// In layout or page
const { title, description } = Astro.props;
---
<head>
<title>{title} | Volare Travel</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
</head>
<script type="application/ld+json">
{JSON.stringify({
"@context": "https://schema.org",
"@type": "Product",
"name": product.title,
"description": product.description
})}
</script>

Product detail pages use a magazine-style editorial layout with an interactive trip configurator in a slide-out drawer.

SectionComponentDescription
Heroviaje/HeroFull-bleed hero with breadcrumbs, country label, duration, price, and Reserve CTA
TextItineraryviaje/TextItineraryDay-by-day itinerary with background image
InfoSectionviaje/InfoSectionAccordion with highlights, meals, accommodation details, not-included
AccommodationCarouselviaje/AccommodationCarouselHorizontally scrollable accommodation cards
ImageSectionviaje/ImageSectionGallery grid (single or triple layout)
Microrelatodestination/MicrorelatoSectionGuest story section (from product translation, shared with product pages)
QandASectionviaje/QandASectionTabbed FAQ categories with accordion
FloatingActionsProductFloatingActionsSticky Reserve/Details buttons + TravelDetailsModal + ConfiguratorDrawer
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 modal

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
  • ConfiguratorDrawer renders TripConfigurator inside the Drawer component
  • Drawer component handles escape key, backdrop click, and body scroll lock
src/shared/ui/
└── Drawer/
└── Drawer.tsx # Reusable slide-out panel with motion animations

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/

The homepage landing page provides an immersive brand introduction with animated carousels and editorial content.

/{market}/home
Example: /es/home
SectionDescription
LandingNavbarSplit navigation with centered logo
LandingHeroFull-screen hero carousel with animated text
AboutSectionBrand mission statement
RegionsCarouselInteractive region/country selector with carousel
PhilosophySectionAnimated accordion with brand philosophy
StoryCardGuest story highlight
VideoSectionVideo player with poster image
ExperienceSectionExperience showcase with image carousel
CTASectionCall-to-action with animated text
LandingFooterNewsletter signup and footer links
src/features/landing/
├── types/
│ └── landing.ts # NavItem, Region, LandingLabels, etc.
├── data/
│ └── landingContent.ts # Market-specific label content
├── components/
│ ├── LandingPage.astro # Main page assembler
│ ├── LandingNavbar.astro # Split navigation
│ ├── LandingHero.tsx # Hero carousel (React + Motion)
│ ├── AboutSection.astro # Mission text
│ ├── RegionsCarousel.tsx # Region/country carousel (React + Motion)
│ ├── PhilosophySection.tsx # Animated accordion (React + Motion)
│ ├── StoryCard.astro # Guest story component
│ ├── VideoSection.tsx # Video player (React)
│ ├── ExperienceSection.astro # Experience showcase
│ ├── ExperienceImageCarousel.tsx # Image carousel (React + Motion)
│ ├── CTASection.astro # Call-to-action
│ └── LandingFooter.astro # Footer with newsletter
└── index.ts # Feature exports
  • React + Framer Motion: Carousels and animations use React with the motion library
  • Market-based content: Labels and content load from getLandingLabels(market)
  • Zero JS sections: Static sections use Astro components (AboutSection, StoryCard, etc.)
  • Interactive sections: Carousels and accordions use React islands

Source: frontend/src/features/landing/

Country/destination landing pages showcase editorial experiences for a specific country, using PublicLayout and the PaisExperienceSection components.

/{market}/destinations/{country}
Example: /es/destinations/argentina
SectionDescription
Title + DescriptionCountry 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 ModalExperienceBottomModal — lists linked trips with itinerary and pricing
Navbar + FooterVia PublicLayout
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 # Styles

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

The checkout flow guides users through customizing their trip after selecting an offer.

TripConfigurator → Loading Screen → Flights → Hotels → Activities → Transfers → Contact → Travelers → Summary & Payment → Confirmation
StepRouteDescription
Loading/{market}/checkout/{offerId}/loadingTransition screen with progress animation
Flights/{market}/checkout/{offerId}/flightsSelect flight options
Hotels/{market}/checkout/{offerId}/hotelsSelect hotel accommodations
Activities/{market}/checkout/{offerId}/activitiesSelect optional activity upgrades per day
Transfers/{market}/checkout/{offerId}/transfersSelect optional transfer upgrades per day
Contact/{market}/checkout/{offerId}/contactEnter booking contact person details
Travelers/{market}/checkout/{offerId}/travelersEnter traveler information with inline name validation (ASCII-only)
Summary/{market}/checkout/{offerId}/summaryReview booking and complete payment
Confirmation/{market}/confirmation/{reference}Booking confirmed, trip details and next steps

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 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 types

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

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 }[]

Source: frontend/src/features/checkout/

The frontend deploys to Cloudflare Pages:

Terminal window
# Deploy to staging
pnpm deploy-staging

Build output is in dist/ directory.