Marketing Site

The public-facing website for Travel Tamers. Built with Astro 5 for static site generation and React 19 islands for interactive features like AI-powered travel chat and multi-step contact forms. Styled with Tailwind 4 using the Dark Sanctuary design system with an elemental design language across all card components.

Architecture Overview

The marketing site follows Astro's islands architecture pattern. Most pages are rendered to static HTML at build time, producing fast, zero-JavaScript pages by default. Interactive components (MilesChat, ContactForm, newsletter signup) are hydrated as React islands only where needed, keeping the overall JavaScript payload small.

Content is managed through Astro content collections with Markdown and MDX files for destinations, experiences, partners, and blog posts. Each collection has a typed schema, so content editors get validation errors before build if required fields are missing.

Homepage hero section with Premium travel personally crafted headline over mountain landscape
Homepage hero — "Premium travel, personally crafted" with mountain hero image

Key Principle: The site ships zero JavaScript for static pages. React only loads on pages that contain interactive islands (chat, forms, search). This keeps Lighthouse scores high and page load times under 1.5 seconds.

Tech Stack

Layer Technology Role
Framework Astro 5 Static site generation, routing, content collections
UI Framework React 19 Interactive islands (MilesChat, ContactForm, etc.)
Styling Tailwind 4 Utility-first CSS with custom design tokens
Animations GSAP Scroll-triggered reveals, entrance animations
Search Pagefind Client-side full-text search across all pages
Typography Self-hosted WOFF2 Playfair Display (headings) + Source Sans 3 (body) — variable fonts, weight 400-700, font-display: swap. No external CDN dependencies.
Icons Custom SVGs Inline SVG elements throughout — no icon library dependency
Analytics GA4 + GTM Traffic analytics and tag management
Hosting nginx (Docker) Static file serving behind Traefik reverse proxy

Directory Structure

site/
├── src/
│   ├── components/       # Astro + React components
│   │   ├── Nav.astro     # 3-way theme toggle (sun/moon/monitor icons)
│   │   ├── Footer.astro
│   │   ├── MilesChat.tsx
│   │   ├── ContactForm.tsx
│   │   └── ...
│   ├── content/          # Astro content collections
│   │   ├── destinations/
│   │   ├── experiences/
│   │   ├── partners/
│   │   └── blog/
│   ├── layouts/          # Page layout templates
│   │   └── BaseLayout.astro  # Flash-prevention script in <head>
│   ├── pages/            # File-based routing
│   └── styles/
│       └── global.css    # Design tokens (1000+ lines), dark mode overrides
├── public/
│   ├── fonts/            # Self-hosted WOFF2 (Playfair Display + Source Sans 3)
│   ├── images/           # Self-hosted partner, experience, and destination images
│   │   ├── partners/     # Partner hero images (all self-hosted)
│   │   └── experiences/  # Experience hero images (all self-hosted)
│   └── js/               # Extracted inline scripts
│       ├── dark-mode.js  # 3-way toggle (light/dark/system)
│       ├── analytics.js
│       ├── cookie-consent.js
│       ├── scroll-progress.js
│       └── buddy-config.js
├── astro.config.mjs
└── package.json

Pages & Routes

Astro uses file-based routing where each .astro file in src/pages/ becomes a URL. Dynamic routes use bracket syntax for content collection pages like [slug].astro.

Route Page Description
/ Home Hero section, destinations grid, experiences preview, partner logos, testimonials (generic fictional names/companies), CTA. "No ticket queues, no runaround" pain point copy. Proper <h3> heading hierarchy throughout.
/destinations/ Destinations Browse all travel destinations with filtering
/destinations/[slug] Destination Detail Rich detail page: country, region, visa info, safety, best time to visit
/experiences/ Experiences 3-column DestinationCard grid layout matching the destinations, partners, and blog listing pages
/experiences/[slug] Experience Detail Expedition cruises, river cruises, luxury resorts, eco-adventures, etc.
/partners/ Partners Browse travel vendor and supplier profiles with partner logos on each card
/partners/[slug] Partner Detail HX Expeditions, NatGeo, AmaWaterways, Ponant, etc. Each detail page includes the partner logo with per-partner logoScale sizing.
/blog/ Blog Travel articles listing with categories. Category filter shows a friendly empty state when no posts match.
/blog/[slug] Blog Post Individual article with rich formatting
/about/ About Company story, team bios (written with natural contractions), Nexion Travel Group affiliation
/contact/ Contact Multi-step contact form (React island)
/how-it-works/ How It Works Service process explanation for potential clients
/las-vegas-golf/ Las Vegas Golf Custom content page for Las Vegas golf packages
/for/[slug] Proposal Pages Company-specific proposal landing pages. Form submits to /webhooks/form-submit with honeypot field; payload sends name, source, companySlug. Page-view tracking hits /api/page-views.
/eclipse/2027 Eclipse 2027 Detailed packages for the August 2027 Eclipse of the Century
/rss.xml RSS Feed Atom/RSS feed of blog posts with autodiscovery link in <head>
/404 Error Custom 404 page with navigation suggestions

Client Pages: All future client-specific pages go under /for/ only. The /clients/ path is legacy. Current pages (sullivan, kelsey, titshaw, alaska-options) remain as-is until they move to booking, then get removed.

Destinations page showing Antarctica, Morocco, and Iceland cards with photos
Destinations page — country cards with Antarctica, Morocco, Iceland

Content Collections

Astro content collections provide type-safe content management using Markdown/MDX files with frontmatter schemas. Each collection is defined in src/content/config.ts with a Zod schema that validates every content file at build time.

Collection

Destinations

Travel destinations with metadata like country, region, continent, visa requirements, safety level, currency, language, best months to visit, and self-hosted hero images.

Collection

Experiences

Travel experience categories: expedition cruises, river cruises, luxury resort stays, eco-adventures, cultural immersions, and more. Each with description, highlights, pricing tiers, and self-hosted hero images in /images/experiences/.

Collection

Partners

Vendor and supplier profiles with partner logos (9 logos, each with a logoScale field for per-partner sizing), self-hosted hero images in /images/partners/, featured itineraries, and commission structures. HX Expeditions, NatGeo, AmaWaterways, Ponant, etc.

Collection

Blog

Travel articles organized by category. Supports rich formatting with MDX components, embedded images, pull quotes, and related destination links. Category filter shows a friendly empty state when no posts match the selected filter.

Adding Content: Create a new .md or .mdx file in the appropriate src/content/ subdirectory. The frontmatter schema will validate your metadata at build time. If any required fields are missing, the build will fail with a clear error message.

Images & Assets

All hero images for partners and experiences are self-hosted in the public/images/ directory, eliminating runtime dependencies on external image CDNs. Destination pages use a mix of self-hosted and curated Unsplash images, all color-graded for the dark palette.

Self-Hosted Image Directories

Directory Contents
/images/partners/ Hero images for all partner detail pages — no Unsplash URL dependencies
/images/experiences/ Hero images for all experience detail pages — no Unsplash URL dependencies
/fonts/ Self-hosted WOFF2 variable fonts (Playfair Display + Source Sans 3)

Partner Logos

Nine partner logos are displayed on both the partner listing cards and individual detail pages. Each partner's content frontmatter includes a logoScale field that controls per-partner logo sizing, ensuring logos of different aspect ratios appear visually balanced. Partner logo SVGs use aria-hidden="true" since they are decorative.

Image Treatments

All hero and card images use the .img-cinematic class, which applies a slight desaturation (saturate(0.88)) and contrast boost (contrast(1.08)) to create a cohesive, editorial look across images from different sources. Featured images also use a Ken Burns effect — a slow 8-second zoom animation that gives static images a sense of depth and movement on hover or when in view.

Interactive Components (React Islands)

While most of the site is static HTML, several components require client-side interactivity. These are built as React 19 components and hydrated using Astro's client:* directives. Each island loads its JavaScript independently, so a user viewing a static destinations page downloads zero React code.

Component Hydration Purpose
MilesChat client:idle AI travel advisor widget (Claude Sonnet powered)
ContactForm client:visible Multi-step form with validation and API submission. Full dark mode support. role="alert" on error messages.
Newsletter client:visible Email signup in the footer. Uses variant="dark" prop for cloud-white text on navy background. Uses res.ok check, aria-label, role="status"/role="alert" for feedback.
OnboardingForm client:visible Client onboarding multi-step form. Full dark mode support. 35+ htmlFor/id pairs, responsive grids, progress bar with role="progressbar", autocomplete attributes.
Accordion client:visible Collapsible FAQ/content sections. Full dark mode support.
Dark Mode Toggle Vanilla JS 3-way toggle (light/dark/system) with sun/moon/monitor icons in Nav.astro. Reads tt_theme from localStorage. prefers-color-scheme listener for system mode.
Cookie Consent Vanilla JS GDPR-style consent banner
Scroll Progress Vanilla JS Reading progress bar at top of page

Hydration Strategy: client:idle loads after the page finishes initial rendering, ideal for non-critical interactive elements like chat. client:visible waits until the component scrolls into view, perfect for below-the-fold forms. Several small behaviors (dark mode, cookies, scroll progress) use vanilla JS extracted to public/js/ to avoid loading React entirely.

MilesChat Widget

MilesChat is the AI-powered travel advisor that appears as a floating action button (FAB) in the bottom-right corner of every page. The FAB is styled as a gold glass speech bubble with a triangular tail created via ::before and ::after pseudo-elements, using backdrop-filter: blur() for the glass effect. The button text is responsive: "Miles" on mobile, "Chat with Miles" on desktop. When clicked, it opens a modal chat interface where visitors can ask travel questions and get personalized recommendations.

How It Works

Visitor clicks the gold speech-bubble FAB

The FAB is fixed to the bottom-right corner with a gold glass finish and triangular tail. Text reads "Miles" on mobile viewports or "Chat with Miles" on larger screens.

Chat modal opens with greeting

The modal presents a conversational interface with the greeting: "Hey there — I'm Miles, your travel concierge." The modal has role="dialog", aria-modal="true", aria-live on the message area, a focus trap, and Escape key to close.

Messages sent to the API

User messages are sent to the Hono API, which forwards them to Claude Sonnet with a system prompt containing Travel Tamers context.

Claude Sonnet via API

AI responds with travel advice

Responses are streamed back to the chat interface. Responses are capped at 250 tokens for concise answers. Scrolling uses offsetTop for reliability.

Can auto-fill and submit contact form

If the conversation leads to a booking inquiry, MilesChat can programmatically fill in the contact form with information gathered during the chat and submit it.

Creates contact + deal + Slack channel

Accessibility

  • role="dialog" and aria-modal="true" on the chat modal
  • aria-live region on the message container for screen reader announcements
  • Focus trap keeps keyboard navigation inside the open modal
  • Escape key closes the modal
  • iOS zoom fix: font-size: max(.92rem, 16px) on inputs prevents unwanted zoom on focus
Las Vegas Golf experience page showing course listings with MilesChat FAB visible
Las Vegas Golf page with MilesChat widget visible (gold speech-bubble FAB, bottom right)

Rate Limiting: MilesChat is rate limited to 20 requests per minute per IP address to prevent abuse. The rate limit is enforced at the API layer. Users who exceed the limit receive a friendly message asking them to slow down. HTML in user messages is escaped to prevent XSS.

Contact Form

The contact form is a multi-step React component that collects visitor information and travel preferences. On submission, it triggers a chain of actions through the API that creates the initial records needed to begin the sales process.

Submission Pipeline

Form validated client-side

React handles field validation with clear error messages before allowing submission.

POST to Hono API

Data sent to /webhooks/form-submit with Zod validation on the server side. A honeypot field detects bot submissions. Payload includes name, source, and companySlug.

Contact record created

A new contact is inserted into tt_fresh_db with all form fields.

Deal record created

An associated deal is created in the sales pipeline for tracking.

Slack channel created

A dedicated Slack channel is spun up for this client, and the team is notified in #sales-alerts.

Slack Bot API
Contact page with Start the conversation headline and message form
Contact page — "Start the conversation" with form and MilesChat callout

Design System

The site implements the Dark Sanctuary design system, a premium aesthetic built around dark navy backgrounds, warm off-white text, and gold accents. The system follows a 60-30-10 color distribution rule: 60% dark backgrounds, 30% text and surfaces, 10% gold highlights.

Elemental Design Language

Card components across the site use an elemental design system that maps natural materials to visual effects. This creates a tactile, layered feel across all card surfaces:

  • Paper grain: Subtle texture applied to card backgrounds for organic warmth
  • Water underglow: Soft blue luminance beneath interactive card elements
  • Metal borders: Metallic-toned border treatments on card edges
  • Fire advisory: CSS classes for attention-drawing highlight states on cards (urgent CTAs, limited availability)

Core Palette

Token Value Usage
Abyss / Navy #0B1120 Primary background, page base color
Cloud / Off-White #F0EDE8 Primary text, headings
Gold #E8B83D Accents, CTAs, highlights, links
Mist #9CA3AF Secondary text, metadata, captions
Danger #A85454 Error states, warnings
Success #3A7460 Confirmation, positive states
Parchment #F4F2EE Light section backgrounds (alternating layers)
Tint Blue #F0F6F9 Light info backgrounds

Typography

Playfair Display is used for all headings (h1 through h3), providing an editorial, premium feel. Source Sans 3 handles body text, UI labels, and everything else, chosen for its excellent legibility at small sizes on screens. Both fonts are self-hosted as variable WOFF2 files (weight range 400-700) in public/fonts/ with font-display: swap, eliminating external CDN dependencies. No Google Fonts <link> tags are used.

Graph Paper Background

A distinctive visual treatment used on several sections: a dual-grid background effect that creates the appearance of graph paper. The minor grid is 28px and the major grid is 112px, layered with a radial vignette for depth. The background uses background-attachment: fixed so the grid stays stationary while content scrolls over it.

Prose Content Rendering

Markdown content rendered via Astro's content collections uses Tailwind's prose class for typographic formatting. The prose-invert variant is only applied on pages with dark backgrounds. Pages with light backgrounds (parchment, white) — including /eclipse/2027, /destinations/[slug], /experiences/[slug], and /partners/[slug] — use the standard prose class to ensure readable dark-on-light text.

Styling Rule: Alternating Layers

Contrast is your friend. Adjacent sections should alternate between lighter and darker backgrounds. On dark pages, alternate between #0B1120 and #0F1625. CTA buttons on dark backgrounds get a 1px #F0EDE8 border; on light backgrounds, a 1px #0F2440 border.

Homepage below-fold showing pain points cards and The Travel Tamers way section
Below-fold content — pain points, "The Travel Tamers way", and MilesChat demo

Card Components

The site uses a family of card components across different content types. All cards share the unified card-depth system, which provides consistent border, shadow, and hover behavior with the elemental design language (paper grain, water underglow, metal borders). Clickable cards include focus-visible rings for keyboard navigation.

Component

DestinationCard

Featured image with cinematic treatment, destination name, country, and a brief tagline. Hover reveals a gold border accent. Links to the full destination detail page. Also used as the layout component on the experiences listing page (3-column grid).

Component

ExperienceCard

Category icon, experience type name, description snippet, and starting price. Used on the homepage grid. The experiences listing page uses DestinationCard in a 3-column grid for visual consistency across listing pages.

Component

PartnerCard

Partner logo (with per-partner logoScale sizing), company name, specialties, and a featured itinerary preview. Nine partner logos displayed on cards and detail pages. Used on the partners listing and the homepage partner strip.

Component

BlogPostCard

Cover image, title, category badge, publication date, and reading time. Used on the blog listing and related posts sections.

Component

TestimonialCard

Client quote with attribution using generic fictional names and companies (David Harmon / Ridgepoint Solutions, Rachel Simmons / Canopy Group, Brian Kessler / Waymark Partners). Displayed in a horizontally scrolling carousel on the homepage. Quotes are toned-down and pulled from content data rather than hardcoded.

Component

FeatureCard

Icon + title + description layout for feature highlights on the homepage. Hover effect removed (was a false affordance since the card is not clickable).

Component

StatCard

Animated stat number with label. Glass effect removed to prevent double shadow artifacts with the card-depth system.

Animations & Effects

GSAP Scroll Reveals

Content sections use GSAP's ScrollTrigger plugin for entrance animations. Elements fade in and translate upward as they enter the viewport. The animations are subtle (20-30px movement, 0.6s duration) to feel polished without being distracting.

Dark Mode: 3-Way Theme Toggle

The site implements a 3-way theme toggle (light / dark / system) in Nav.astro using sun, moon, and monitor icons. The selected preference is stored in localStorage under the key tt_theme. A flash-prevention script in BaseLayout.astro's <head> reads this value before first paint and applies the html.dark class immediately, preventing any flash of wrong theme.

The dark-mode.js script handles the 3-way model: when set to "system", it listens on prefers-color-scheme and responds to OS-level changes in real time.

Token Overrides for Dark Mode

Tailwind 4 custom classes (e.g., .text-navy, .bg-white) are not covered by Tailwind's built-in dark: variants because they are defined as custom utilities in @theme. To handle this, 25+ token overrides in global.css flip these classes under html.dark, covering 2,000+ class instances across the site:

  • Text overrides: .text-navy, .text-charcoal, .text-warm-gray, .hover\:text-navy, plus arbitrary hex text colors (#2D3436, #374151, #4B5563, #1F2937)
  • Background overrides: .bg-white#131B2E, .bg-parchment#151D30, .bg-tint-blue, .bg-white/90, .bg-sky-50, .bg-gold-50, plus arbitrary hex backgrounds (#F4F2EE, #ffffff, #F0F6F9)
  • Gradient overrides: .from-white, .to-white, .from-[#F0EDE8], .to-transparent
  • Border/divider overrides: .border-light-border, .bg-light-border, .divide-[#E5E1DB], .border-parchment-border, .border-tint-blue-border
  • Gold/accent overrides: Gold text, border, and background hex values preserved; hover states maintained

Dark mode coverage extends to all React islands (ContactForm, OnboardingForm, NewsletterSignup, Accordion), Pagefind search UI, and the cookie consent banner (explicit dark: classes on text elements). The btn-secondary class has safety overrides on .bg-abyss-600 and .bg-abyss-800 sections to ensure visibility. Font-serif text-stroke effects are disabled in dark mode to avoid rendering artifacts.

SEO & Analytics

Google Tag Manager & GA4

Service ID Notes
GTM Container GTM-5L5JMXHF Manages all tracking tags
GA4 Property G-TCM7G30PJC Traffic and event analytics

Pagefind Search

Pagefind generates a client-side search index at build time, indexing all pages and content collection entries. Visitors can search across destinations, experiences, blog posts, and general pages without any server-side infrastructure. The search index is small (typically under 100KB) and loads on demand. The Pagefind UI is styled for the Dark Sanctuary palette in both light and dark modes, with custom input, result, and highlight colors matching the site's design tokens.

RSS Feed

The site publishes an RSS feed at /rss.xml containing all blog posts. An autodiscovery <link> tag is included in the <head> of every page via BaseLayout.astro, so RSS readers automatically detect the feed.

Sitemap

Astro's sitemap plugin automatically generates sitemap.xml at build time, including all static pages and content collection entries. This is submitted to Google Search Console for indexing.

Content Security Policy

The site enforces a Content Security Policy via nginx headers. The policy allows:

  • Scripts: 'self', 'unsafe-inline' (required for GTM and dark-mode prevention), GTM and GA4 domains
  • Images: 'self', images.unsplash.com (remaining destination images), data: URIs
  • Styles: 'self', 'unsafe-inline'
  • Fonts: 'self' (self-hosted WOFF2, no external CDN)
  • Connect: 'self', api.traveltamers.com, GA4 endpoints

The 'unsafe-inline' for scripts is a concession for GTM compatibility and dark-mode flash prevention. Inline scripts that could be extracted have been moved to public/js/.

Build & Development

Commands

# From the site/ directory:

npm run dev      # Start Astro dev server (port 4321, hot reload)
npm run build    # Build static output to dist/
npm run preview  # Serve the built site locally for testing

Development Notes

  • The dev server runs on port 4321 with hot module replacement.
  • Content collection changes (new .md files, frontmatter edits) trigger automatic rebuilds.
  • React islands hot-reload independently without full page refresh.
  • Tailwind 4 uses the new CSS-first configuration in global.css with a @theme block containing all custom design tokens.
  • All external services are optional in dev — when API keys are absent, features log to console instead of failing.

Key Files

File Purpose
astro.config.mjs Astro configuration: integrations, output mode, site URL
src/styles/global.css All design tokens, Tailwind theme overrides (1000+ lines)
src/content/config.ts Content collection schemas (Zod validation for frontmatter)
src/components/Nav.astro Main navigation with CTA button and 3-way theme toggle (sun/moon/monitor icons)
public/js/tt-widget.js MilesChat FAB widget script (gold glass speech bubble)
public/js/dark-mode.js 3-way dark mode toggle (light/dark/system) with prefers-color-scheme listener

Deployment

The marketing site is deployed as static files served by nginx inside a Docker container, sitting behind the Traefik reverse proxy. The deployment pipeline is fully automated through GitHub Actions.

Deployment Pipeline

GitHub Actions triggered

Push to main branch or manual dispatch starts the deploy workflow.

GitHub Actions

Astro build runs

npm run build generates static HTML, CSS, JS, and Pagefind search index into dist/.

Astro SSG

Files transferred via SCP

The built dist/ directory is copied to the VPS at /opt/tt-fresh/.

SCP over SSH

Directory swap

The old site directory is renamed (kept as rollback), and the new build is moved into place.

Health check

An HTTP request verifies the site is responding correctly. On failure, the previous version is restored automatically.

Production Infrastructure

# Docker container: tt-fresh (nginx)
# Serves: traveltamers.com
# Traefik labels handle:
#   - SSL termination (Let's Encrypt)
#   - HTTP → HTTPS redirect
#   - CSP and security headers
#   - Rate limiting
Mobile view of homepage showing hero section on iPhone viewport
Mobile responsive — homepage on iPhone with full hero and CTA

Rollback Safety: Each deployment keeps the previous build. If the health check fails after a deploy, the pipeline automatically swaps back to the previous version within seconds. Manual rollback is also available by SSH-ing to the VPS and renaming directories.