Files
website/LANDING_PAGE_CONVERSION_PLAN.md
matt ec72c5d62b
Some checks failed
build-website / build (push) Failing after 1m2s
Initial import of HarborSmith website
2025-09-18 22:20:01 +02:00

37 KiB

HarborSmith Landing Page Conversion Plan

From Static HTML to Nuxt 3 + Tailwind CSS v4


Table of Contents

  1. Executive Summary
  2. Current State Analysis
  3. Target Architecture
  4. Implementation Strategy
  5. Technical Implementation
  6. Component Architecture
  7. Theme System
  8. Performance Optimizations
  9. Deployment Strategy
  10. Migration Checklist

Executive Summary

This document outlines the comprehensive plan for converting the HarborSmith landing page from static HTML/CSS/JS to a production-ready Nuxt 3 application with Tailwind CSS v4, while preserving the sophisticated design system with 4 theme variants, video hero, and premium animations.

Key Objectives

  • Preserve 100% visual parity with existing mockups
  • Implement modern development practices with TypeScript
  • Achieve < 1 second page load time
  • Maintain SEO optimization with SSG
  • Deploy as standalone Docker container

Approach

Hybrid system using Tailwind for layout/utilities, CSS custom properties for dynamic theming, and Vue composables for interactivity.


Current State Analysis

Existing Mockup Structure

website-mockups/
├── HTML Files (18 total)
│   ├── index.html              # Main landing page
│   ├── charter-booking-*.html  # 4-step booking flow
│   ├── maintenance/*.html      # Portal pages
│   └── portal-login.html       # Authentication
├── CSS Files
│   ├── voyage-layout.css       # Main layout styles
│   ├── themes.css             # Theme variables
│   ├── animations.css         # Keyframe animations
│   └── booking.css            # Booking-specific styles
└── JavaScript
    ├── main.js                # Core functionality
    ├── animations.js          # Scroll effects
    └── booking.js             # Booking logic

Current Technologies

  • Styling: Plain CSS with CSS variables
  • Themes: 4 variants (Nautical, Coastal Dawn, Deep Sea, Monaco)
  • Icons: Lucide via CDN
  • Fonts: Inter + Playfair Display (Google Fonts)
  • Video: Hero background with fallback
  • Animations: CSS keyframes + JavaScript scroll effects

Design System Highlights

  • Warm gradients and ocean-inspired colors
  • Premium yacht aesthetic
  • Smooth transitions and animations
  • Mobile-responsive design
  • Accessibility considerations (WCAG AA)

Target Architecture

Technology Stack

Frontend Framework:    Nuxt 3 (v3.10+)
CSS Framework:        Tailwind CSS v4 (beta)
UI Components:        Nuxt UI v3
Animation:           @vueuse/motion
State Management:     Pinia
Icons:               lucide-vue-next
Type Safety:         TypeScript
Image Optimization:   Nuxt Image
Deployment:          Docker + nginx

Project Structure

harborsmith-web/
├── app.vue                  # Root component with theme provider
├── nuxt.config.ts          # Nuxt configuration
├── tailwind.config.ts      # Tailwind with custom properties
├── package.json            # Dependencies
├── Dockerfile              # Production build
├── nginx.conf              # Production server config
├── components/
│   ├── layout/
│   │   ├── HarborNav.vue       # Navigation with theme switcher
│   │   └── HarborFooter.vue    # Footer component
│   ├── sections/
│   │   ├── HeroVideo.vue       # Video hero section
│   │   ├── TrustBadges.vue     # Social proof
│   │   ├── FleetGrid.vue       # Vessel showcase
│   │   ├── Services.vue        # Services overview
│   │   └── Testimonials.vue    # Customer reviews
│   └── ui/
│       ├── ThemedButton.vue    # Gradient CTAs
│       ├── VesselCard.vue      # Fleet display cards
│       └── ThemeSwitcher.vue   # Theme selector dropdown
├── composables/
│   ├── useTheme.ts         # Theme switching logic
│   ├── useScrollEffects.ts # Scroll animations
│   └── useVideoLoader.ts   # Video optimization
├── stores/
│   └── theme.ts            # Pinia theme store
├── assets/
│   └── css/
│       ├── base.css        # Global styles
│       ├── themes.css      # CSS custom properties
│       └── animations.css  # Keyframe definitions
└── public/
    ├── videos/             # Hero videos
    ├── images/             # Static images
    └── fonts/              # Self-hosted fonts (optional)

Implementation Strategy

Phase 1: Foundation Setup (Day 1)

  1. Initialize Nuxt 3 project
  2. Configure Tailwind CSS v4
  3. Set up TypeScript
  4. Install core dependencies
  5. Create base project structure

Phase 2: Theme System (Day 2)

  1. Port CSS variables to Tailwind config
  2. Implement theme switcher with Pinia
  3. Create theme persistence logic
  4. Test all 4 theme variants

Phase 3: Component Development (Days 3-4)

  1. Convert navigation component
  2. Build video hero section
  3. Create reusable UI components
  4. Implement fleet showcase grid
  5. Add testimonial carousel

Phase 4: Animations & Polish (Day 5)

  1. Implement scroll animations
  2. Add hover effects
  3. Optimize transitions
  4. Fine-tune responsive design

Phase 5: Optimization & Deployment (Day 6)

  1. Image optimization
  2. Performance testing
  3. Docker configuration
  4. Production deployment

Technical Implementation

1. Project Initialization

# Create Nuxt 3 application
npx nuxi@latest init harborsmith-web --template minimal
cd harborsmith-web

# Install core dependencies
npm install -D @nuxtjs/tailwindcss@next @nuxt/ui@next
npm install lucide-vue-next @vueuse/motion @vueuse/nuxt
npm install pinia @pinia/nuxt
npm install @nuxt/image

# Development dependencies
npm install -D @types/node typescript sass

2. Nuxt Configuration

// nuxt.config.ts
export default defineNuxtConfig({
  devtools: { enabled: true },

  modules: [
    '@nuxt/ui',
    '@nuxtjs/tailwindcss',
    '@vueuse/nuxt',
    '@pinia/nuxt',
    '@nuxt/image',
  ],

  css: [
    '~/assets/css/base.css',
    '~/assets/css/themes.css',
    '~/assets/css/animations.css',
  ],

  typescript: {
    strict: true,
    typeCheck: true,
  },

  nitro: {
    preset: 'static',
    prerender: {
      routes: ['/'],
      crawlLinks: true,
    },
    compressPublicAssets: true,
  },

  image: {
    quality: 80,
    format: ['webp', 'jpg'],
    screens: {
      xs: 320,
      sm: 640,
      md: 768,
      lg: 1024,
      xl: 1280,
      xxl: 1536,
    },
  },

  app: {
    head: {
      charset: 'utf-8',
      viewport: 'width=device-width, initial-scale=1',
      title: 'HarborSmith - Premium Yacht Charters SF Bay',
      meta: [
        { name: 'description', content: 'Experience luxury yacht charters in the San Francisco Bay. Premium vessels, professional maintenance, unforgettable adventures.' },
        { property: 'og:title', content: 'HarborSmith - Your Adventure Awaits' },
        { property: 'og:image', content: '/og-image.jpg' },
      ],
      link: [
        { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
        { rel: 'preconnect', href: 'https://fonts.googleapis.com' },
        { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' },
        {
          rel: 'stylesheet',
          href: 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Playfair+Display:wght@400;700;900&display=swap'
        },
      ],
      script: [
        // Prevent theme flash
        {
          innerHTML: `
            (function() {
              const theme = localStorage.getItem('harborsmith-theme') || 'nautical';
              document.documentElement.className = 'theme-' + theme;
            })();
          `,
          type: 'text/javascript',
        }
      ],
    },
  },

  experimental: {
    payloadExtraction: false,
    renderJsonPayloads: false,
    viewTransition: true,
  },
})

3. Tailwind Configuration

// tailwind.config.ts
import type { Config } from 'tailwindcss'

export default <Config>{
  content: [],
  theme: {
    extend: {
      colors: {
        // Dynamic theme colors using CSS variables
        'primary': 'rgb(var(--primary-blue) / <alpha-value>)',
        'accent': 'rgb(var(--warm-orange) / <alpha-value>)',
        'accent-hover': 'rgb(var(--warm-amber) / <alpha-value>)',
        'accent-light': 'rgb(var(--warm-yellow) / <alpha-value>)',
        'cream': 'rgb(var(--soft-cream) / <alpha-value>)',
        'text-primary': 'rgb(var(--text-dark) / <alpha-value>)',
        'text-secondary': 'rgb(var(--text-light) / <alpha-value>)',
      },
      backgroundImage: {
        'gradient-warm': 'var(--gradient-warm)',
        'gradient-sunset': 'var(--gradient-sunset)',
        'gradient-ocean': 'var(--gradient-ocean)',
      },
      fontFamily: {
        'sans': ['Inter', '-apple-system', 'BlinkMacSystemFont', 'sans-serif'],
        'display': ['Playfair Display', 'serif'],
      },
      spacing: {
        '18': '4.5rem',
        '88': '22rem',
        '120': '30rem',
      },
      animation: {
        'fade-up': 'fadeInUp 0.8s ease forwards',
        'fade-up-delay': 'fadeInUp 0.8s ease 0.2s forwards',
        'fade-up-delay-2': 'fadeInUp 0.8s ease 0.4s forwards',
        'float': 'float 20s ease-in-out infinite',
        'pulse-slow': 'pulse 3s ease-in-out infinite',
      },
      transitionTimingFunction: {
        'bounce-in': 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
      },
    },
  },
  plugins: [],
}

Component Architecture

Video Hero Component

<!-- components/sections/HeroVideo.vue -->
<template>
  <section class="hero-voyage relative h-screen overflow-hidden">
    <!-- Video Background -->
    <div class="absolute inset-0 z-0">
      <!-- Video Element -->
      <ClientOnly>
        <video
          v-if="!videoError && videoLoaded"
          ref="videoRef"
          :key="videoUrl"
          autoplay
          loop
          muted
          playsinline
          @loadeddata="onVideoLoaded"
          @error="onVideoError"
          class="w-full h-full object-cover scale-110"
        >
          <source :src="videoUrl" type="video/mp4">
        </video>
      </ClientOnly>

      <!-- Fallback/Poster Image -->
      <NuxtImg
        :src="posterImage"
        alt="San Francisco Bay"
        class="absolute inset-0 w-full h-full object-cover"
        :class="{ 'opacity-0': videoLoaded && !videoError }"
        loading="eager"
        quality="90"
      />

      <!-- Gradient Overlays -->
      <div class="absolute inset-0 bg-gradient-warm opacity-40 mix-blend-multiply" />
      <div class="absolute inset-0 bg-gradient-to-b from-black/30 via-transparent to-black/50" />
    </div>

    <!-- Hero Content -->
    <div class="relative z-10 h-full flex items-center justify-center px-4">
      <div class="text-center text-white max-w-4xl mx-auto">
        <!-- Trust Badge -->
        <div class="inline-flex items-center gap-2 bg-white/10 backdrop-blur-sm rounded-full px-4 py-2 mb-6">
          <div class="flex">
            <Icon v-for="i in 5" :key="i" name="lucide:star" class="w-4 h-4 text-yellow-400 fill-current" />
          </div>
          <span class="text-sm">500+ Premium Charters</span>
        </div>

        <!-- Headlines -->
        <h1 class="font-display text-5xl md:text-6xl lg:text-7xl font-bold mb-6 leading-tight">
          Your Adventure Awaits
        </h1>
        <p class="text-xl md:text-2xl mb-8 opacity-95 font-light">
          Premium Yacht Charters in the San Francisco Bay
        </p>

        <!-- CTA Buttons -->
        <div class="flex flex-col sm:flex-row gap-4 justify-center">
          <ThemedButton size="lg" variant="primary" @click="scrollToFleet">
            Explore Our Fleet
            <Icon name="lucide:arrow-right" class="ml-2" />
          </ThemedButton>
          <ThemedButton size="lg" variant="outline" @click="navigateToBooking">
            View Availability
          </ThemedButton>
        </div>
      </div>
    </div>

    <!-- Scroll Indicator -->
    <div class="absolute bottom-8 left-1/2 -translate-x-1/2 animate-bounce">
      <button @click="scrollToNext" class="text-white/70 hover:text-white transition-colors">
        <Icon name="lucide:chevron-down" class="w-8 h-8" />
      </button>
    </div>
  </section>
</template>

<script setup lang="ts">
const videoRef = ref<HTMLVideoElement>()
const videoLoaded = ref(false)
const videoError = ref(false)

// Video configuration
const videoUrl = 'https://videos.pexels.com/video-files/3571264/3571264-uhd_2560_1440_30fps.mp4'
const posterImage = '/images/hero-poster.jpg'

// Video handlers
const onVideoLoaded = () => {
  videoLoaded.value = true
}

const onVideoError = () => {
  videoError.value = true
  console.warn('Video failed to load, using fallback image')
}

// Navigation
const scrollToFleet = () => {
  document.getElementById('fleet')?.scrollIntoView({
    behavior: 'smooth',
    block: 'start'
  })
}

const scrollToNext = () => {
  window.scrollBy({
    top: window.innerHeight,
    behavior: 'smooth'
  })
}

const navigateToBooking = () => {
  navigateTo('/booking')
}

// Lazy load video after initial render
onMounted(() => {
  // Only load video on desktop or good connection
  if (navigator.connection?.effectiveType === '4g' || !navigator.connection) {
    videoRef.value?.load()
  }
})
</script>

Navigation Component

<!-- components/layout/HarborNav.vue -->
<template>
  <nav
    :class="[
      'fixed top-0 w-full z-50 transition-all duration-500',
      {
        'bg-white/95 backdrop-blur-md shadow-lg': scrolled,
        'bg-gradient-to-b from-black/50 to-transparent': !scrolled
      }
    ]"
  >
    <div class="container mx-auto px-4">
      <div class="flex items-center justify-between h-20">
        <!-- Logo -->
        <NuxtLink to="/" class="flex items-center gap-3 group">
          <img
            src="/logo.jpg"
            alt="HarborSmith"
            class="h-12 w-12 rounded-lg shadow-md group-hover:shadow-xl transition-shadow"
          >
          <span
            class="font-display text-2xl font-bold transition-colors"
            :class="scrolled ? 'text-primary' : 'text-white'"
          >
            HarborSmith
          </span>
        </NuxtLink>

        <!-- Desktop Navigation -->
        <div class="hidden md:flex items-center gap-8">
          <!-- Nav Links -->
          <NuxtLink
            v-for="link in navLinks"
            :key="link.href"
            :to="link.href"
            :class="[
              'font-medium transition-colors relative',
              scrolled ? 'text-gray-700 hover:text-accent' : 'text-white hover:text-accent-light',
              'after:absolute after:bottom-0 after:left-0 after:w-0 after:h-0.5',
              'after:bg-accent after:transition-all hover:after:w-full'
            ]"
          >
            {{ link.label }}
          </NuxtLink>

          <!-- Theme Switcher -->
          <ThemeSwitcher :scrolled="scrolled" />

          <!-- CTA Button -->
          <ThemedButton variant="primary" size="sm">
            Book Charter
            <Icon name="lucide:calendar" class="ml-2 w-4 h-4" />
          </ThemedButton>
        </div>

        <!-- Mobile Menu Button -->
        <button
          @click="mobileMenuOpen = !mobileMenuOpen"
          class="md:hidden text-current"
        >
          <Icon
            :name="mobileMenuOpen ? 'lucide:x' : 'lucide:menu'"
            class="w-6 h-6"
          />
        </button>
      </div>
    </div>

    <!-- Mobile Menu -->
    <Transition name="slide-down">
      <div
        v-if="mobileMenuOpen"
        class="md:hidden bg-white/95 backdrop-blur-md border-t"
      >
        <div class="container mx-auto px-4 py-4 space-y-2">
          <NuxtLink
            v-for="link in navLinks"
            :key="link.href"
            :to="link.href"
            @click="mobileMenuOpen = false"
            class="block px-4 py-2 text-gray-700 hover:text-accent hover:bg-gray-50 rounded-lg transition-colors"
          >
            {{ link.label }}
          </NuxtLink>
          <div class="pt-2 border-t">
            <ThemedButton variant="primary" size="sm" class="w-full">
              Book Charter
            </ThemedButton>
          </div>
        </div>
      </div>
    </Transition>
  </nav>
</template>

<script setup lang="ts">
const scrolled = ref(false)
const mobileMenuOpen = ref(false)

const navLinks = [
  { label: 'Fleet', href: '#fleet' },
  { label: 'Services', href: '#services' },
  { label: 'Experiences', href: '#experiences' },
  { label: 'About', href: '/about' },
  { label: 'Contact', href: '/contact' },
]

// Handle scroll behavior
onMounted(() => {
  const handleScroll = () => {
    scrolled.value = window.scrollY > 50
  }

  window.addEventListener('scroll', handleScroll, { passive: true })

  onUnmounted(() => {
    window.removeEventListener('scroll', handleScroll)
  })
})
</script>

<style scoped>
.slide-down-enter-active,
.slide-down-leave-active {
  transition: all 0.3s ease;
}

.slide-down-enter-from,
.slide-down-leave-to {
  transform: translateY(-100%);
  opacity: 0;
}
</style>

Theme System

CSS Custom Properties

/* assets/css/themes.css */
:root {
  /* Default Theme: Nautical */
  --primary-blue: 0 31 63;      /* #001f3f */
  --warm-orange: 220 20 60;      /* #dc143c */
  --warm-amber: 185 28 60;       /* #b91c3c */
  --warm-yellow: 239 68 68;      /* #ef4444 */
  --soft-cream: 240 244 248;     /* #f0f4f8 */
  --text-dark: 10 22 40;         /* #0a1628 */
  --text-light: 74 85 104;       /* #4a5568 */

  /* Gradients */
  --gradient-warm: linear-gradient(135deg, #dc143c 0%, #ef4444 100%);
  --gradient-sunset: linear-gradient(135deg, #b91c3c 0%, #dc143c 50%, #ef4444 100%);
  --gradient-ocean: linear-gradient(135deg, #001f3f 0%, #003366 100%);

  /* Shadows */
  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
  --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
  --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1);
}

/* Coastal Dawn Theme */
.theme-coastal-dawn {
  --primary-blue: 169 180 194;   /* #A9B4C2 */
  --warm-orange: 212 175 55;     /* #D4AF37 */
  --warm-amber: 201 169 97;      /* #C9A961 */
  --warm-yellow: 230 208 136;    /* #E6D088 */
  --soft-cream: 248 247 244;     /* #F8F7F4 */
  --text-dark: 51 55 69;         /* #333745 */
  --text-light: 107 114 128;     /* #6B7280 */

  --gradient-warm: linear-gradient(135deg, #D4AF37 0%, #E6D088 100%);
  --gradient-sunset: linear-gradient(135deg, #C9A961 0%, #D4AF37 50%, #E6D088 100%);
  --gradient-ocean: linear-gradient(135deg, #A9B4C2 0%, #C5D3E0 100%);
}

/* Deep Sea Theme */
.theme-deep-sea {
  --primary-blue: 30 32 34;      /* #1E2022 */
  --warm-orange: 0 191 255;      /* #00BFFF */
  --warm-amber: 30 144 255;      /* #1E90FF */
  --warm-yellow: 65 105 225;     /* #4169E1 */
  --soft-cream: 42 45 48;        /* #2A2D30 */
  --text-dark: 229 228 226;      /* #E5E4E2 */
  --text-light: 192 192 192;     /* #C0C0C0 */

  --gradient-warm: linear-gradient(135deg, #00BFFF 0%, #4169E1 100%);
  --gradient-sunset: linear-gradient(135deg, #1E90FF 0%, #00BFFF 50%, #4169E1 100%);
  --gradient-ocean: linear-gradient(135deg, #1E2022 0%, #2A2D30 100%);
}

/* Monaco White Theme */
.theme-monaco {
  --primary-blue: 44 62 80;      /* #2C3E50 */
  --warm-orange: 231 76 60;      /* #E74C3C */
  --warm-amber: 230 126 34;      /* #E67E22 */
  --warm-yellow: 243 156 18;     /* #F39C12 */
  --soft-cream: 248 249 250;     /* #F8F9FA */
  --text-dark: 44 62 80;         /* #2C3E50 */
  --text-light: 127 140 141;     /* #7F8C8D */

  --gradient-warm: linear-gradient(135deg, #E74C3C 0%, #F39C12 100%);
  --gradient-sunset: linear-gradient(135deg, #E67E22 0%, #E74C3C 50%, #F39C12 100%);
  --gradient-ocean: linear-gradient(135deg, #2C3E50 0%, #34495E 100%);
}

Theme Store (Pinia)

// stores/theme.ts
import { defineStore } from 'pinia'

export type ThemeVariant = 'nautical' | 'coastal-dawn' | 'deep-sea' | 'monaco'

interface ThemeOption {
  id: ThemeVariant
  name: string
  description: string
  colors: {
    primary: string
    accent: string
    background: string
  }
}

export const useThemeStore = defineStore('theme', {
  state: () => ({
    currentTheme: 'nautical' as ThemeVariant,
    themes: [
      {
        id: 'nautical',
        name: 'Classic Nautical',
        description: 'Deep navy and crimson',
        colors: { primary: '#001f3f', accent: '#dc143c', background: '#ffffff' }
      },
      {
        id: 'coastal-dawn',
        name: 'Coastal Dawn',
        description: 'Soft blue and gold',
        colors: { primary: '#A9B4C2', accent: '#D4AF37', background: '#F8F7F4' }
      },
      {
        id: 'deep-sea',
        name: 'Deep Sea',
        description: 'Dark mode with electric blue',
        colors: { primary: '#1E2022', accent: '#00BFFF', background: '#2A2D30' }
      },
      {
        id: 'monaco',
        name: 'Monaco White',
        description: 'Clean and sophisticated',
        colors: { primary: '#2C3E50', accent: '#E74C3C', background: '#F8F9FA' }
      }
    ] as ThemeOption[]
  }),

  getters: {
    activeTheme: (state) => state.themes.find(t => t.id === state.currentTheme),
  },

  actions: {
    setTheme(theme: ThemeVariant) {
      this.currentTheme = theme

      // Apply theme class to HTML element
      if (process.client) {
        document.documentElement.className = `theme-${theme}`

        // Persist to localStorage
        localStorage.setItem('harborsmith-theme', theme)

        // Also set cookie for SSR
        const cookie = useCookie('harborsmith-theme', {
          httpOnly: false,
          sameSite: 'strict',
          maxAge: 60 * 60 * 24 * 365 // 1 year
        })
        cookie.value = theme
      }
    },

    loadTheme() {
      if (process.client) {
        const saved = localStorage.getItem('harborsmith-theme') ||
                     useCookie('harborsmith-theme').value

        if (saved && this.themes.find(t => t.id === saved)) {
          this.setTheme(saved as ThemeVariant)
        }
      }
    }
  }
})

Theme Switcher Component

<!-- components/ui/ThemeSwitcher.vue -->
<template>
  <UDropdown
    :items="themeMenuItems"
    :popper="{ placement: 'bottom-end' }"
    :ui="{ item: { disabled: 'cursor-default opacity-60' } }"
  >
    <UButton
      :color="scrolled ? 'gray' : 'white'"
      variant="ghost"
      size="sm"
      :icon="currentThemeIcon"
      :class="!scrolled && 'hover:bg-white/10'"
    >
      <template #trailing>
        <Icon name="lucide:chevron-down" class="w-4 h-4 ml-1" />
      </template>
    </UButton>

    <template #item="{ item }">
      <div class="flex items-center justify-between w-full">
        <div class="flex items-center gap-2">
          <div class="flex gap-1">
            <span
              v-for="color in item.colors"
              :key="color"
              :style="{ backgroundColor: color }"
              class="w-3 h-3 rounded-full border border-gray-200"
            />
          </div>
          <span>{{ item.label }}</span>
        </div>
        <Icon
          v-if="item.active"
          name="lucide:check"
          class="w-4 h-4 text-accent"
        />
      </div>
    </template>
  </UDropdown>
</template>

<script setup lang="ts">
import type { DropdownItem } from '#ui/types'

const props = defineProps<{
  scrolled?: boolean
}>()

const themeStore = useThemeStore()

const currentThemeIcon = computed(() => {
  const icons = {
    'nautical': 'lucide:anchor',
    'coastal-dawn': 'lucide:sunrise',
    'deep-sea': 'lucide:waves',
    'monaco': 'lucide:sun'
  }
  return icons[themeStore.currentTheme]
})

const themeMenuItems = computed<DropdownItem[][]>(() => [[
  {
    label: 'Classic Nautical',
    colors: ['#001f3f', '#dc143c', '#ffffff'],
    active: themeStore.currentTheme === 'nautical',
    click: () => themeStore.setTheme('nautical')
  },
  {
    label: 'Coastal Dawn',
    colors: ['#A9B4C2', '#D4AF37', '#F8F7F4'],
    active: themeStore.currentTheme === 'coastal-dawn',
    click: () => themeStore.setTheme('coastal-dawn')
  },
  {
    label: 'Deep Sea',
    colors: ['#1E2022', '#00BFFF', '#2A2D30'],
    active: themeStore.currentTheme === 'deep-sea',
    click: () => themeStore.setTheme('deep-sea')
  },
  {
    label: 'Monaco White',
    colors: ['#2C3E50', '#E74C3C', '#F8F9FA'],
    active: themeStore.currentTheme === 'monaco',
    click: () => themeStore.setTheme('monaco')
  }
]])
</script>

Performance Optimizations

1. Image Optimization Strategy

<!-- components/ui/OptimizedImage.vue -->
<template>
  <div class="relative overflow-hidden" :class="containerClass">
    <!-- Blur placeholder -->
    <div
      v-if="!loaded"
      class="absolute inset-0 bg-gradient-to-br from-gray-200 to-gray-300 animate-pulse"
    />

    <!-- Main image -->
    <NuxtImg
      :src="src"
      :alt="alt"
      :loading="loading"
      :quality="quality"
      :format="format"
      :sizes="sizes"
      @load="onLoad"
      class="w-full h-full object-cover transition-opacity duration-500"
      :class="{ 'opacity-0': !loaded }"
    />
  </div>
</template>

<script setup lang="ts">
interface Props {
  src: string
  alt: string
  loading?: 'lazy' | 'eager'
  quality?: number
  format?: string
  sizes?: string
  containerClass?: string
}

const props = withDefaults(defineProps<Props>(), {
  loading: 'lazy',
  quality: 80,
  format: 'webp',
  sizes: '100vw'
})

const loaded = ref(false)
const onLoad = () => loaded.value = true
</script>

2. Scroll Animation Composable

// composables/useScrollEffects.ts
import { useMotion } from '@vueuse/motion'
import { ref, onMounted, onUnmounted } from 'vue'

export const useScrollEffects = () => {
  const elements = ref<HTMLElement[]>([])
  const observer = ref<IntersectionObserver>()

  const fadeInUp = {
    initial: {
      opacity: 0,
      y: 50
    },
    enter: {
      opacity: 1,
      y: 0,
      transition: {
        duration: 800,
        ease: [0.4, 0, 0.2, 1]
      }
    }
  }

  const fadeInScale = {
    initial: {
      opacity: 0,
      scale: 0.95
    },
    enter: {
      opacity: 1,
      scale: 1,
      transition: {
        duration: 600,
        ease: [0.4, 0, 0.2, 1]
      }
    }
  }

  const initScrollAnimations = () => {
    observer.value = new IntersectionObserver(
      (entries) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            entry.target.classList.add('animate-visible')
            observer.value?.unobserve(entry.target)
          }
        })
      },
      {
        threshold: 0.1,
        rootMargin: '50px'
      }
    )

    // Find all elements with data-animate attribute
    elements.value = Array.from(
      document.querySelectorAll('[data-animate]')
    ) as HTMLElement[]

    elements.value.forEach(el => observer.value?.observe(el))
  }

  onMounted(() => initScrollAnimations())
  onUnmounted(() => observer.value?.disconnect())

  return {
    fadeInUp,
    fadeInScale
  }
}

3. Performance Monitoring

// plugins/performance.client.ts
export default defineNuxtPlugin(() => {
  if (process.dev) return

  // Web Vitals monitoring
  onNuxtReady(() => {
    // First Contentful Paint
    const paintObserver = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.name === 'first-contentful-paint') {
          console.log('FCP:', entry.startTime)
          // Send to analytics
        }
      }
    })
    paintObserver.observe({ entryTypes: ['paint'] })

    // Largest Contentful Paint
    const lcpObserver = new PerformanceObserver((list) => {
      const entries = list.getEntries()
      const lastEntry = entries[entries.length - 1]
      console.log('LCP:', lastEntry.startTime)
      // Send to analytics
    })
    lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] })

    // Cumulative Layout Shift
    let clsValue = 0
    const clsObserver = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
          clsValue += entry.value
          console.log('CLS:', clsValue)
        }
      }
    })
    clsObserver.observe({ entryTypes: ['layout-shift'] })
  })
})

Deployment Strategy

Docker Configuration

# Dockerfile
# Build stage
FROM node:20-alpine AS builder

# Set working directory
WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy source code
COPY . .

# Build application
RUN npm run generate

# Production stage
FROM nginx:alpine

# Install nginx-module-brotli for better compression
RUN apk add --no-cache nginx-mod-http-brotli

# Copy built static files
COPY --from=builder /app/.output/public /usr/share/nginx/html

# Copy nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf

# Copy custom nginx site config
COPY default.conf /etc/nginx/conf.d/default.conf

# Expose port
EXPOSE 80

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1

# Start nginx
CMD ["nginx", "-g", "daemon off;"]

nginx Configuration

# nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # Logging
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';
    access_log /var/log/nginx/access.log main;

    # Performance
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;

    # Gzip compression
    gzip on;
    gzip_disable "msie6";
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml text/javascript
               application/json application/javascript application/xml+rss
               application/rss+xml application/atom+xml image/svg+xml
               text/x-js text/x-cross-domain-policy application/x-font-ttf
               application/x-font-opentype application/vnd.ms-fontobject
               image/x-icon;

    # Brotli compression
    brotli on;
    brotli_comp_level 6;
    brotli_types text/plain text/css text/xml text/javascript
                 application/json application/javascript application/xml+rss
                 application/rss+xml application/atom+xml image/svg+xml;

    include /etc/nginx/conf.d/*.conf;
}
# default.conf
server {
    listen 80;
    server_name localhost;
    root /usr/share/nginx/html;
    index index.html;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "no-referrer-when-downgrade" always;
    add_header Content-Security-Policy "default-src 'self' https:; script-src 'self' 'unsafe-inline' https://unpkg.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; media-src 'self' https://videos.pexels.com;" always;

    # Cache static assets
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2|ttf|svg|webp)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # Cache HTML (shorter duration)
    location ~* \.(html)$ {
        expires 1h;
        add_header Cache-Control "public, must-revalidate";
    }

    # SPA fallback
    location / {
        try_files $uri $uri/ /index.html;
    }

    # Health check endpoint
    location /health {
        access_log off;
        return 200 "OK";
    }
}

Docker Compose (Development)

# docker-compose.yml
version: '3.8'

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: harborsmith-web
    ports:
      - "3001:80"
    environment:
      - NODE_ENV=production
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"]
      interval: 30s
      timeout: 10s
      retries: 3
    networks:
      - harborsmith

networks:
  harborsmith:
    driver: bridge

Migration Checklist

Pre-Migration

  • Backup current mockups
  • Document all custom styles
  • List all JavaScript functionality
  • Catalog all images and videos
  • Note responsive breakpoints

Phase 1: Setup

  • Initialize Nuxt 3 project
  • Configure TypeScript
  • Install dependencies
  • Setup Tailwind CSS v4
  • Configure Nuxt modules

Phase 2: Theme System

  • Port CSS variables
  • Create theme files
  • Implement Pinia store
  • Build theme switcher
  • Test theme persistence

Phase 3: Components

  • Convert navigation
  • Build video hero
  • Create button components
  • Port trust badges
  • Convert fleet grid
  • Build testimonials
  • Add footer

Phase 4: Content

  • Migrate images
  • Optimize videos
  • Add meta tags
  • Setup analytics
  • Configure sitemap

Phase 5: Optimization

  • Run Lighthouse audit
  • Optimize bundle size
  • Test all viewports
  • Check accessibility
  • Validate SEO

Phase 6: Deployment

  • Build Docker image
  • Configure nginx
  • Setup CI/CD
  • Deploy to staging
  • Production deployment

Post-Migration

  • Performance monitoring
  • Error tracking
  • User testing
  • Documentation
  • Team training

Testing Strategy

Unit Tests (Vitest)

// components/ui/ThemedButton.test.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import ThemedButton from './ThemedButton.vue'

describe('ThemedButton', () => {
  it('renders with primary variant', () => {
    const wrapper = mount(ThemedButton, {
      props: {
        variant: 'primary'
      }
    })

    expect(wrapper.classes()).toContain('bg-gradient-warm')
  })

  it('emits click event', async () => {
    const wrapper = mount(ThemedButton)
    await wrapper.trigger('click')

    expect(wrapper.emitted('click')).toHaveLength(1)
  })
})

E2E Tests (Playwright)

// tests/e2e/landing.spec.ts
import { test, expect } from '@playwright/test'

test.describe('Landing Page', () => {
  test('loads with video hero', async ({ page }) => {
    await page.goto('/')

    // Check hero section
    const hero = await page.locator('.hero-voyage')
    await expect(hero).toBeVisible()

    // Check video or fallback image
    const video = page.locator('video')
    const hasVideo = await video.count() > 0

    if (hasVideo) {
      await expect(video).toHaveAttribute('autoplay')
    } else {
      const fallbackImage = page.locator('.hero-voyage img')
      await expect(fallbackImage).toBeVisible()
    }
  })

  test('theme switcher works', async ({ page }) => {
    await page.goto('/')

    // Open theme menu
    await page.click('[data-testid="theme-switcher"]')

    // Select coastal dawn theme
    await page.click('text=Coastal Dawn')

    // Check theme applied
    const html = page.locator('html')
    await expect(html).toHaveClass('theme-coastal-dawn')

    // Check persistence
    await page.reload()
    await expect(html).toHaveClass('theme-coastal-dawn')
  })
})

Success Metrics

Performance Targets

  • Lighthouse Score: > 95 (all categories)
  • First Contentful Paint: < 1.2s
  • Largest Contentful Paint: < 2.5s
  • Time to Interactive: < 3.5s
  • Cumulative Layout Shift: < 0.1
  • JavaScript Bundle: < 200KB (initial)
  • CSS Bundle: < 50KB

Quality Metrics

  • TypeScript Coverage: 100%
  • Test Coverage: > 80%
  • Accessibility: WCAG AA compliant
  • SEO Score: 100
  • Mobile Score: 100

Business Metrics

  • Conversion Rate: Track booking CTAs
  • Engagement: Time on site, scroll depth
  • Theme Usage: Analytics on theme preferences
  • Performance: Real user monitoring

Maintenance & Updates

Regular Tasks

  • Update dependencies monthly
  • Review performance metrics weekly
  • Monitor error logs daily
  • Backup before major changes
  • Document all customizations

Optimization Opportunities

  • Implement service worker for offline
  • Add PWA capabilities
  • Integrate with CDN
  • Implement A/B testing
  • Add personalization features

Conclusion

This comprehensive plan provides a clear roadmap for converting the HarborSmith landing page from static HTML to a modern Nuxt 3 application. The approach preserves the sophisticated design while implementing best practices for performance, maintainability, and developer experience.

The hybrid strategy of using Tailwind for utilities with CSS custom properties for theming ensures flexibility while maintaining the exact visual design. The component-based architecture with TypeScript provides type safety and reusability for future development.

Following this plan will result in a production-ready landing page that loads in under 1 second, maintains perfect visual parity with the mockups, and provides a solid foundation for the entire HarborSmith web platform.