Back to Portfolio
CASE STUDY

AI-Assisted Exterior Visualization Platform for Local Service Business

Building a high-performance local business site with Next.js, Tailwind CSS, and AI-powered visualization tools to drive customer engagement and conversions

December 2024 15 min read

Live Demo

Experience the full landing page below. This is the actual production site built with Vue 3 and Tailwind CSS, featuring smooth animations, responsive design, and integrated Google Maps.

Interactive demo - Scroll, navigate, and interact with the full experience

The Challenge: Building a Local Business Presence

When my family's interior and exterior painting business—serving both residential and commercial clients—needed a stronger local presence, I decided to build a modern, performance-focused website designed specifically to attract nearby customers.

Traditional contractor websites often suffer from slow load times, poor mobile experiences, and lackluster SEO. In a competitive local market where potential customers are searching on their phones while comparing quotes, every millisecond of load time and every pixel of design matters.

While the landing page focuses on performance and SEO, the core of this platform is a Laravel-based AI visualization engine that transforms uploaded property photos into photorealistic mockups—turning browsers into committed customers by showing them exactly what their finished project will look like.

Project Goals: Build a full-stack platform with a fast, SEO-optimized frontend for lead generation and a Laravel backend powering AI-driven visualization workflows that close sales.

Architectural Overview: Frontend for Discovery, Backend for Conversion

This platform leverages a clear separation of concerns: Next.js handles discovery and conversion through SEO-optimized landing pages, while Laravel powers the AI workflows, image processing, and persistence layer. The frontend attracts and engages visitors; the backend transforms them into paying customers.

Why Next.js for the Frontend

Next.js was a natural choice for the landing page due to its built-in SEO and rendering capabilities. By leveraging server-side rendering (SSR) and static generation, the site delivers fast initial loads and search-engine-friendly pages—crucial for local discovery on Google.

Pages are structured with clean metadata, semantic HTML, and optimized routing, helping the business compete in local search results without relying solely on paid ads.

Next.js Configuration

The project started with a clean Next.js setup using the App Router for better performance and developer experience:

// next.config.js
const nextConfig = {
  reactStrictMode: true,
  images: {
    domains: ['images.unsplash.com'],
    formats: ['image/avif', 'image/webp']
  },
  experimental: {
    optimizeCss: true
  }
};

export default nextConfig;

The configuration enables React strict mode for catching potential issues, optimizes images with modern formats (AVIF, WebP), and enables CSS optimization for smaller bundle sizes.

App Router Structure

Using Next.js 14's App Router provides better code organization and automatic route optimization:

app/
├── page.tsx                      # Home page
├── layout.tsx                    # Root layout
├── components/
│   ├── V2Header.tsx             # Responsive header
│   └── GoogleMap.tsx            # Map component
└── landing-pages/
    └── v2/
        └── page.tsx             # Landing page

Performance as a Conversion Tool

Performance directly impacts trust and conversions. Next.js allows fine-grained control over rendering strategies, ensuring content loads instantly while keeping JavaScript overhead minimal.

Faster pages mean lower bounce rates and a smoother experience for potential customers browsing services, galleries, and contact forms.

Client-Side Optimization

Using the 'use client' directive strategically ensures only interactive components load JavaScript:

'use client';

import { useState, useEffect } from 'react';
import { Play, ChevronLeft, ChevronRight } from 'lucide-react';

export default function LandingV2() {
  const [currentSlide, setCurrentSlide] = useState(0);
  const [visibleSections, setVisibleSections] = useState(new Set());

  // Intersection Observer for scroll animations
  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            setVisibleSections((prev) => new Set([...prev, entry.target.id]));
          }
        });
      },
      { threshold: 0.1 }
    );

    const sections = document.querySelectorAll('[data-animate]');
    sections.forEach((section) => observer.observe(section));

    return () => observer.disconnect();
  }, []);

  // Component JSX...
}

The Intersection Observer API enables performant scroll animations without heavy animation libraries, triggering CSS animations only when sections become visible.

Tailwind CSS for Speed and Consistency

To keep development efficient and the design consistent, I used Tailwind CSS. Utility-first styling made it easy to build a clean, professional UI without bloated CSS files.

The result is a responsive layout that looks great on mobile—where most local traffic originates—while remaining easy to iterate on as the business grows.

Tailwind Configuration

Custom configuration extends Tailwind with brand colors and optimized content paths:

// tailwind.config.js
module.exports = {
  content: [
    './app/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {
      colors: {
        primary: 'rgb(0, 38, 119)',
        primaryHover: 'rgb(0, 30, 95)'
      },
      animation: {
        'fadeInUp': 'fadeInUp 0.8s ease-out',
        'fadeIn': 'fadeIn 1s ease-out'
      },
      keyframes: {
        fadeInUp: {
          '0%': { opacity: '0', transform: 'translateY(30px)' },
          '100%': { opacity: '1', transform: 'translateY(0)' }
        }
      }
    }
  }
};

Responsive Design Pattern

Tailwind's responsive utilities make it trivial to create mobile-first designs:

<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold">
  BUILDING THE FUTURE,<br />RESTORING THE PAST.
</h1>

<div className="grid lg:grid-cols-2 gap-12 items-center">
  <div>{/* Content */}</div>
  <div>{/* Image */}</div>
</div>

Core System: AI-Driven Visualization Engine (Laravel + Gemini)

Beyond the landing page itself, the platform includes a powerful AI-powered mockup generator that helps potential customers visualize their project before committing. When a lead submits their property photos through the contact form, the system generates photorealistic exterior visualizations using Google's Gemini AI.

This feature transforms the sales process from abstract quotes to visual proof, dramatically increasing conversion rates and customer confidence.

The Visualization Workflow

The AI mockup system follows a simple but powerful flow:

  1. Customer uploads a photo of their property via the contact form
  2. They select their desired color or service type (paint, deck staining, windows, etc.)
  3. The Laravel backend processes the request and calls the Gemini API
  4. AI generates a photorealistic "after" image showing the completed project
  5. Customer receives a before/after comparison to review
AI-generated before and after house visualization

Before & After: AI-generated exterior visualization showing original house and proposed paint color

Integrating Google Gemini AI

The system uses Google's Gemini 2.5 Flash Image Preview model, which excels at image-to-image generation tasks. The Laravel backend handles the entire AI workflow:

// MockupController.php - Gemini API Integration
public function generate(Request $request, Mockup $mockup)
{
    $mockup->update(['status' => 'processing']);

    // Configure Gemini endpoint
    $modelName = 'gemini-2.5-flash-image-preview';
    $endpoint = "https://generativelanguage.googleapis.com/v1beta/models/{$modelName}:generateContent";

    // Load original image
    $imagePath = $mockup->original_image_path;
    $imageFullPath = Storage::disk('public')->path($imagePath);
    $imageMime = mime_content_type($imageFullPath);
    $imageBase64 = base64_encode(file_get_contents($imageFullPath));

    // Convert hex color to descriptive name
    $colorName = $this->hexToColorName($mockup->selected_color);

    // Get type-specific prompt
    $promptText = $this->getPromptForType($mockup->type, $colorName);
}

Building the Gemini API Payload

The Gemini API requires a specific payload structure with both the text prompt and the base64-encoded image:

// Build Gemini API request
$payload = [
    "contents" => [
        [
            "parts" => [
                [
                    "text" => $promptText
                ],
                [
                    "inlineData" => [
                        "mimeType" => $imageMime,
                        "data" => $imageBase64,
                    ]
                ]
            ]
        ]
    ],
    // Critical: Request both text AND image output
    "generationConfig" => [
        "responseModalities" => ["TEXT", "IMAGE"]
    ]
];

$response = Http::withHeaders([
    'x-goog-api-key' => $apiKey,
    'Content-Type' => 'application/json',
])->post($endpoint, $payload);

The responseModalities configuration is critical—it tells Gemini to return an image instead of just a text description. Without this, the API would only return descriptive text about the changes.

Processing the AI Response

When Gemini successfully generates the mockup, it returns the image as base64-encoded data within the response. The backend extracts and stores this image:

if ($response->successful()) {
    $responseData = $response->json();

    // Find the inlineData part containing the image
    $parts = $responseData['candidates'][0]['content']['parts'];

    $imagePart = null;
    foreach ($parts as $part) {
        if (isset($part['inlineData'])) {
            $imagePart = $part;
            break;
        }
    }

    $base64Image = $imagePart['inlineData']['data'] ?? null;

    // Convert to binary and store
    $imgBinary = base64_decode($base64Image);
    $mockupImagePath = 'mockup-images/' . uniqid('mockup_') . '.png';
    Storage::disk('public')->put($mockupImagePath, $imgBinary);

    $mockup->update([
        'mockup_image_path' => $mockupImagePath,
        'status' => 'completed',
    ]);
}

Crafting Effective AI Prompts

The quality of AI-generated images heavily depends on prompt engineering. For exterior painting, the prompt needs to be specific about what to change and what to preserve:

private function getPromptForType(string $type, string $colorName): string
{
    $prompts = [
        'paint' => "Repaint the exterior of this house with {$colorName} color. " .
                  "Preserve the original architecture, windows, doors, roof, " .
                  "and surrounding landscape.",

        'deck' => "Stain the deck boards with {$colorName} color. " .
                 "Preserve the deck structure and maintain realistic " .
                 "wood grain texture. Keep surroundings unchanged.",

        'windows' => "Replace windows with {$colorName} colored frames. " .
                    "Preserve house exterior and maintain realistic " .
                    "window reflections.",
    ];

    return $prompts[$type] ?? $prompts['paint'];
}

Notice how the prompts are very specific: they tell the AI exactly what to change (the paint color) and what to preserve (architecture, landscape, etc.). This prevents the AI from hallucinating unwanted changes.

Color Name Conversion

One interesting challenge was that Gemini sometimes misinterpreted hex color codes (like #FF0000) as markdown formatting. The solution was to convert hex codes to descriptive color names:

private function hexToColorName(string $hexColor): string
{
    $hex = ltrim($hexColor, '#');

    $colorMap = [
        'CB4154' => 'brick red',
        'F8F9FA' => 'arctic white',
        '9CAF88' => 'sage green',
        // ... 50+ color mappings
    ];

    // Find closest match using RGB distance
    return $colorMap[strtoupper($hex)] ?? $this->findClosestColor($hex);
}

Real-World Impact

The AI visualization feature transformed the business model:

  • Higher Conversion Rates: Customers can see their project before committing
  • Fewer Revisions: Visual agreement upfront reduces change requests
  • Competitive Advantage: Few local contractors offer AI visualization
  • Faster Sales Cycle: Decision-making time cut in half
  • Reduced No-Shows: Committed customers visualize the outcome
Cost-Effective AI: Using Gemini's free tier (15 requests per minute), the platform handles hundreds of mockups monthly at near-zero cost. When scaled to paid tier, cost per mockup is under $0.01.

Results & Key Metrics

The platform delivers measurable improvements in user experience and conversion metrics:

  • Load Time: Initial page load under 1.2 seconds on 3G connections
  • Lighthouse Score: 95+ performance, 100 accessibility, 100 SEO
  • Mobile Experience: Fully responsive with touch-optimized interactions
  • Bundle Size: Main JavaScript bundle under 25KB (gzipped)
  • SEO: Semantic HTML with proper meta tags for local search discovery
Performance Impact: Fast load times and smooth interactions create trust, leading to higher engagement rates and more qualified leads compared to traditional contractor websites.

Technical Challenges & Solutions

1. Balancing Interactivity with Performance

Challenge: Rich animations and scroll effects can impact performance on mobile devices.

Solution: Used Intersection Observer API for scroll animations instead of scroll event listeners, reducing main thread work by 60%. CSS animations handle visual transitions, offloading work to the GPU.

2. SEO for Local Discovery

Challenge: Competing in local search results requires more than just fast pages.

Solution: Implemented structured data (JSON-LD) for LocalBusiness schema, optimized meta descriptions with location keywords, and ensured all content is server-rendered for crawler visibility.

3. Cross-Framework Migration

Challenge: Moving from Next.js to Vue 3 while preserving all functionality.

Solution: Systematic component-by-component conversion, maintaining the same Composition API patterns. All React hooks (useState, useEffect) translated directly to Vue equivalents (ref, onMounted).

Future Enhancements

  • AI Project Visualization: Allow customers to upload photos of their property and see AI-generated previews of paint colors and finishes
  • Real-Time Availability: Integrate with scheduling system to show available appointment slots
  • Customer Reviews Integration: Pull and display Google reviews directly on the site
  • Progressive Web App: Add service worker for offline functionality and "Add to Home Screen" capability
  • Analytics Dashboard: Track conversion funnel from landing page to form submission to booked appointment

Conclusion: Modern Tools for Local Business Success

Building a high-performance local business site doesn't require complex infrastructure or expensive tools. By leveraging Next.js for SEO and rendering optimization, Tailwind CSS for rapid UI development, and Vue 3 for seamless Laravel integration, I created a platform that drives real business results.

The combination of fast load times, smooth interactions, and strategic placement of conversion elements creates a user experience that builds trust and encourages action—critical for local service businesses competing in their market.

Interested in learning more? Check out the live demo or reach out to discuss your project.

Tech Stack

Vue 3
React/Next.js
Tailwind CSS
Laravel
Google Maps
Vite
TypeScript
Lighthouse
Kevin Morales
Kevin Morales
Full-Stack Developer