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

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

Flights → Hotels → Activities → Transfers → Travelers → Summary & Payment → Confirmation
StepRouteDescription
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
Travelers/{market}/checkout/{offerId}/travelersEnter traveler information (names, documents, contacts)
Summary/{market}/checkout/{offerId}/summaryReview booking and complete payment
Confirmation/{market}/confirmation/{reference}Booking confirmed, trip details and next steps

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, NavigationFooter, HelpWidget
src/features/checkout/
├── types/ # TypeScript interfaces
│ ├── checkout.ts # Shared types (OfferSummary, Currency, Labels)
│ ├── activity.ts # Activity selection types
│ ├── transfer.ts # Transfer selection types
│ ├── traveler.ts # Traveler data types
│ └── summary.ts # Summary page types
├── components/
│ ├── ActivitySelectionPage.tsx
│ ├── TransferSelectionPage.tsx
│ ├── TravelerDataPage.tsx
│ ├── SummaryPaymentPage.tsx
│ ├── ActivitySelector/ # Day sections, cards
│ ├── TransferSelector/ # Day sections, cards
│ ├── TravelerForm/ # Traveler input forms
│ ├── Summary/ # Summary page components
│ ├── StickyHeader.tsx # Trip summary header
│ ├── 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.